diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
commit | d8bbc7858622b6d9c278469aab701ca0b609cddf (patch) | |
tree | eff41dc61d9f714852212739e6b3738b82a2af87 /dom | |
parent | Releasing progress-linux version 125.0.3-1~progress7.99u1. (diff) | |
download | firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip |
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom')
956 files changed, 27826 insertions, 9873 deletions
diff --git a/dom/animation/Animation.cpp b/dom/animation/Animation.cpp index ad52495e67..fbfd689c9a 100644 --- a/dom/animation/Animation.cpp +++ b/dom/animation/Animation.cpp @@ -11,7 +11,6 @@ #include "AnimationUtils.h" #include "mozAutoDocUpdate.h" #include "mozilla/dom/AnimationBinding.h" -#include "mozilla/dom/AnimationPlaybackEvent.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/DocumentInlines.h" #include "mozilla/dom/DocumentTimeline.h" @@ -645,7 +644,8 @@ void Animation::Cancel(PostRestyleMode aPostRestyle) { } ResetFinishedPromise(); - QueuePlaybackEvent(u"cancel"_ns, GetTimelineCurrentTimeAsTimeStamp()); + QueuePlaybackEvent(nsGkAtoms::oncancel, + GetTimelineCurrentTimeAsTimeStamp()); } StickyTimeDuration activeTime = @@ -1189,7 +1189,7 @@ void Animation::Remove() { UpdateEffect(PostRestyleMode::IfNeeded); PostUpdate(); - QueuePlaybackEvent(u"remove"_ns, GetTimelineCurrentTimeAsTimeStamp()); + QueuePlaybackEvent(nsGkAtoms::onremove, GetTimelineCurrentTimeAsTimeStamp()); } bool Animation::HasLowerCompositeOrderThan(const Animation& aOther) const { @@ -1869,10 +1869,11 @@ void Animation::DoFinishNotificationImmediately(MicroTaskRunnable* aAsync) { MaybeResolveFinishedPromise(); - QueuePlaybackEvent(u"finish"_ns, AnimationTimeToTimeStamp(EffectEnd())); + QueuePlaybackEvent(nsGkAtoms::onfinish, + AnimationTimeToTimeStamp(EffectEnd())); } -void Animation::QueuePlaybackEvent(const nsAString& aName, +void Animation::QueuePlaybackEvent(nsAtom* aOnEvent, TimeStamp&& aScheduledEventTime) { // Use document for timing. // https://drafts.csswg.org/web-animations-1/#document-for-timing @@ -1886,20 +1887,19 @@ void Animation::QueuePlaybackEvent(const nsAString& aName, return; } - AnimationPlaybackEventInit init; - if (aName.EqualsLiteral("finish") || aName.EqualsLiteral("remove")) { - init.mCurrentTime = GetCurrentTimeAsDouble(); + Nullable<double> currentTime; + if (aOnEvent == nsGkAtoms::onfinish || aOnEvent == nsGkAtoms::onremove) { + currentTime = GetCurrentTimeAsDouble(); } + + Nullable<double> timelineTime; if (mTimeline) { - init.mTimelineTime = mTimeline->GetCurrentTimeAsDouble(); + timelineTime = mTimeline->GetCurrentTimeAsDouble(); } - RefPtr<AnimationPlaybackEvent> event = - AnimationPlaybackEvent::Constructor(this, aName, init); - event->SetTrusted(true); - - presContext->AnimationEventDispatcher()->QueueEvent(AnimationEventInfo( - std::move(event), std::move(aScheduledEventTime), this)); + presContext->AnimationEventDispatcher()->QueueEvent( + AnimationEventInfo(aOnEvent, currentTime, timelineTime, + std::move(aScheduledEventTime), this)); } bool Animation::IsRunningOnCompositor() const { diff --git a/dom/animation/Animation.h b/dom/animation/Animation.h index d7edfedfc2..e80370370e 100644 --- a/dom/animation/Animation.h +++ b/dom/animation/Animation.h @@ -26,6 +26,7 @@ struct JSContext; class nsCSSPropertyIDSet; class nsIFrame; class nsIGlobalObject; +class nsAtom; namespace mozilla { @@ -450,8 +451,7 @@ class Animation : public DOMEventTargetHelper, void DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag); friend class AsyncFinishNotification; void DoFinishNotificationImmediately(MicroTaskRunnable* aAsync = nullptr); - void QueuePlaybackEvent(const nsAString& aName, - TimeStamp&& aScheduledEventTime); + void QueuePlaybackEvent(nsAtom* aOnEvent, TimeStamp&& aScheduledEventTime); /** * Remove this animation from the pending animation tracker and reset diff --git a/dom/animation/AnimationEventDispatcher.cpp b/dom/animation/AnimationEventDispatcher.cpp index 86884abbe5..0ec5686d8f 100644 --- a/dom/animation/AnimationEventDispatcher.cpp +++ b/dom/animation/AnimationEventDispatcher.cpp @@ -91,10 +91,6 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AnimationEventDispatcher) ImplCycleCollectionTraverse( cb, target->mElement, "mozilla::AnimationEventDispatcher.mPendingEvents.mTarget"); - } else { - ImplCycleCollectionTraverse( - cb, info.mData.as<AnimationEventInfo::WebAnimationData>().mEvent, - "mozilla::AnimationEventDispatcher.mPendingEvents.mEvent"); } ImplCycleCollectionTraverse( cb, info.mAnimation, diff --git a/dom/animation/AnimationEventDispatcher.h b/dom/animation/AnimationEventDispatcher.h index 98d1557901..dc11972f6d 100644 --- a/dom/animation/AnimationEventDispatcher.h +++ b/dom/animation/AnimationEventDispatcher.h @@ -12,6 +12,7 @@ #include "mozilla/Attributes.h" #include "mozilla/ContentEvents.h" #include "mozilla/EventDispatcher.h" +#include "mozilla/EventListenerManager.h" #include "mozilla/Variant.h" #include "mozilla/dom/AnimationPlaybackEvent.h" #include "mozilla/dom/KeyframeEffect.h" @@ -44,7 +45,10 @@ struct AnimationEventInfo { }; struct WebAnimationData { - RefPtr<dom::AnimationPlaybackEvent> mEvent; + const RefPtr<nsAtom> mOnEvent; + const dom::Nullable<double> mCurrentTime; + const dom::Nullable<double> mTimelineTime; + const TimeStamp mEventEnqueueTimeStamp{TimeStamp::Now()}; }; using Data = Variant<CssAnimationData, CssTransitionData, WebAnimationData>; @@ -100,12 +104,15 @@ struct AnimationEventInfo { } // For web animation events - AnimationEventInfo(RefPtr<dom::AnimationPlaybackEvent>&& aEvent, + AnimationEventInfo(nsAtom* aOnEvent, + const dom::Nullable<double>& aCurrentTime, + const dom::Nullable<double>& aTimelineTime, TimeStamp&& aScheduledEventTimeStamp, dom::Animation* aAnimation) : mAnimation(aAnimation), mScheduledEventTimeStamp(std::move(aScheduledEventTimeStamp)), - mData(WebAnimationData{std::move(aEvent)}) {} + mData(WebAnimationData{RefPtr{aOnEvent}, aCurrentTime, aTimelineTime}) { + } AnimationEventInfo(const AnimationEventInfo& aOther) = delete; AnimationEventInfo& operator=(const AnimationEventInfo& aOther) = delete; @@ -137,10 +144,27 @@ struct AnimationEventInfo { // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) MOZ_CAN_RUN_SCRIPT_BOUNDARY void Dispatch(nsPresContext* aPresContext) { if (mData.is<WebAnimationData>()) { - RefPtr playbackEvent = mData.as<WebAnimationData>().mEvent; + const auto& data = mData.as<WebAnimationData>(); + EventListenerManager* elm = mAnimation->GetExistingListenerManager(); + if (!elm || !elm->HasListenersFor(data.mOnEvent)) { + return; + } + + dom::AnimationPlaybackEventInit init; + init.mCurrentTime = data.mCurrentTime; + init.mTimelineTime = data.mTimelineTime; + MOZ_ASSERT(nsDependentAtomString(data.mOnEvent).Find(u"on"_ns) == 0, + "mOnEvent atom should start with 'on'!"); + RefPtr<dom::AnimationPlaybackEvent> event = + dom::AnimationPlaybackEvent::Constructor( + mAnimation, Substring(nsDependentAtomString(data.mOnEvent), 2), + init); + event->SetTrusted(true); + event->WidgetEventPtr()->AssignEventTime( + WidgetEventTime(data.mEventEnqueueTimeStamp)); RefPtr target = mAnimation; EventDispatcher::DispatchDOMEvent(target, nullptr /* WidgetEvent */, - playbackEvent, aPresContext, + event, aPresContext, nullptr /* nsEventStatus */); return; } diff --git a/dom/animation/test/chrome.toml b/dom/animation/test/chrome.toml index fa1d21457c..0783d9bb5a 100644 --- a/dom/animation/test/chrome.toml +++ b/dom/animation/test/chrome.toml @@ -5,10 +5,6 @@ prefs = [ "layout.css.basic-shape-rect.enabled=true", "layout.css.basic-shape-xywh.enabled=true", "layout.css.individual-transform.enabled=true", - "layout.css.motion-path-basic-shapes.enabled=true", - "layout.css.motion-path-coord-box.enabled=true", - "layout.css.motion-path-offset-position.enabled=true", - "layout.css.motion-path-ray.enabled=true", ] support-files = [ "testcommon.js", diff --git a/dom/audiochannel/nsIAudioChannelAgent.idl b/dom/audiochannel/nsIAudioChannelAgent.idl index 69a8344449..2a25d2cede 100644 --- a/dom/audiochannel/nsIAudioChannelAgent.idl +++ b/dom/audiochannel/nsIAudioChannelAgent.idl @@ -36,7 +36,7 @@ interface nsIAudioChannelAgentCallback : nsISupports /** * Notified when the window volume/mute is changed */ - void windowVolumeChanged(in float aVolume, in bool aMuted); + void windowVolumeChanged(in float aVolume, in boolean aMuted); /** * Notified when the window needs to be suspended or resumed. @@ -46,7 +46,7 @@ interface nsIAudioChannelAgentCallback : nsISupports /** * Notified when the capture state is changed. */ - void windowAudioCaptureChanged(in bool aCapture); + void windowAudioCaptureChanged(in boolean aCapture); }; /** diff --git a/dom/base/AbstractRange.cpp b/dom/base/AbstractRange.cpp index 91234bf0a7..c9138a19d2 100644 --- a/dom/base/AbstractRange.cpp +++ b/dom/base/AbstractRange.cpp @@ -10,6 +10,7 @@ #include "mozilla/Assertions.h" #include "mozilla/Attributes.h" #include "mozilla/RangeUtils.h" +#include "mozilla/dom/ChildIterator.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/StaticRange.h" #include "mozilla/dom/Selection.h" @@ -87,6 +88,29 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AbstractRange) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRegisteredClosestCommonInclusiveAncestor) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END +// When aMarkDesendants is true, Set +// DescendantOfClosestCommonInclusiveAncestorForRangeInSelection flag for the +// flattened children of aNode. When aMarkDesendants is false, unset that flag +// for the flattened children of aNode. +void UpdateDescendantsInFlattenedTree(const nsIContent& aNode, + bool aMarkDesendants) { + if (!aNode.IsElement() || aNode.IsHTMLElement(nsGkAtoms::slot)) { + return; + } + + FlattenedChildIterator iter(&aNode); + for (nsIContent* child = iter.GetNextChild(); child; + child = iter.GetNextChild()) { + if (aMarkDesendants) { + child->SetDescendantOfClosestCommonInclusiveAncestorForRangeInSelection(); + } else { + child + ->ClearDescendantOfClosestCommonInclusiveAncestorForRangeInSelection(); + } + UpdateDescendantsInFlattenedTree(*child, aMarkDesendants); + } +} + void AbstractRange::MarkDescendants(const nsINode& aNode) { // Set NodeIsDescendantOfClosestCommonInclusiveAncestorForRangeInSelection on // aNode's descendants unless aNode is already marked as a range common @@ -95,10 +119,22 @@ void AbstractRange::MarkDescendants(const nsINode& aNode) { if (!aNode.IsMaybeSelected()) { // don't set the Descendant bit on |aNode| itself nsINode* node = aNode.GetNextNode(&aNode); + if (!node) { + if (aNode.GetShadowRootForSelection()) { + UpdateDescendantsInFlattenedTree(*aNode.AsContent(), true); + } + return; + } while (node) { node->SetDescendantOfClosestCommonInclusiveAncestorForRangeInSelection(); if (!node->IsClosestCommonInclusiveAncestorForRangeInSelection()) { - node = node->GetNextNode(&aNode); + if (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) { + UpdateDescendantsInFlattenedTree(*node->AsContent(), true); + // sub-tree of node has been marked already + node = node->GetNextNonChildNode(&aNode); + } else { + node = node->GetNextNode(&aNode); + } } else { // optimize: skip this sub-tree since it's marked already. node = node->GetNextNonChildNode(&aNode); @@ -116,10 +152,22 @@ void AbstractRange::UnmarkDescendants(const nsINode& aNode) { .IsDescendantOfClosestCommonInclusiveAncestorForRangeInSelection()) { // we know |aNode| doesn't have any bit set nsINode* node = aNode.GetNextNode(&aNode); + if (!node) { + if (aNode.GetShadowRootForSelection()) { + UpdateDescendantsInFlattenedTree(*aNode.AsContent(), false); + } + return; + } while (node) { node->ClearDescendantOfClosestCommonInclusiveAncestorForRangeInSelection(); if (!node->IsClosestCommonInclusiveAncestorForRangeInSelection()) { - node = node->GetNextNode(&aNode); + if (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) { + UpdateDescendantsInFlattenedTree(*node->AsContent(), false); + // sub-tree has been marked already + node = node->GetNextNonChildNode(&aNode); + } else { + node = node->GetNextNode(&aNode); + } } else { // We found an ancestor of an overlapping range, skip its descendants. node = node->GetNextNonChildNode(&aNode); @@ -185,10 +233,62 @@ bool AbstractRange::MaybeCacheToReuse(RangeType& aInstance) { return true; } -nsINode* AbstractRange::GetClosestCommonInclusiveAncestor() const { - return mIsPositioned ? nsContentUtils::GetClosestCommonInclusiveAncestor( - mStart.Container(), mEnd.Container()) - : nullptr; +nsINode* AbstractRange::GetClosestCommonInclusiveAncestor( + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) const { + if (!mIsPositioned) { + return nullptr; + } + nsINode* startContainer = + aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes + ? GetMayCrossShadowBoundaryStartContainer() + : GetStartContainer(); + nsINode* endContainer = + aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes + ? GetMayCrossShadowBoundaryEndContainer() + : GetEndContainer(); + + if (MayCrossShadowBoundary() && + aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) { + // Since both the start container and the end container are + // guaranteed to be in the same composed document. + // If one of the boundary is a document, use that document + // as the common ancestor since both nodes. + const bool oneBoundaryIsDocument = + (startContainer && startContainer->IsDocument()) || + (endContainer && endContainer->IsDocument()); + if (oneBoundaryIsDocument) { + MOZ_ASSERT_IF( + startContainer && startContainer->IsDocument(), + !endContainer || endContainer->GetComposedDoc() == startContainer); + MOZ_ASSERT_IF( + endContainer && endContainer->IsDocument(), + !startContainer || startContainer->GetComposedDoc() == endContainer); + + return startContainer ? startContainer->GetComposedDoc() + : endContainer->GetComposedDoc(); + } + + const auto rescope = [](nsINode*& aContainer) { + if (!aContainer) { + return; + } + // RangeBoundary allows the container to be shadow roots; When + // this happens, we should use the shadow host here. + if (auto* shadowRoot = ShadowRoot::FromNode(aContainer)) { + aContainer = shadowRoot->GetHost(); + return; + } + }; + + rescope(startContainer); + rescope(endContainer); + + return nsContentUtils::GetCommonFlattenedTreeAncestorForSelection( + startContainer ? startContainer->AsContent() : nullptr, + endContainer ? endContainer->AsContent() : nullptr); + } + return nsContentUtils::GetClosestCommonInclusiveAncestor(startContainer, + endContainer); } // static @@ -237,9 +337,28 @@ nsresult AbstractRange::SetStartAndEndInternal( return NS_ERROR_DOM_INDEX_SIZE_ERR; } - // If they have different root, this should be collapsed at the end point. + // Different root if (newStartRoot != newEndRoot) { - aRange->DoSetRange(aEndBoundary, aEndBoundary, newEndRoot); + if (aRange->IsStaticRange()) { + // StaticRange allows nodes in different trees, so set start and end + // accordingly + aRange->DoSetRange(aStartBoundary, aEndBoundary, newEndRoot); + } else { + MOZ_ASSERT(aRange->IsDynamicRange()); + // In contrast, nsRange keeps both. It has a pair of start and end + // which they have been collapsed to one end, and it also may have a pair + // of start and end which are the original value. + aRange->DoSetRange(aEndBoundary, aEndBoundary, newEndRoot); + + // Don't create the cross shadow bounday range if the one of the roots is + // an UA widget regardless whether the boundaries are allowed to cross + // shadow boundary or not. + if (!IsRootUAWidget(newStartRoot) && !IsRootUAWidget(newEndRoot)) { + aRange->AsDynamicRange() + ->CreateOrUpdateCrossShadowBoundaryRangeIfNeeded(aStartBoundary, + aEndBoundary); + } + } return NS_OK; } @@ -274,7 +393,10 @@ void AbstractRange::RegisterSelection(Selection& aSelection) { bool isFirstSelection = mSelections.IsEmpty(); mSelections.AppendElement(&aSelection); if (isFirstSelection && !mRegisteredClosestCommonInclusiveAncestor) { - nsINode* commonAncestor = GetClosestCommonInclusiveAncestor(); + nsINode* commonAncestor = GetClosestCommonInclusiveAncestor( + StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() + ? AllowRangeCrossShadowBoundary::Yes + : AllowRangeCrossShadowBoundary::No); MOZ_ASSERT(commonAncestor, "unexpected disconnected nodes"); RegisterClosestCommonInclusiveAncestor(commonAncestor); } @@ -358,7 +480,8 @@ void AbstractRange::UnregisterClosestCommonInclusiveAncestor( void AbstractRange::UpdateCommonAncestorIfNecessary() { nsINode* oldCommonAncestor = mRegisteredClosestCommonInclusiveAncestor; - nsINode* newCommonAncestor = GetClosestCommonInclusiveAncestor(); + nsINode* newCommonAncestor = + GetClosestCommonInclusiveAncestor(AllowRangeCrossShadowBoundary::Yes); if (newCommonAncestor != oldCommonAncestor) { if (oldCommonAncestor) { UnregisterClosestCommonInclusiveAncestor(oldCommonAncestor, false); @@ -379,6 +502,59 @@ void AbstractRange::UpdateCommonAncestorIfNecessary() { } } +const RangeBoundary& AbstractRange::MayCrossShadowBoundaryStartRef() const { + return IsDynamicRange() ? AsDynamicRange()->MayCrossShadowBoundaryStartRef() + : mStart; +} + +const RangeBoundary& AbstractRange::MayCrossShadowBoundaryEndRef() const { + return IsDynamicRange() ? AsDynamicRange()->MayCrossShadowBoundaryEndRef() + : mEnd; +} + +nsIContent* AbstractRange::GetMayCrossShadowBoundaryChildAtStartOffset() const { + return IsDynamicRange() + ? AsDynamicRange()->GetMayCrossShadowBoundaryChildAtStartOffset() + : mStart.GetChildAtOffset(); +} + +nsIContent* AbstractRange::GetMayCrossShadowBoundaryChildAtEndOffset() const { + return IsDynamicRange() + ? AsDynamicRange()->GetMayCrossShadowBoundaryChildAtEndOffset() + : mEnd.GetChildAtOffset(); +} + +nsINode* AbstractRange::GetMayCrossShadowBoundaryStartContainer() const { + return IsDynamicRange() + ? AsDynamicRange()->GetMayCrossShadowBoundaryStartContainer() + : mStart.Container(); +} + +nsINode* AbstractRange::GetMayCrossShadowBoundaryEndContainer() const { + return IsDynamicRange() + ? AsDynamicRange()->GetMayCrossShadowBoundaryEndContainer() + : mEnd.Container(); +} + +bool AbstractRange::MayCrossShadowBoundary() const { + return IsDynamicRange() ? !!AsDynamicRange()->GetCrossShadowBoundaryRange() + : false; +} + +uint32_t AbstractRange::MayCrossShadowBoundaryStartOffset() const { + return IsDynamicRange() + ? AsDynamicRange()->MayCrossShadowBoundaryStartOffset() + : static_cast<uint32_t>(*mStart.Offset( + RangeBoundary::OffsetFilter::kValidOrInvalidOffsets)); +} + +uint32_t AbstractRange::MayCrossShadowBoundaryEndOffset() const { + return IsDynamicRange() + ? AsDynamicRange()->MayCrossShadowBoundaryEndOffset() + : static_cast<uint32_t>(*mEnd.Offset( + RangeBoundary::OffsetFilter::kValidOrInvalidOffsets)); +} + nsINode* AbstractRange::GetParentObject() const { return mOwner; } JSObject* AbstractRange::WrapObject(JSContext* aCx, @@ -395,4 +571,12 @@ void AbstractRange::ClearForReuse() { mCalledByJS = false; } +/*static*/ +bool AbstractRange::IsRootUAWidget(const nsINode* aRoot) { + MOZ_ASSERT(aRoot); + if (const ShadowRoot* shadowRoot = ShadowRoot::FromNode(aRoot)) { + return shadowRoot->IsUAWidget(); + } + return false; +} } // namespace mozilla::dom diff --git a/dom/base/AbstractRange.h b/dom/base/AbstractRange.h index c70aaf19ec..2f9b59158a 100644 --- a/dom/base/AbstractRange.h +++ b/dom/base/AbstractRange.h @@ -31,10 +31,15 @@ class Document; class Selection; class StaticRange; +enum class AllowRangeCrossShadowBoundary : bool { No, Yes }; + class AbstractRange : public nsISupports, public nsWrapperCache, // For linking together selection-associated ranges. public mozilla::LinkedListElement<AbstractRange> { + using AllowRangeCrossShadowBoundary = + mozilla::dom::AllowRangeCrossShadowBoundary; + protected: explicit AbstractRange(nsINode* aNode, bool aIsDynamicRange); virtual ~AbstractRange(); @@ -51,18 +56,33 @@ class AbstractRange : public nsISupports, NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AbstractRange) + /** + * All of the MayCrossShadowBoundary* methods are used to get the boundary + * endpoints that cross shadow boundaries. They would return + * the same value as the non-MayCrossShadowBoundary* methods if the range + * boundaries don't cross shadow boundaries. + */ const RangeBoundary& StartRef() const { return mStart; } + const RangeBoundary& MayCrossShadowBoundaryStartRef() const; + const RangeBoundary& EndRef() const { return mEnd; } + const RangeBoundary& MayCrossShadowBoundaryEndRef() const; nsIContent* GetChildAtStartOffset() const { return mStart.GetChildAtOffset(); } + nsIContent* GetMayCrossShadowBoundaryChildAtStartOffset() const; + nsIContent* GetChildAtEndOffset() const { return mEnd.GetChildAtOffset(); } + nsIContent* GetMayCrossShadowBoundaryChildAtEndOffset() const; + bool IsPositioned() const { return mIsPositioned; } /** * https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor */ - nsINode* GetClosestCommonInclusiveAncestor() const; + nsINode* GetClosestCommonInclusiveAncestor( + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No) const; // WebIDL @@ -75,7 +95,12 @@ class AbstractRange : public nsISupports, // `IsPositioned()` directly. nsINode* GetStartContainer() const { return mStart.Container(); } + nsINode* GetMayCrossShadowBoundaryStartContainer() const; + nsINode* GetEndContainer() const { return mEnd.Container(); } + nsINode* GetMayCrossShadowBoundaryEndContainer() const; + + bool MayCrossShadowBoundary() const; Document* GetComposedDocOfContainers() const { return mStart.Container() ? mStart.Container()->GetComposedDoc() : nullptr; @@ -86,12 +111,15 @@ class AbstractRange : public nsISupports, return static_cast<uint32_t>( *mStart.Offset(RangeBoundary::OffsetFilter::kValidOrInvalidOffsets)); } + uint32_t MayCrossShadowBoundaryStartOffset() const; // FYI: Returns 0 if it's not positioned. uint32_t EndOffset() const { return static_cast<uint32_t>( *mEnd.Offset(RangeBoundary::OffsetFilter::kValidOrInvalidOffsets)); } + uint32_t MayCrossShadowBoundaryEndOffset() const; + bool Collapsed() const { return !mIsPositioned || (mStart.Container() == mEnd.Container() && StartOffset() == EndOffset()); @@ -132,6 +160,11 @@ class AbstractRange : public nsISupports, */ bool IsInSelection(const mozilla::dom::Selection& aSelection) const; + /** + * Return true if aRoot is a UA shadow root. + */ + static bool IsRootUAWidget(const nsINode* aRoot); + protected: template <typename SPT, typename SRT, typename EPT, typename ERT, typename RangeType> diff --git a/dom/base/BodyUtil.cpp b/dom/base/BodyUtil.cpp index e8de3d18ec..45ca58e2d9 100644 --- a/dom/base/BodyUtil.cpp +++ b/dom/base/BodyUtil.cpp @@ -422,9 +422,10 @@ already_AddRefed<FormData> BodyUtil::ConsumeFormData( if (isValidUrlEncodedMimeType) { RefPtr<FormData> fd = new FormData(aParent); DebugOnly<bool> status = URLParams::Parse( - aStr, true, [&fd](const nsAString& aName, const nsAString& aValue) { - ErrorResult rv; - fd->Append(aName, aValue, rv); + aStr, true, [&fd](const nsACString& aName, const nsACString& aValue) { + IgnoredErrorResult rv; + fd->Append(NS_ConvertUTF8toUTF16(aName), + NS_ConvertUTF8toUTF16(aValue), rv); MOZ_ASSERT(!rv.Failed()); return true; }); diff --git a/dom/base/ChromeUtils.cpp b/dom/base/ChromeUtils.cpp index 0df1cd3c9b..407f33e044 100644 --- a/dom/base/ChromeUtils.cpp +++ b/dom/base/ChromeUtils.cpp @@ -1289,9 +1289,10 @@ void ChromeUtils::GetBaseDomainFromPartitionKey(dom::GlobalObject& aGlobal, nsString scheme; nsString pkBaseDomain; int32_t port; + bool ancestor; - if (!mozilla::OriginAttributes::ParsePartitionKey(aPartitionKey, scheme, - pkBaseDomain, port)) { + if (!mozilla::OriginAttributes::ParsePartitionKey( + aPartitionKey, scheme, pkBaseDomain, port, ancestor)) { aRv.Throw(NS_ERROR_FAILURE); return; } @@ -1317,7 +1318,10 @@ void ChromeUtils::GetPartitionKeyFromURL(dom::GlobalObject& aGlobal, } mozilla::OriginAttributes attrs; - attrs.SetPartitionKey(uri); + // For now, uses assume the partition key is cross-site. + // We will need to not make this assumption to allow access + // to same-site partitioned cookies in the cookie extension API. + attrs.SetPartitionKey(uri, false); aPartitionKey = attrs.mPartitionKey; } @@ -1515,10 +1519,10 @@ already_AddRefed<Promise> ChromeUtils::RequestProcInfo(GlobalObject& aGlobal, // DOM windows. /* aUtilityInfo = */ std::move(utilityActors), /* aChild = */ 0 // Without a ContentProcess, no ChildId. -#ifdef XP_MACOSX +#ifdef XP_DARWIN , /* aChildTask = */ aGeckoProcess->GetChildTask() -#endif // XP_MACOSX +#endif // XP_DARWIN ); }); @@ -1617,10 +1621,10 @@ already_AddRefed<Promise> ChromeUtils::RequestProcInfo(GlobalObject& aGlobal, /* aWindowInfo = */ std::move(windows), /* aUtilityInfo = */ nsTArray<UtilityInfo>(), /* aChild = */ contentParent->ChildID() -#ifdef XP_MACOSX +#ifdef XP_DARWIN , /* aChildTask = */ contentParent->Process()->GetChildTask() -#endif // XP_MACOSX +#endif // XP_DARWIN ); } diff --git a/dom/base/ContentIterator.cpp b/dom/base/ContentIterator.cpp index 6819353520..0b405f0348 100644 --- a/dom/base/ContentIterator.cpp +++ b/dom/base/ContentIterator.cpp @@ -7,6 +7,7 @@ #include "ContentIterator.h" #include "mozilla/Assertions.h" +#include "mozilla/dom/ShadowRoot.h" #include "mozilla/DebugOnly.h" #include "mozilla/RangeBoundary.h" #include "mozilla/RangeUtils.h" @@ -26,6 +27,69 @@ using namespace dom; __VA_ARGS__); \ template aResultType ContentIteratorBase<nsINode*>::aMethodName(__VA_ARGS__) +/** + * IteratorHelpers contains the static methods to help extra values + * based on whether or not the iterator allows to iterate nodes cross the shadow + * boundary. + */ +struct IteratorHelpers { + IteratorHelpers() = delete; + + static nsINode* GetStartContainer(AbstractRange* aRange, + bool aAllowCrossShadowBoundary) { + MOZ_ASSERT(aRange); + return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && + aAllowCrossShadowBoundary) + ? aRange->GetMayCrossShadowBoundaryStartContainer() + : aRange->GetStartContainer(); + } + + static int32_t StartOffset(AbstractRange* aRange, + bool aAllowCrossShadowBoundary) { + MOZ_ASSERT(aRange); + return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && + aAllowCrossShadowBoundary) + ? aRange->MayCrossShadowBoundaryStartOffset() + : aRange->StartOffset(); + } + + static nsINode* GetEndContainer(AbstractRange* aRange, + bool aAllowCrossShadowBoundary) { + MOZ_ASSERT(aRange); + return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && + aAllowCrossShadowBoundary) + ? aRange->GetMayCrossShadowBoundaryEndContainer() + : aRange->GetEndContainer(); + } + + static int32_t EndOffset(AbstractRange* aRange, + bool aAllowCrossShadowBoundary) { + MOZ_ASSERT(aRange); + return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && + aAllowCrossShadowBoundary) + ? aRange->MayCrossShadowBoundaryEndOffset() + : aRange->EndOffset(); + } + + // FIXME(sefeng): This doesn't work with slots / flattened tree. + static nsINode* GetParentNode(nsINode& aNode, + bool aAllowCrossShadowBoundary) { + return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && + aAllowCrossShadowBoundary) + ? aNode.GetParentOrShadowHostNode() + : aNode.GetParentNode(); + } + + static ShadowRoot* GetShadowRoot(const nsINode* aNode, + bool aAllowCrossShadowBoundary) { + MOZ_ASSERT(aNode); + return (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && + aAllowCrossShadowBoundary) + ? aNode->GetShadowRootForSelection() + : nullptr; + } +}; + static bool ComparePostMode(const RawRangeBoundary& aStart, const RawRangeBoundary& aEnd, nsINode& aNode) { nsINode* parent = aNode.GetParentNode(); @@ -541,17 +605,45 @@ nsINode* ContentIteratorBase<NodeType>::GetDeepFirstChild(nsINode* aRoot) { // static template <typename NodeType> nsIContent* ContentIteratorBase<NodeType>::GetDeepFirstChild( - nsIContent* aRoot) { + nsIContent* aRoot, bool aAllowCrossShadowBoundary) { if (NS_WARN_IF(!aRoot)) { return nullptr; } nsIContent* node = aRoot; - nsIContent* child = node->GetFirstChild(); + nsIContent* child = nullptr; + + if (ShadowRoot* shadowRoot = + IteratorHelpers::GetShadowRoot(node, aAllowCrossShadowBoundary)) { + // When finding the deepest child of node, if this node has a + // web exposed shadow root, we use this shadow root to find the deepest + // child. + // If the first candidate should be a slotted content, + // shadowRoot->GetFirstChild() should be able to return the <slot> element. + // It's probably correct I think. Then it's up to the caller of this + // iterator to decide whether to use the slot's assigned nodes or not. + MOZ_ASSERT(aAllowCrossShadowBoundary); + child = shadowRoot->GetFirstChild(); + } else { + child = node->GetFirstChild(); + } while (child) { node = child; - child = node->GetFirstChild(); + if (ShadowRoot* shadowRoot = + IteratorHelpers::GetShadowRoot(node, aAllowCrossShadowBoundary)) { + // When finding the deepest child of node, if this node has a + // web exposed shadow root, we use this shadow root to find the deepest + // child. + // If the first candidate should be a slotted content, + // shadowRoot->GetFirstChild() should be able to return the <slot> + // element. It's probably correct I think. Then it's up to the caller of + // this iterator to decide whether to use the slot's assigned nodes or + // not. + child = shadowRoot->GetFirstChild(); + } else { + child = node->GetFirstChild(); + } } return node; @@ -569,23 +661,41 @@ nsINode* ContentIteratorBase<NodeType>::GetDeepLastChild(nsINode* aRoot) { // static template <typename NodeType> -nsIContent* ContentIteratorBase<NodeType>::GetDeepLastChild(nsIContent* aRoot) { +nsIContent* ContentIteratorBase<NodeType>::GetDeepLastChild( + nsIContent* aRoot, bool aAllowCrossShadowBoundary) { if (NS_WARN_IF(!aRoot)) { return nullptr; } nsIContent* node = aRoot; - while (node->HasChildren()) { - nsIContent* child = node->GetLastChild(); - node = child; + + ShadowRoot* shadowRoot = + IteratorHelpers::GetShadowRoot(node, aAllowCrossShadowBoundary); + // FIXME(sefeng): This doesn't work with slots / flattened tree. + while (node->HasChildren() || (shadowRoot && shadowRoot->HasChildren())) { + if (node->HasChildren()) { + node = node->GetLastChild(); + } else { + MOZ_ASSERT(shadowRoot); + // If this node doesn't have a child, but it's also a shadow host + // that can be selected, we go into this shadow tree. + node = shadowRoot->GetLastChild(); + } + shadowRoot = + IteratorHelpers::GetShadowRoot(node, aAllowCrossShadowBoundary); } return node; } -// Get the next sibling, or parent's next sibling, or grandpa's next sibling... +// Get the next sibling, or parent's next sibling, or shadow host's next +// sibling (when aAllowCrossShadowBoundary is true), or grandpa's next +// sibling... +// // static +// template <typename NodeType> -nsIContent* ContentIteratorBase<NodeType>::GetNextSibling(nsINode* aNode) { +nsIContent* ContentIteratorBase<NodeType>::GetNextSibling( + nsINode* aNode, bool aAllowCrossShadowBoundary) { if (NS_WARN_IF(!aNode)) { return nullptr; } @@ -594,18 +704,32 @@ nsIContent* ContentIteratorBase<NodeType>::GetNextSibling(nsINode* aNode) { return next; } - nsINode* parent = aNode->GetParentNode(); + nsINode* parent = + IteratorHelpers::GetParentNode(*aNode, aAllowCrossShadowBoundary); if (NS_WARN_IF(!parent)) { return nullptr; } - return ContentIteratorBase::GetNextSibling(parent); + if (aAllowCrossShadowBoundary) { + // This is temporary solution. + // For shadow root, instead of getting to the sibling of the parent + // directly, we need to get into the light tree of the parent to handle + // slotted contents. + if (ShadowRoot* shadowRoot = ShadowRoot::FromNode(aNode)) { + if (nsIContent* child = parent->GetFirstChild()) { + return child; + } + } + } + + return ContentIteratorBase::GetNextSibling(parent, aAllowCrossShadowBoundary); } -// Get the prev sibling, or parent's prev sibling, or grandpa's prev sibling... -// static +// Get the prev sibling, or parent's prev sibling, or shadow host's prev sibling +// (when aAllowCrossShadowBoundary is true), or grandpa's prev sibling... static template <typename NodeType> -nsIContent* ContentIteratorBase<NodeType>::GetPrevSibling(nsINode* aNode) { +nsIContent* ContentIteratorBase<NodeType>::GetPrevSibling( + nsINode* aNode, bool aAllowCrossShadowBoundary) { if (NS_WARN_IF(!aNode)) { return nullptr; } @@ -614,12 +738,13 @@ nsIContent* ContentIteratorBase<NodeType>::GetPrevSibling(nsINode* aNode) { return prev; } - nsINode* parent = aNode->GetParentNode(); + nsINode* parent = + IteratorHelpers::GetParentNode(*aNode, aAllowCrossShadowBoundary); if (NS_WARN_IF(!parent)) { return nullptr; } - return ContentIteratorBase::GetPrevSibling(parent); + return ContentIteratorBase::GetPrevSibling(parent, aAllowCrossShadowBoundary); } template <typename NodeType> @@ -860,19 +985,41 @@ nsresult ContentSubtreeIterator::Init(const RawRangeBoundary& aStartBoundary, return InitWithRange(); } +nsresult ContentSubtreeIterator::InitWithAllowCrossShadowBoundary( + AbstractRange* aRange) { + MOZ_ASSERT(aRange); + + if (NS_WARN_IF(!aRange->IsPositioned())) { + return NS_ERROR_INVALID_ARG; + } + + mRange = aRange; + + mAllowCrossShadowBoundary = AllowRangeCrossShadowBoundary::Yes; + return InitWithRange(); +} + void ContentSubtreeIterator::CacheInclusiveAncestorsOfEndContainer() { mInclusiveAncestorsOfEndContainer.Clear(); - nsINode* const endContainer = mRange->GetEndContainer(); + nsINode* const endContainer = + IteratorHelpers::GetEndContainer(mRange, IterAllowCrossShadowBoundary()); nsIContent* endNode = endContainer->IsContent() ? endContainer->AsContent() : nullptr; while (endNode) { mInclusiveAncestorsOfEndContainer.AppendElement(endNode); - endNode = endNode->GetParent(); + // Cross the boundary for contents in shadow tree. + nsINode* parent = IteratorHelpers::GetParentNode( + *endNode, IterAllowCrossShadowBoundary()); + if (!parent || !parent->IsContent()) { + break; + } + endNode = parent->AsContent(); } } nsIContent* ContentSubtreeIterator::DetermineCandidateForFirstContent() const { - nsINode* startContainer = mRange->GetStartContainer(); + nsINode* startContainer = IteratorHelpers::GetStartContainer( + mRange, IterAllowCrossShadowBoundary()); nsIContent* firstCandidate = nullptr; // find first node in range nsINode* node = nullptr; @@ -880,9 +1027,14 @@ nsIContent* ContentSubtreeIterator::DetermineCandidateForFirstContent() const { // no children, start at the node itself node = startContainer; } else { - nsIContent* child = mRange->GetChildAtStartOffset(); - MOZ_ASSERT(child == - startContainer->GetChildAt_Deprecated(mRange->StartOffset())); + nsIContent* child = + IterAllowCrossShadowBoundary() + ? mRange->GetMayCrossShadowBoundaryChildAtStartOffset() + : mRange->GetChildAtStartOffset(); + + MOZ_ASSERT(child == startContainer->GetChildAt_Deprecated( + IteratorHelpers::StartOffset( + mRange, IterAllowCrossShadowBoundary()))); if (!child) { // offset after last child node = startContainer; @@ -893,11 +1045,13 @@ nsIContent* ContentSubtreeIterator::DetermineCandidateForFirstContent() const { if (!firstCandidate) { // then firstCandidate is next node after node - firstCandidate = ContentIteratorBase::GetNextSibling(node); + firstCandidate = ContentIteratorBase::GetNextSibling( + node, IterAllowCrossShadowBoundary()); } if (firstCandidate) { - firstCandidate = ContentIteratorBase::GetDeepFirstChild(firstCandidate); + firstCandidate = ContentIteratorBase::GetDeepFirstChild( + firstCandidate, IterAllowCrossShadowBoundary()); } return firstCandidate; @@ -926,9 +1080,12 @@ nsIContent* ContentSubtreeIterator::DetermineFirstContent() const { nsIContent* ContentSubtreeIterator::DetermineCandidateForLastContent() const { nsIContent* lastCandidate{nullptr}; - nsINode* endContainer = mRange->GetEndContainer(); + nsINode* endContainer = + IteratorHelpers::GetEndContainer(mRange, IterAllowCrossShadowBoundary()); // now to find the last node - int32_t offset = mRange->EndOffset(); + int32_t offset = + IteratorHelpers::EndOffset(mRange, IterAllowCrossShadowBoundary()); + int32_t numChildren = endContainer->GetChildCount(); nsINode* node = nullptr; @@ -939,7 +1096,9 @@ nsIContent* ContentSubtreeIterator::DetermineCandidateForLastContent() const { if (!offset || !numChildren) { node = endContainer; } else { - lastCandidate = mRange->EndRef().Ref(); + lastCandidate = IterAllowCrossShadowBoundary() + ? mRange->MayCrossShadowBoundaryEndRef().Ref() + : mRange->EndRef().Ref(); MOZ_ASSERT(lastCandidate == endContainer->GetChildAt_Deprecated(--offset)); NS_ASSERTION(lastCandidate, "tree traversal trouble in ContentSubtreeIterator::Init"); @@ -947,11 +1106,13 @@ nsIContent* ContentSubtreeIterator::DetermineCandidateForLastContent() const { if (!lastCandidate) { // then lastCandidate is prev node before node - lastCandidate = ContentIteratorBase::GetPrevSibling(node); + lastCandidate = ContentIteratorBase::GetPrevSibling( + node, IterAllowCrossShadowBoundary()); } if (lastCandidate) { - lastCandidate = ContentIteratorBase::GetDeepLastChild(lastCandidate); + lastCandidate = ContentIteratorBase::GetDeepLastChild( + lastCandidate, IterAllowCrossShadowBoundary()); } return lastCandidate; @@ -962,11 +1123,17 @@ nsresult ContentSubtreeIterator::InitWithRange() { MOZ_ASSERT(mRange->IsPositioned()); // get the start node and offset, convert to nsINode - mClosestCommonInclusiveAncestor = mRange->GetClosestCommonInclusiveAncestor(); - nsINode* startContainer = mRange->GetStartContainer(); - const int32_t startOffset = mRange->StartOffset(); - nsINode* endContainer = mRange->GetEndContainer(); - const int32_t endOffset = mRange->EndOffset(); + mClosestCommonInclusiveAncestor = + mRange->GetClosestCommonInclusiveAncestor(mAllowCrossShadowBoundary); + + nsINode* startContainer = IteratorHelpers::GetStartContainer( + mRange, IterAllowCrossShadowBoundary()); + const int32_t startOffset = + IteratorHelpers::StartOffset(mRange, IterAllowCrossShadowBoundary()); + nsINode* endContainer = + IteratorHelpers::GetEndContainer(mRange, IterAllowCrossShadowBoundary()); + const int32_t endOffset = + IteratorHelpers::EndOffset(mRange, IterAllowCrossShadowBoundary()); MOZ_ASSERT(mClosestCommonInclusiveAncestor && startContainer && endContainer); // Bug 767169 MOZ_ASSERT(uint32_t(startOffset) <= startContainer->Length() && @@ -1044,14 +1211,23 @@ void ContentSubtreeIterator::Next() { return; } - nsINode* nextNode = ContentIteratorBase::GetNextSibling(mCurNode); + nsINode* nextNode = ContentIteratorBase::GetNextSibling( + mCurNode, IterAllowCrossShadowBoundary()); + NS_ASSERTION(nextNode, "No next sibling!?! This could mean deadlock!"); int32_t i = mInclusiveAncestorsOfEndContainer.IndexOf(nextNode); while (i != -1) { // as long as we are finding ancestors of the endpoint of the range, // dive down into their children - nextNode = nextNode->GetFirstChild(); + ShadowRoot* root = IteratorHelpers::GetShadowRoot( + Element::FromNode(nextNode), IterAllowCrossShadowBoundary()); + if (!root) { + nextNode = nextNode->GetFirstChild(); + } else { + nextNode = mRange->MayCrossShadowBoundary() ? root->GetFirstChild() + : nextNode->GetFirstChild(); + } NS_ASSERTION(nextNode, "Iterator error, expected a child node!"); // should be impossible to get a null pointer. If we went all the way @@ -1098,7 +1274,8 @@ nsresult ContentSubtreeIterator::PositionAt(nsINode* aCurNode) { nsIContent* ContentSubtreeIterator::GetTopAncestorInRange( nsINode* aNode) const { - if (!aNode || !aNode->GetParentNode()) { + if (!aNode || + !IteratorHelpers::GetParentNode(*aNode, IterAllowCrossShadowBoundary())) { return nullptr; } @@ -1114,15 +1291,23 @@ nsIContent* ContentSubtreeIterator::GetTopAncestorInRange( return nullptr; } + nsIContent* lastContentInShadowTree = nullptr; while (content) { - nsIContent* parent = content->GetParent(); + nsINode* parent = IteratorHelpers::GetParentNode( + *content, IterAllowCrossShadowBoundary()); + // content always has a parent. If its parent is the root, however -- // i.e., either it's not content, or it is content but its own parent is // null -- then we're finished, since we don't go up to the root. // + // Caveat: If iteration crossing shadow boundary is allowed + // and the root is a shadow root, we keep going up to the + // shadow host and continue. + // // We have to special-case this because CompareNodeToRange treats the root // node differently -- see bug 765205. - if (!parent || !parent->GetParentNode()) { + if (!parent || !IteratorHelpers::GetParentNode( + *parent, IterAllowCrossShadowBoundary())) { return content; } @@ -1130,10 +1315,28 @@ nsIContent* ContentSubtreeIterator::GetTopAncestorInRange( RangeUtils::IsNodeContainedInRange(*parent, mRange); MOZ_ALWAYS_TRUE(isNodeContainedInRange); if (!isNodeContainedInRange.value()) { + if (IterAllowCrossShadowBoundary() && content->IsShadowRoot()) { + MOZ_ASSERT(parent->GetShadowRoot() == content); + // host element is not in range, the last content in tree + // should be the ancestor. + MOZ_ASSERT(lastContentInShadowTree); + return lastContentInShadowTree; + } return content; } - content = parent; + // When we cross the boundary, we keep a reference to the + // last content that is in tree, because if we later + // find the shadow host element is not in the range, that means + // the last content in the tree should be top ancestor in range. + // + // Using shadow root doesn't make sense here because it doesn't + // represent a actual content. + if (IterAllowCrossShadowBoundary() && parent->IsShadowRoot()) { + lastContentInShadowTree = content; + } + + content = parent->AsContent(); } MOZ_CRASH("This should only be possible if aNode was null"); diff --git a/dom/base/ContentIterator.h b/dom/base/ContentIterator.h index b645c4147e..67962d41d5 100644 --- a/dom/base/ContentIterator.h +++ b/dom/base/ContentIterator.h @@ -82,15 +82,26 @@ class ContentIteratorBase { // Recursively get the deepest first/last child of aRoot. This will return // aRoot itself if it has no children. static nsINode* GetDeepFirstChild(nsINode* aRoot); - static nsIContent* GetDeepFirstChild(nsIContent* aRoot); + // If aAllowCrossShadowBoundary is true, it'll continue with the shadow tree + // when it reaches to a shadow host. + static nsIContent* GetDeepFirstChild(nsIContent* aRoot, + bool aAllowCrossShadowBoundary); static nsINode* GetDeepLastChild(nsINode* aRoot); - static nsIContent* GetDeepLastChild(nsIContent* aRoot); + // If aAllowCrossShadowBoundary is true, it'll continue with the shadow tree + // when it reaches to a shadow host. + static nsIContent* GetDeepLastChild(nsIContent* aRoot, + bool aAllowCrossShadowBoundary); // Get the next/previous sibling of aNode, or its parent's, or grandparent's, // etc. Returns null if aNode and all its ancestors have no next/previous // sibling. - static nsIContent* GetNextSibling(nsINode* aNode); - static nsIContent* GetPrevSibling(nsINode* aNode); + // + // If aAllowCrossShadowBoundary is true, it'll continue with the shadow host + // when it reaches to a shadow root. + static nsIContent* GetNextSibling(nsINode* aNode, + bool aAllowCrossShadowBoundary = false); + static nsIContent* GetPrevSibling(nsINode* aNode, + bool aAllowCrossShadowBoundary = false); nsINode* NextNode(nsINode* aNode); nsINode* PrevNode(nsINode* aNode); @@ -219,6 +230,29 @@ class ContentSubtreeIterator final : public SafeContentIteratorBase { virtual nsresult Init(nsINode* aRoot) override; virtual nsresult Init(dom::AbstractRange* aRange) override; + + /** + * Initialize the iterator with aRange that does correct things + * when the aRange's start and/or the end containers are + * in shadow dom. + * + * If both start and end containers are in light dom, the iterator + * won't do anything special. + * + * When the start container is in shadow dom, the iterator can + * find the correct start node by crossing the shadow + * boundary when needed. + * + * When the end container is in shadow dom, the iterator can find + * the correct end node by crossing the shadow boundary when + * needed. Also when the next node is an ancestor of + * the end node, it can correctly iterate into the + * subtree of it by crossing the shadow boundary. + * + * Examples of what nodes will be returned can be found + * at test_content_iterator_subtree_shadow_tree.html. + */ + nsresult InitWithAllowCrossShadowBoundary(dom::AbstractRange* aRange); virtual nsresult Init(nsINode* aStartContainer, uint32_t aStartOffset, nsINode* aEndContainer, uint32_t aEndOffset) override; virtual nsresult Init(const RawRangeBoundary& aStartBoundary, @@ -276,10 +310,18 @@ class ContentSubtreeIterator final : public SafeContentIteratorBase { // the range's start and end nodes will never be considered "in" it. nsIContent* GetTopAncestorInRange(nsINode* aNode) const; + bool IterAllowCrossShadowBoundary() const { + return mAllowCrossShadowBoundary == dom::AllowRangeCrossShadowBoundary::Yes; + } + RefPtr<dom::AbstractRange> mRange; // See <https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor>. AutoTArray<nsIContent*, 8> mInclusiveAncestorsOfEndContainer; + + // Whether this iterator allows to iterate nodes across shadow boundary. + dom::AllowRangeCrossShadowBoundary mAllowCrossShadowBoundary = + dom::AllowRangeCrossShadowBoundary::No; }; } // namespace mozilla diff --git a/dom/base/ContentProcessMessageManager.cpp b/dom/base/ContentProcessMessageManager.cpp index 7661d1036f..9723341782 100644 --- a/dom/base/ContentProcessMessageManager.cpp +++ b/dom/base/ContentProcessMessageManager.cpp @@ -104,6 +104,7 @@ JSObject* ContentProcessMessageManager::GetOrCreateWrapper() { jsapi.Init(); if (!GetOrCreateDOMReflectorNoWrap(jsapi.cx(), this, &val)) { + JS_ClearPendingException(jsapi.cx()); return nullptr; } } @@ -111,11 +112,15 @@ JSObject* ContentProcessMessageManager::GetOrCreateWrapper() { return &val.toObject(); } -void ContentProcessMessageManager::LoadScript(const nsAString& aURL) { +bool ContentProcessMessageManager::LoadScript(const nsAString& aURL) { Init(); - JS::Rooted<JSObject*> messageManager(mozilla::dom::RootingCx(), - GetOrCreateWrapper()); - LoadScriptInternal(messageManager, aURL, true); + JSObject* wrapper = GetOrCreateWrapper(); + if (wrapper) { + JS::Rooted<JSObject*> messageManager(mozilla::dom::RootingCx(), wrapper); + LoadScriptInternal(messageManager, aURL, true); + return true; + } + return false; } void ContentProcessMessageManager::SetInitialProcessData( diff --git a/dom/base/ContentProcessMessageManager.h b/dom/base/ContentProcessMessageManager.h index b7c54ba452..0d437b5e50 100644 --- a/dom/base/ContentProcessMessageManager.h +++ b/dom/base/ContentProcessMessageManager.h @@ -58,7 +58,7 @@ class ContentProcessMessageManager : public nsIMessageSender, virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; - JSObject* GetOrCreateWrapper(); + [[nodiscard]] JSObject* GetOrCreateWrapper(); using MessageManagerGlobal::AddMessageListener; using MessageManagerGlobal::AddWeakMessageListener; @@ -84,7 +84,7 @@ class ContentProcessMessageManager : public nsIMessageSender, return xpc::NativeGlobal(xpc::PrivilegedJunkScope()); } - virtual void LoadScript(const nsAString& aURL); + [[nodiscard]] virtual bool LoadScript(const nsAString& aURL); bool IsProcessScoped() const override { return true; } diff --git a/dom/base/DirectionalityUtils.cpp b/dom/base/DirectionalityUtils.cpp index dd427c61b1..2e4ada4800 100644 --- a/dom/base/DirectionalityUtils.cpp +++ b/dom/base/DirectionalityUtils.cpp @@ -191,10 +191,8 @@ static bool ParticipatesInAutoDirection(const nsIContent* aContent) { if (aContent->IsShadowRoot()) { return true; } - dom::NodeInfo* ni = aContent->NodeInfo(); - return ni->NamespaceID() == kNameSpaceID_XHTML && - !ni->Equals(nsGkAtoms::script) && !ni->Equals(nsGkAtoms::style) && - !ni->Equals(nsGkAtoms::input) && !ni->Equals(nsGkAtoms::textarea); + return !aContent->IsAnyOfHTMLElements(nsGkAtoms::script, nsGkAtoms::style, + nsGkAtoms::input, nsGkAtoms::textarea); } /** diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp index 4e9286a91e..8cbf8b8075 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp @@ -172,6 +172,8 @@ #include "mozilla/dom/FeaturePolicy.h" #include "mozilla/dom/FeaturePolicyUtils.h" #include "mozilla/dom/FontFaceSet.h" +#include "mozilla/dom/FragmentDirective.h" +#include "mozilla/dom/fragmentdirectives_ffi_generated.h" #include "mozilla/dom/FromParser.h" #include "mozilla/dom/HighlightRegistry.h" #include "mozilla/dom/HTMLAllCollection.h" @@ -198,6 +200,7 @@ #include "mozilla/dom/NetErrorInfoBinding.h" #include "mozilla/dom/NodeInfo.h" #include "mozilla/dom/NodeIterator.h" +#include "mozilla/dom/nsHTTPSOnlyUtils.h" #include "mozilla/dom/PContentChild.h" #include "mozilla/dom/PWindowGlobalChild.h" #include "mozilla/dom/PageTransitionEvent.h" @@ -1437,7 +1440,6 @@ Document::Document(const char* aContentType) mThrowOnDynamicMarkupInsertionCounter(0), mIgnoreOpensDuringUnloadCounter(0), mSavedResolution(1.0f), - mSavedResolutionBeforeMVM(1.0f), mGeneration(0), mCachedTabSizeGeneration(0), mNextFormNumber(0), @@ -2484,6 +2486,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(Document) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFontFaceSet) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReadyForIdle) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentL10n) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFragmentDirective) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHighlightRegistry) // Traverse all Document nsCOMPtrs. @@ -2631,6 +2634,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Document) NS_IMPL_CYCLE_COLLECTION_UNLINK(mFontFaceSet) NS_IMPL_CYCLE_COLLECTION_UNLINK(mReadyForIdle) NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentL10n) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFragmentDirective) NS_IMPL_CYCLE_COLLECTION_UNLINK(mHighlightRegistry) NS_IMPL_CYCLE_COLLECTION_UNLINK(mParser) NS_IMPL_CYCLE_COLLECTION_UNLINK(mOnloadBlocker) @@ -4065,6 +4069,21 @@ void Document::StopDocumentLoad() { void Document::SetDocumentURI(nsIURI* aURI) { nsCOMPtr<nsIURI> oldBase = GetDocBaseURI(); mDocumentURI = aURI; + // This loosely implements §3.4.1 of Text Fragments + // https://wicg.github.io/scroll-to-text-fragment/#invoking-text-directives + // Unlike specified in the spec, the fragment directive is not stripped from + // the URL in the session history entry. Instead it is removed when the URL is + // set in the `Document`. Also, instead of storing the `uninvokedDirective` in + // `Document` as mentioned in the spec, the extracted directives are moved to + // the `FragmentDirective` object which deals with finding the ranges to + // highlight in `ScrollToRef()`. + // XXX(:jjaschke): This is only a temporary solution. + // https://bugzil.la/1881429 is filed for revisiting this. + nsTArray<TextDirective> textDirectives; + FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragment( + mDocumentURI, &textDirectives); + FragmentDirective()->SetTextDirectives(std::move(textDirectives)); + nsIURI* newBase = GetDocBaseURI(); mChromeRulesEnabled = URLExtraData::ChromeRulesEnabled(aURI); @@ -4103,10 +4122,11 @@ void Document::SetDocumentURI(nsIURI* aURI) { } } -static void GetFormattedTimeString(PRTime aTime, +static void GetFormattedTimeString(PRTime aTime, bool aUniversal, nsAString& aFormattedTimeString) { PRExplodedTime prtime; - PR_ExplodeTime(aTime, PR_LocalTimeParameters, &prtime); + PR_ExplodeTime(aTime, aUniversal ? PR_GMTParameters : PR_LocalTimeParameters, + &prtime); // "MM/DD/YYYY hh:mm:ss" char formatedTime[24]; if (SprintfLiteral(formatedTime, "%02d/%02d/%04d %02d:%02d:%02d", @@ -4124,7 +4144,9 @@ void Document::GetLastModified(nsAString& aLastModified) const { if (!mLastModified.IsEmpty()) { aLastModified.Assign(mLastModified); } else { - GetFormattedTimeString(PR_Now(), aLastModified); + GetFormattedTimeString(PR_Now(), + ShouldResistFingerprinting(RFPTarget::JSDateTimeUTC), + aLastModified); } } @@ -6401,7 +6423,7 @@ void Document::SetLastFocusTime(const TimeStamp& aFocusTime) { mLastFocusTime = aFocusTime; } -void Document::GetReferrer(nsAString& aReferrer) const { +void Document::GetReferrer(nsACString& aReferrer) const { aReferrer.Truncate(); if (!mReferrerInfo) { return; @@ -6412,13 +6434,7 @@ void Document::GetReferrer(nsAString& aReferrer) const { return; } - nsAutoCString uri; - nsresult rv = URLDecorationStripper::StripTrackingIdentifiers(referrer, uri); - if (NS_WARN_IF(NS_FAILED(rv))) { - return; - } - - CopyUTF8toUTF16(uri, aReferrer); + URLDecorationStripper::StripTrackingIdentifiers(referrer, aReferrer); } void Document::GetCookie(nsAString& aCookie, ErrorResult& aRv) { @@ -7683,6 +7699,10 @@ static void NotifyActivityChangedCallback(nsISupports* aSupports) { void Document::NotifyActivityChanged() { EnumerateActivityObservers(NotifyActivityChangedCallback); + // https://w3c.github.io/screen-wake-lock/#handling-document-loss-of-full-activity + if (!IsActive()) { + UnlockAllWakeLocks(WakeLockType::Screen); + } } void Document::SetContainer(nsDocShell* aContainer) { @@ -11111,7 +11131,9 @@ void Document::RetrieveRelevantHeaders(nsIChannel* aChannel) { mLastModified.Truncate(); if (modDate != 0) { - GetFormattedTimeString(modDate, mLastModified); + GetFormattedTimeString(modDate, + ShouldResistFingerprinting(RFPTarget::JSDateTimeUTC), + mLastModified); } } @@ -12537,14 +12559,6 @@ void Document::UpdateDocumentStates(DocumentState aMaybeChangedStates, } } - if (aMaybeChangedStates.HasAtLeastOneOfStates(DocumentState::LWTHEME)) { - if (ComputeDocumentLWTheme()) { - mState |= DocumentState::LWTHEME; - } else { - mState &= ~DocumentState::LWTHEME; - } - } - if (aMaybeChangedStates.HasState(DocumentState::WINDOW_INACTIVE)) { BrowsingContext* bc = GetBrowsingContext(); if (!bc || !bc->GetIsActiveBrowserWindow()) { @@ -13089,25 +13103,29 @@ void Document::SetScrollToRef(nsIURI* aDocumentURI) { // https://html.spec.whatwg.org/#scrolling-to-a-fragment void Document::ScrollToRef() { - if (mScrolledToRefAlready) { - RefPtr<PresShell> presShell = GetPresShell(); - if (presShell) { - presShell->ScrollToAnchor(); - } + RefPtr<PresShell> presShell = GetPresShell(); + if (!presShell) { return; } - - // 2. If fragment is the empty string, then return the special value top of - // the document. - if (mScrollToRef.IsEmpty()) { + if (mScrolledToRefAlready) { + presShell->ScrollToAnchor(); return; } - RefPtr<PresShell> presShell = GetPresShell(); - if (!presShell) { + // If text directives is non-null, then highlight the text directives and + // scroll to the last one. + // XXX(:jjaschke): Document policy integration should happen here + // as soon as https://bugzil.la/1860915 lands. + // XXX(:jjaschke): Same goes for User Activation and security aspects, + // tracked in https://bugzil.la/1888756. + const bool didScrollToTextFragment = + presShell->HighlightAndGoToTextFragment(true); + + // 2. If fragment is the empty string and no text directives have been + // scrolled to, then return the special value top of the document. + if (didScrollToTextFragment || mScrollToRef.IsEmpty()) { return; } - // 3. Let potentialIndicatedElement be the result of finding a potential // indicated element given document and fragment. NS_ConvertUTF8toUTF16 ref(mScrollToRef); @@ -15089,11 +15107,6 @@ void Document::HideAllPopoversUntil(nsINode& aEndpoint, } while (repeatingHide); } -MOZ_CAN_RUN_SCRIPT_BOUNDARY void -Document::HideAllPopoversWithoutRunningScript() { - return HideAllPopoversUntil(*this, false, false); -} - void Document::HidePopover(Element& aPopover, bool aFocusPreviousElement, bool aFireEvents, ErrorResult& aRv) { RefPtr<nsGenericHTMLElement> popoverHTMLEl = @@ -15548,13 +15561,21 @@ bool Document::HasPendingFullscreenRequests() { return !iter.AtEnd(); } +MOZ_CAN_RUN_SCRIPT_BOUNDARY bool Document::ApplyFullscreen(UniquePtr<FullscreenRequest> aRequest) { if (!FullscreenElementReadyCheck(*aRequest)) { return false; } + Element* elem = aRequest->Element(); + + RefPtr<nsINode> hideUntil = elem->GetTopmostPopoverAncestor(nullptr, false); + if (!hideUntil) { + hideUntil = OwnerDoc(); + } + RefPtr<Document> doc = aRequest->Document(); - doc->HideAllPopoversWithoutRunningScript(); + doc->HideAllPopoversUntil(*hideUntil, false, true); // Stash a reference to any existing fullscreen doc, we'll use this later // to detect if the origin which is fullscreen has changed. @@ -15580,7 +15601,6 @@ bool Document::ApplyFullscreen(UniquePtr<FullscreenRequest> aRequest) { // Set the fullscreen element. This sets the fullscreen style on the // element, and the fullscreen-ancestor styles on ancestors of the element // in this document. - Element* elem = aRequest->Element(); SetFullscreenElement(*elem); // Set the iframe fullscreen flag. if (auto* iframe = HTMLIFrameElement::FromNode(elem)) { @@ -15698,6 +15718,11 @@ void Document::UpdateVisibilityState(DispatchVisibilityChange aDispatchEvent) { for (auto* listener : mWorkerListeners) { listener->OnVisible(visible); } + + // https://w3c.github.io/screen-wake-lock/#handling-document-loss-of-visibility + if (!visible) { + UnlockAllWakeLocks(WakeLockType::Screen); + } } } @@ -16558,16 +16583,6 @@ void Document::SetStateObject(nsIStructuredCloneContainer* scContainer) { mCachedStateObjectValid = false; } -bool Document::ComputeDocumentLWTheme() const { - if (!NodePrincipal()->IsSystemPrincipal()) { - return false; - } - - Element* element = GetRootElement(); - return element && element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::lwtheme, - nsGkAtoms::_true, eCaseMatters); -} - already_AddRefed<Element> Document::CreateHTMLElement(nsAtom* aTag) { RefPtr<mozilla::dom::NodeInfo> nodeInfo; nodeInfo = mNodeInfoManager->GetNodeInfo(aTag, nullptr, kNameSpaceID_XHTML, @@ -17525,6 +17540,18 @@ Document::CreatePermissionGrantPromise( p = new StorageAccessAPIHelper::StorageAccessPermissionGrantPromise:: Private(__func__); + // Before we prompt, see if we are same-site + if (aFrameOnly) { + nsIChannel* channel = self->GetChannel(); + if (channel) { + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + if (!loadInfo->GetIsThirdPartyContextToTopWindow()) { + p->Resolve(StorageAccessAPIHelper::eAllow, __func__); + return p; + } + } + } + RefPtr<PWindowGlobalChild::GetStorageAccessPermissionPromise> promise; // Test the permission MOZ_ASSERT(XRE_IsContentProcess()); @@ -18357,9 +18384,13 @@ class UnlockAllWakeLockRunnable final : public Runnable { void Document::UnlockAllWakeLocks(WakeLockType aType) { // Perform unlock in a runnable to prevent UnlockAll being MOZ_CAN_RUN_SCRIPT - RefPtr<UnlockAllWakeLockRunnable> runnable = - MakeRefPtr<UnlockAllWakeLockRunnable>(aType, this); - NS_DispatchToMainThread(runnable); + if (!ActiveWakeLocks(aType).IsEmpty()) { + RefPtr<UnlockAllWakeLockRunnable> runnable = + MakeRefPtr<UnlockAllWakeLockRunnable>(aType, this); + nsresult rv = NS_DispatchToMainThread(runnable); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + Unused << rv; + } } RefPtr<Document::AutomaticStorageAccessPermissionGrantPromise> @@ -18637,12 +18668,24 @@ nsICookieJarSettings* Document::CookieJarSettings() { net::CookieJarSettings::Cast(mCookieJarSettings) ->SetFingerprintingRandomizationKey(randomKey); } + + // Inerit the top level windowContext id from the parent. + net::CookieJarSettings::Cast(mCookieJarSettings) + ->SetTopLevelWindowContextId( + net::CookieJarSettings::Cast(inProcessParent->CookieJarSettings()) + ->GetTopLevelWindowContextId()); } else { mCookieJarSettings = net::CookieJarSettings::Create(NodePrincipal()); + + if (IsTopLevelContentDocument()) { + net::CookieJarSettings::Cast(mCookieJarSettings) + ->SetTopLevelWindowContextId(InnerWindowID()); + } } if (auto* wgc = GetWindowGlobalChild()) { net::CookieJarSettingsArgs csArgs; + net::CookieJarSettings::Cast(mCookieJarSettings)->Serialize(csArgs); // Update cookie settings in the parent process if (!wgc->SendUpdateCookieJarSettings(csArgs)) { @@ -19042,6 +19085,13 @@ HighlightRegistry& Document::HighlightRegistry() { return *mHighlightRegistry; } +FragmentDirective* Document::FragmentDirective() { + if (!mFragmentDirective) { + mFragmentDirective = MakeRefPtr<class FragmentDirective>(this); + } + return mFragmentDirective; +} + RadioGroupContainer& Document::OwnedRadioGroupContainer() { if (!mRadioGroupContainer) { mRadioGroupContainer = MakeUnique<RadioGroupContainer>(); diff --git a/dom/base/Document.h b/dom/base/Document.h index a52c61addf..0b0d0ca3d0 100644 --- a/dom/base/Document.h +++ b/dom/base/Document.h @@ -244,6 +244,7 @@ class EventListener; struct FailedCertSecurityInfo; class FeaturePolicy; class FontFaceSet; +class FragmentDirective; class FrameRequestCallback; class ImageTracker; class HighlightRegistry; @@ -2992,15 +2993,6 @@ class Document : public nsINode, SetStateObject(aDocument->mStateObjectContainer); } - /** - * Returns true if there is a lightweight theme specified. This is used to - * determine the state of the :-moz-lwtheme pseudo-class. - */ - bool ComputeDocumentLWTheme() const; - void ResetDocumentLWTheme() { - UpdateDocumentStates(DocumentState::LWTHEME, true); - } - // Whether we're a media document or not. enum class MediaDocumentKind { NotMedia, @@ -3281,7 +3273,7 @@ class Document : public nsINode, void SetDomain(const nsAString& aDomain, mozilla::ErrorResult& rv); void GetCookie(nsAString& aCookie, mozilla::ErrorResult& rv); void SetCookie(const nsAString& aCookie, mozilla::ErrorResult& rv); - void GetReferrer(nsAString& aReferrer) const; + void GetReferrer(nsACString& aReferrer) const; void GetLastModified(nsAString& aLastModified) const; void GetReadyState(nsAString& aReadyState) const; @@ -3377,7 +3369,6 @@ class Document : public nsINode, bool aFocusPreviousElement, bool aFireEvents); - MOZ_CAN_RUN_SCRIPT_BOUNDARY void HideAllPopoversWithoutRunningScript(); // Hides the given popover element, see // https://html.spec.whatwg.org/multipage/popover.html#hide-popover-algorithm MOZ_CAN_RUN_SCRIPT void HidePopover(Element& popover, @@ -4100,6 +4091,13 @@ class Document : public nsINode, */ class HighlightRegistry& HighlightRegistry(); + /** + * @brief Returns the `FragmentDirective` object which contains information + * and functionality to extract or create text directives. + * Guaranteed to be non-null. + */ + class FragmentDirective* FragmentDirective(); + bool ShouldResistFingerprinting(RFPTarget aTarget) const; bool IsInPrivateBrowsing() const; @@ -4159,7 +4157,8 @@ class Document : public nsINode, // Apply the fullscreen state to the document, and trigger related // events. It returns false if the fullscreen element ready check // fails and nothing gets changed. - bool ApplyFullscreen(UniquePtr<FullscreenRequest>); + MOZ_CAN_RUN_SCRIPT_BOUNDARY bool ApplyFullscreen( + UniquePtr<FullscreenRequest>); void RemoveDocStyleSheetsFromStyleSets(); void ResetStylesheetsToURI(nsIURI* aURI); @@ -5314,9 +5313,6 @@ class Document : public nsINode, // Pres shell resolution saved before entering fullscreen mode. float mSavedResolution; - // Pres shell resolution saved before creating a MobileViewportManager. - float mSavedResolutionBeforeMVM; - nsCOMPtr<nsICookieJarSettings> mCookieJarSettings; bool mHasStoragePermission; @@ -5381,6 +5377,7 @@ class Document : public nsINode, nsTArray<CanvasUsage> mCanvasUsage; uint64_t mLastCanvasUsage = 0; + RefPtr<class FragmentDirective> mFragmentDirective; UniquePtr<RadioGroupContainer> mRadioGroupContainer; public: @@ -5392,11 +5389,6 @@ class Document : public nsINode, nsRefPtrHashtable<nsRefPtrHashKey<Element>, nsXULPrototypeElement> mL10nProtoElements; - float GetSavedResolutionBeforeMVM() { return mSavedResolutionBeforeMVM; } - void SetSavedResolutionBeforeMVM(float aResolution) { - mSavedResolutionBeforeMVM = aResolution; - } - void LoadEventFired(); RadioGroupContainer& OwnedRadioGroupContainer(); diff --git a/dom/base/Element.cpp b/dom/base/Element.cpp index be31000278..b6f5d5c3be 100644 --- a/dom/base/Element.cpp +++ b/dom/base/Element.cpp @@ -1055,6 +1055,14 @@ already_AddRefed<nsIScreen> Element::GetScreen() { return nullptr; } +double Element::CurrentCSSZoom() { + nsIFrame* f = GetPrimaryFrame(FlushType::Frames); + if (!f) { + return 1.0; + } + return f->Style()->EffectiveZoom().ToFloat(); +} + already_AddRefed<DOMRect> Element::GetBoundingClientRect() { RefPtr<DOMRect> rect = new DOMRect(ToSupports(OwnerDoc())); @@ -1233,15 +1241,14 @@ bool Element::CanAttachShadowDOM() const { return true; } -// https://dom.spec.whatwg.org/commit-snapshots/1eadf0a4a271acc92013d1c0de8c730ac96204f9/#dom-element-attachshadow -already_AddRefed<ShadowRoot> Element::AttachShadow( - const ShadowRootInit& aInit, ErrorResult& aError, - ShadowRootDeclarative aNewShadowIsDeclarative) { +// https://dom.spec.whatwg.org/#dom-element-attachshadow +already_AddRefed<ShadowRoot> Element::AttachShadow(const ShadowRootInit& aInit, + ErrorResult& aError) { /** * Step 1, 2, and 3. */ if (!CanAttachShadowDOM()) { - aError.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + aError.ThrowNotSupportedError("Unable to attach ShadowDOM"); return nullptr; } @@ -1249,21 +1256,27 @@ already_AddRefed<ShadowRoot> Element::AttachShadow( * 4. If element is a shadow host, then: */ if (RefPtr<ShadowRoot> root = GetShadowRoot()) { - /* - * 1. If element’s shadow root’s declarative is false, then throw an - * "NotSupportedError" DOMException. + /** + * 1. Let currentShadowRoot be element’s shadow root. + * + * 2. If any of the following are true: + * currentShadowRoot’s declarative is false; or + * currentShadowRoot’s mode is not mode, + * then throw a "NotSupportedError" DOMException. */ - if (!root->IsDeclarative()) { - aError.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + if (!root->IsDeclarative() || root->Mode() != aInit.mMode) { + aError.ThrowNotSupportedError( + "Unable to re-attach to existing ShadowDOM"); return nullptr; } - // https://github.com/whatwg/dom/issues/1235 - root->SetIsDeclarative(aNewShadowIsDeclarative); - /* - * 2. Otherwise, remove all of element’s shadow root’s children, in tree - * order, and return. + /** + * 3. Otherwise: + * 1. Remove all of currentShadowRoot’s children, in tree order. + * 2. Set currentShadowRoot’s declarative to false. + * 3. Return. */ root->ReplaceChildren(nullptr, aError); + root->SetIsDeclarative(ShadowRootDeclarative::No); return root.forget(); } @@ -1273,14 +1286,12 @@ already_AddRefed<ShadowRoot> Element::AttachShadow( return AttachShadowWithoutNameChecks( aInit.mMode, DelegatesFocus(aInit.mDelegatesFocus), aInit.mSlotAssignment, - ShadowRootClonable(aInit.mClonable), - ShadowRootDeclarative(aNewShadowIsDeclarative)); + ShadowRootClonable(aInit.mClonable)); } already_AddRefed<ShadowRoot> Element::AttachShadowWithoutNameChecks( ShadowRootMode aMode, DelegatesFocus aDelegatesFocus, - SlotAssignmentMode aSlotAssignment, ShadowRootClonable aClonable, - ShadowRootDeclarative aDeclarative) { + SlotAssignmentMode aSlotAssignment, ShadowRootClonable aClonable) { nsAutoScriptBlocker scriptBlocker; auto* nim = mNodeInfo->NodeInfoManager(); @@ -1304,9 +1315,9 @@ already_AddRefed<ShadowRoot> Element::AttachShadowWithoutNameChecks( * context object's node document, host is context object, * and mode is init's mode. */ - RefPtr<ShadowRoot> shadowRoot = - new (nim) ShadowRoot(this, aMode, aDelegatesFocus, aSlotAssignment, - aClonable, aDeclarative, nodeInfo.forget()); + RefPtr<ShadowRoot> shadowRoot = new (nim) + ShadowRoot(this, aMode, aDelegatesFocus, aSlotAssignment, aClonable, + ShadowRootDeclarative::No, nodeInfo.forget()); if (NodeOrAncestorHasDirAuto()) { shadowRoot->SetAncestorHasDirAuto(); @@ -1336,6 +1347,22 @@ already_AddRefed<ShadowRoot> Element::AttachShadowWithoutNameChecks( dispatcher->PostDOMEvent(); } + const LinkedList<AbstractRange>* ranges = + GetExistingClosestCommonInclusiveAncestorRanges(); + if (ranges) { + for (const AbstractRange* range : *ranges) { + if (range->MayCrossShadowBoundary()) { + MOZ_ASSERT(range->IsDynamicRange()); + StaticRange* crossBoundaryRange = + range->AsDynamicRange()->GetCrossShadowBoundaryRange(); + MOZ_ASSERT(crossBoundaryRange); + // We may have previously selected this node before it + // becomes a shadow host, so we need to reset the values + // in RangeBoundaries to accommodate the change. + crossBoundaryRange->NotifyNodeBecomesShadowHost(this); + } + } + } /** * 10. Return shadow. */ @@ -1460,13 +1487,7 @@ void Element::GetAttribute(const nsAString& aName, DOMString& aReturn) { if (val) { val->ToString(aReturn); } else { - if (IsXULElement()) { - // XXX should be SetDOMStringToNull(aReturn); - // See bug 232598 - // aReturn is already empty - } else { - aReturn.SetNull(); - } + aReturn.SetNull(); } } @@ -1930,9 +1951,7 @@ nsresult Element::BindToTree(BindContext& aContext, nsINode& aParent) { // This has to be here, rather than in nsGenericHTMLElement::BindToTree, // because it has to happen after updating the parent pointer, but before // recursively binding the kids. - if (IsHTMLElement()) { - SetDirOnBind(this, nsIContent::FromNode(aParent)); - } + SetDirOnBind(this, nsIContent::FromNode(aParent)); UpdateEditableState(false); @@ -2145,9 +2164,7 @@ void Element::UnbindFromTree(UnbindContext& aContext) { // This has to be here, rather than in nsGenericHTMLElement::UnbindFromTree, // because it has to happen after unsetting the parent pointer, but before // recursively unbinding the kids. - if (IsHTMLElement()) { - ResetDir(this); - } + ResetDir(this); for (nsIContent* child = GetFirstChild(); child; child = child->GetNextSibling()) { diff --git a/dom/base/Element.h b/dom/base/Element.h index 40a8052aef..fb88274a88 100644 --- a/dom/base/Element.h +++ b/dom/base/Element.h @@ -257,7 +257,8 @@ class Element : public FragmentOrElement { #ifdef MOZILLA_INTERNAL_API explicit Element(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) : FragmentOrElement(std::move(aNodeInfo)), - mState(ElementState::READONLY | ElementState::DEFINED) { + mState(ElementState::READONLY | ElementState::DEFINED | + ElementState::LTR) { MOZ_ASSERT(mNodeInfo->NodeType() == ELEMENT_NODE, "Bad NodeType in aNodeInfo"); SetIsElement(); @@ -1352,10 +1353,8 @@ class Element : public FragmentOrElement { enum class ShadowRootDeclarative : bool { No, Yes }; MOZ_CAN_RUN_SCRIPT_BOUNDARY - already_AddRefed<ShadowRoot> AttachShadow( - const ShadowRootInit& aInit, ErrorResult& aError, - ShadowRootDeclarative aNewShadowIsDeclarative = - ShadowRootDeclarative::No); + already_AddRefed<ShadowRoot> AttachShadow(const ShadowRootInit& aInit, + ErrorResult& aError); bool CanAttachShadowDOM() const; enum class DelegatesFocus : bool { No, Yes }; @@ -1364,8 +1363,7 @@ class Element : public FragmentOrElement { already_AddRefed<ShadowRoot> AttachShadowWithoutNameChecks( ShadowRootMode aMode, DelegatesFocus = DelegatesFocus::No, SlotAssignmentMode aSlotAssignmentMode = SlotAssignmentMode::Named, - ShadowRootClonable aClonable = ShadowRootClonable::No, - ShadowRootDeclarative aDeclarative = ShadowRootDeclarative::No); + ShadowRootClonable aClonable = ShadowRootClonable::No); // Attach UA Shadow Root if it is not attached. enum class NotifyUAWidgetSetup : bool { No, Yes }; @@ -1499,6 +1497,8 @@ class Element : public FragmentOrElement { return CSSPixel::FromAppUnits(GetClientAreaRect().Width()); } + MOZ_CAN_RUN_SCRIPT double CurrentCSSZoom(); + // This function will return the block size of first line box, no matter if // the box is 'block' or 'inline'. The return unit is pixel. If the element // can't get a primary frame, we will return be zero. diff --git a/dom/base/EventSource.cpp b/dom/base/EventSource.cpp index f70db487dd..def3c90ec0 100644 --- a/dom/base/EventSource.cpp +++ b/dom/base/EventSource.cpp @@ -568,7 +568,15 @@ nsresult EventSourceImpl::ParseURL(const nsAString& aURL) { NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr<nsIURI> srcURI; - rv = NS_NewURI(getter_AddRefs(srcURI), aURL, nullptr, baseURI); + nsCOMPtr<Document> doc = + mIsMainThread ? GetEventSource()->GetDocumentIfCurrent() : nullptr; + if (doc) { + rv = NS_NewURI(getter_AddRefs(srcURI), aURL, doc->GetDocumentCharacterSet(), + baseURI); + } else { + rv = NS_NewURI(getter_AddRefs(srcURI), aURL, nullptr, baseURI); + } + NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SYNTAX_ERR); nsAutoString origin; diff --git a/dom/base/FragmentDirective.cpp b/dom/base/FragmentDirective.cpp new file mode 100644 index 0000000000..3300a85751 --- /dev/null +++ b/dom/base/FragmentDirective.cpp @@ -0,0 +1,879 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FragmentDirective.h" +#include <cstdint> +#include "RangeBoundary.h" +#include "mozilla/Assertions.h" +#include "Document.h" +#include "mozilla/dom/FragmentDirectiveBinding.h" +#include "mozilla/dom/FragmentOrElement.h" +#include "mozilla/dom/NodeBinding.h" +#include "mozilla/dom/Text.h" +#include "mozilla/intl/WordBreaker.h" +#include "nsComputedDOMStyle.h" +#include "nsContentUtils.h" +#include "nsDOMAttributeMap.h" +#include "nsGkAtoms.h" +#include "nsICSSDeclaration.h" +#include "nsIFrame.h" +#include "nsINode.h" +#include "nsIURIMutator.h" +#include "nsRange.h" +#include "nsString.h" + +namespace mozilla::dom { +static LazyLogModule sFragmentDirectiveLog("FragmentDirective"); + +/** Converts a `TextDirective` into a percent-encoded string. */ +nsCString ToString(const TextDirective& aTextDirective) { + nsCString str; + create_text_directive(&aTextDirective, &str); + return str; +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(FragmentDirective, mDocument) +NS_IMPL_CYCLE_COLLECTING_ADDREF(FragmentDirective) +NS_IMPL_CYCLE_COLLECTING_RELEASE(FragmentDirective) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FragmentDirective) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +FragmentDirective::FragmentDirective(Document* aDocument) + : mDocument(aDocument) {} + +JSObject* FragmentDirective::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return FragmentDirective_Binding::Wrap(aCx, this, aGivenProto); +} + +void FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragment( + nsCOMPtr<nsIURI>& aURI, nsTArray<TextDirective>* aTextDirectives) { + if (!aURI || !StaticPrefs::dom_text_fragments_enabled()) { + return; + } + bool hasRef = false; + aURI->GetHasRef(&hasRef); + if (!hasRef) { + return; + } + + nsAutoCString hash; + aURI->GetRef(hash); + + ParsedFragmentDirectiveResult fragmentDirective; + const bool hasRemovedFragmentDirective = + parse_fragment_directive(&hash, &fragmentDirective); + if (!hasRemovedFragmentDirective) { + return; + } + Unused << NS_MutateURI(aURI) + .SetRef(fragmentDirective.url_without_fragment_directive) + .Finalize(aURI); + if (aTextDirectives) { + aTextDirectives->SwapElements(fragmentDirective.text_directives); + } +} + +nsTArray<RefPtr<nsRange>> FragmentDirective::FindTextFragmentsInDocument() { + MOZ_ASSERT(mDocument); + mDocument->FlushPendingNotifications(FlushType::Frames); + nsTArray<RefPtr<nsRange>> textDirectiveRanges; + for (const TextDirective& textDirective : mUninvokedTextDirectives) { + if (RefPtr<nsRange> range = FindRangeForTextDirective(textDirective)) { + textDirectiveRanges.AppendElement(range); + } + } + mUninvokedTextDirectives.Clear(); + return textDirectiveRanges; +} + +/** + * @brief Determine if `aNode` should be considered when traversing the DOM. + * + * A node is "search invisible" if it is an element in the HTML namespace and + * 1. The computed value of its `display` property is `none` + * 2. It serializes as void + * 3. It is one of the following types: + * - HTMLIFrameElement + * - HTMLImageElement + * - HTMLMeterElement + * - HTMLObjectElement + * - HTMLProgressElement + * - HTMLStyleElement + * - HTMLScriptElement + * - HTMLVideoElement + * - HTMLAudioElement + * 4. It is a `select` element whose `multiple` content attribute is absent + * + * see https://wicg.github.io/scroll-to-text-fragment/#search-invisible + */ +bool NodeIsSearchInvisible(nsINode& aNode) { + if (!aNode.IsElement()) { + return false; + } + // 2. If the node serializes as void. + nsAtom* nodeNameAtom = aNode.NodeInfo()->NameAtom(); + if (FragmentOrElement::IsHTMLVoid(nodeNameAtom)) { + return true; + } + // 3. Is any of the following types: HTMLIFrameElement, HTMLImageElement, + // HTMLMeterElement, HTMLObjectElement, HTMLProgressElement, HTMLStyleElement, + // HTMLScriptElement, HTMLVideoElement, HTMLAudioElement + if (aNode.IsAnyOfHTMLElements( + nsGkAtoms::iframe, nsGkAtoms::image, nsGkAtoms::meter, + nsGkAtoms::object, nsGkAtoms::progress, nsGkAtoms::style, + nsGkAtoms::script, nsGkAtoms::video, nsGkAtoms::audio)) { + return true; + } + // 4. Is a select element whose multiple content attribute is absent. + if (aNode.IsHTMLElement(nsGkAtoms::select)) { + return aNode.GetAttributes()->GetNamedItem(u"multiple"_ns) == nullptr; + } + // This is tested last because it's the most expensive check. + // 1. The computed value of its 'display' property is 'none'. + const Element* nodeAsElement = Element::FromNode(aNode); + const RefPtr<const ComputedStyle> computedStyle = + nsComputedDOMStyle::GetComputedStyleNoFlush(nodeAsElement); + return !computedStyle || + computedStyle->StyleDisplay()->mDisplay == StyleDisplay::None; +} + +/** + * @brief Returns true if `aNode` has block-level display. + * A node has block-level display if it is an element and the computed value + * of its display property is any of + * - block + * - table + * - flow-root + * - grid + * - flex + * - list-item + * + * See https://wicg.github.io/scroll-to-text-fragment/#has-block-level-display + */ +bool NodeHasBlockLevelDisplay(nsINode& aNode) { + if (!aNode.IsElement()) { + return false; + } + const Element* nodeAsElement = Element::FromNode(aNode); + const RefPtr<const ComputedStyle> computedStyle = + nsComputedDOMStyle::GetComputedStyleNoFlush(nodeAsElement); + if (!computedStyle) { + return false; + } + const StyleDisplay& styleDisplay = computedStyle->StyleDisplay()->mDisplay; + return styleDisplay == StyleDisplay::Block || + styleDisplay == StyleDisplay::Table || + styleDisplay == StyleDisplay::FlowRoot || + styleDisplay == StyleDisplay::Grid || + styleDisplay == StyleDisplay::Flex || styleDisplay.IsListItem(); +} + +/** + * @brief Get the Block Ancestor For `aNode`. + * + * see https://wicg.github.io/scroll-to-text-fragment/#nearest-block-ancestor + */ +nsINode* GetBlockAncestorForNode(nsINode* aNode) { + // 1. Let curNode be node. + RefPtr<nsINode> curNode = aNode; + // 2. While curNode is non-null + while (curNode) { + // 2.1. If curNode is not a Text node and it has block-level display then + // return curNode. + if (!curNode->IsText() && NodeHasBlockLevelDisplay(*curNode)) { + return curNode; + } + // 2.2. Otherwise, set curNode to curNode’s parent. + curNode = curNode->GetParentNode(); + } + // 3.Return node’s node document's document element. + return aNode->GetOwnerDocument(); +} + +/** + * @brief Returns true if `aNode` is part of a non-searchable subtree. + * + * A node is part of a non-searchable subtree if it is or has a shadow-including + * ancestor that is search invisible. + * + * see https://wicg.github.io/scroll-to-text-fragment/#non-searchable-subtree + */ +bool NodeIsPartOfNonSearchableSubTree(nsINode& aNode) { + nsINode* node = &aNode; + do { + if (NodeIsSearchInvisible(*node)) { + return true; + } + } while ((node = node->GetParentOrShadowHostNode())); + return false; +} + +/** + * @brief Return true if `aNode` is a visible Text node. + * + * A node is a visible text node if it is a Text node, the computed value of + * its parent element's visibility property is visible, and it is being + * rendered. + * + * see https://wicg.github.io/scroll-to-text-fragment/#visible-text-node + */ +bool NodeIsVisibleTextNode(const nsINode& aNode) { + const Text* text = Text::FromNode(aNode); + if (!text) { + return false; + } + const nsIFrame* frame = text->GetPrimaryFrame(); + return frame && frame->StyleVisibility()->IsVisible(); +} + +enum class TextScanDirection { Left = -1, Right = 1 }; + +/** + * @brief Tests if there is whitespace at the given position and direction. + * + * This algorithm tests for whitespaces and ` ` at `aPos`. + * It returns the size of the whitespace found at the position, i.e. 5/6 for + * ` /;` and 1 otherwise. + * + * This function follows a subsection of this section of the spec, but has been + * adapted to be able to scan in both directions: + * https://wicg.github.io/scroll-to-text-fragment/#next-non-whitespace-position + */ +uint32_t IsWhitespaceAtPosition(nsString& aText, uint32_t aPos, + TextScanDirection aDirection) { + if (aText.Length() == 0) { + return 0; + } + if (aDirection == TextScanDirection::Right) { + if (aText.Length() > (aPos + 5)) { + if (Substring(aText, aPos, 5).Equals(u" ")) { + return aText.Length() > (aPos + 6) && aText.CharAt(aPos + 6) == u';' + ? 6 + : 5; + } + } + } else { + if (aPos > 6 && Substring(aText, aPos - 6, 6).Equals(u" ")) { + return 6; + } + if (aPos > 5 && Substring(aText, aPos - 5, 5).Equals(u" ")) { + return 5; + } + } + return uint32_t(IsSpaceCharacter(aText.CharAt(aPos))); +} + +/** Advances the start of `aRange` to the next non-whitespace position. + * The function follows this section of the spec: + * https://wicg.github.io/scroll-to-text-fragment/#next-non-whitespace-position + */ +void AdvanceStartToNextNonWhitespacePosition(nsRange& aRange) { + // 1. While range is not collapsed: + while (!aRange.Collapsed()) { + // 1.1. Let node be range's start node. + RefPtr<nsINode> node = aRange.GetStartContainer(); + MOZ_ASSERT(node); + // 1.2. Let offset be range's start offset. + const uint32_t offset = aRange.StartOffset(); + // 1.3. If node is part of a non-searchable subtree or if node is not a + // visible text node or if offset is equal to node's length then: + if (NodeIsPartOfNonSearchableSubTree(*node) || + !NodeIsVisibleTextNode(*node) || offset == node->Length()) { + // 1.3.1. Set range's start node to the next node, in shadow-including + // tree order. + // 1.3.2. Set range's start offset to 0. + if (NS_FAILED(aRange.SetStart(node->GetNextNode(), 0))) { + return; + } + // 1.3.3. Continue. + continue; + } + const Text* text = Text::FromNode(node); + nsAutoString textData; + text->GetData(textData); + // These steps are moved to `IsWhitespaceAtPosition()`. + // 1.4. If the substring data of node at offset offset and count 6 is equal + // to the string " " then: + // 1.4.1. Add 6 to range’s start offset. + // 1.5. Otherwise, if the substring data of node at offset offset and count + // 5 is equal to the string " " then: + // 1.5.1. Add 5 to range’s start offset. + // 1.6. Otherwise: + // 1.6.1 Let cp be the code point at the offset index in node’s data. + // 1.6.2 If cp does not have the White_Space property set, return. + // 1.6.3 Add 1 to range’s start offset. + const uint32_t whitespace = + IsWhitespaceAtPosition(textData, offset, TextScanDirection::Right); + if (whitespace == 0) { + return; + } + + aRange.SetStart(node, offset + whitespace); + } +} + +/** + * @brief Moves `aRangeBoundary` one word in `aDirection`. + * + * Word boundaries are determined using `intl::WordBreaker::FindWord()`. + * + * + * @param aRangeBoundary[in] The range boundary that should be moved. + * Must be set and valid. + * @param aDirection[in] The direction into which to move. + * @return A new `RangeBoundary` which is moved to the next word. + */ +RangeBoundary MoveRangeBoundaryOneWord(const RangeBoundary& aRangeBoundary, + TextScanDirection aDirection) { + MOZ_ASSERT(aRangeBoundary.IsSetAndValid()); + RefPtr<nsINode> curNode = aRangeBoundary.Container(); + uint32_t offset = *aRangeBoundary.Offset( + RangeBoundary::OffsetFilter::kValidOrInvalidOffsets); + + const int offsetIncrement = int(aDirection); + // Get the text node of the start of the range and the offset. + // This is the current position of the start of the range. + nsAutoString text; + if (NodeIsVisibleTextNode(*curNode)) { + const Text* textNode = Text::FromNode(curNode); + textNode->GetData(text); + + // Assuming that the current position might not be at a word boundary, + // advance to the word boundary at word begin/end. + if (!IsWhitespaceAtPosition(text, offset, aDirection)) { + const intl::WordRange wordRange = + intl::WordBreaker::FindWord(text, offset); + if (aDirection == TextScanDirection::Right && + offset != wordRange.mBegin) { + offset = wordRange.mEnd; + } else if (aDirection == TextScanDirection::Left && + offset != wordRange.mEnd) { + // The additional -1 is necessary to move to offset to *before* the + // start of the word. + offset = wordRange.mBegin - 1; + } + } + } + // Now, skip any whitespace, so that `offset` points to the word boundary of + // the next word (which is the one this algorithm actually aims to move over). + while (curNode) { + if (!NodeIsVisibleTextNode(*curNode) || NodeIsSearchInvisible(*curNode) || + offset >= curNode->Length()) { + curNode = aDirection == TextScanDirection::Left ? curNode->GetPrevNode() + : curNode->GetNextNode(); + if (!curNode) { + break; + } + offset = + aDirection == TextScanDirection::Left ? curNode->Length() - 1 : 0; + if (const Text* textNode = Text::FromNode(curNode)) { + textNode->GetData(text); + } + continue; + } + if (const uint32_t whitespace = + IsWhitespaceAtPosition(text, offset, aDirection)) { + offset += offsetIncrement * whitespace; + continue; + } + + // At this point, the caret has been moved to the next non-whitespace + // position. + // find word boundaries at the current position + const intl::WordRange wordRange = intl::WordBreaker::FindWord(text, offset); + offset = aDirection == TextScanDirection::Left ? wordRange.mBegin + : wordRange.mEnd; + + return {curNode, offset}; + } + return {}; +} + +RefPtr<nsRange> FragmentDirective::FindRangeForTextDirective( + const TextDirective& aTextDirective) { + MOZ_LOG(sFragmentDirectiveLog, LogLevel::Info, + ("FragmentDirective::%s(): Find range for text directive '%s'.", + __FUNCTION__, ToString(aTextDirective).Data())); + // 1. Let searchRange be a range with start (document, 0) and end (document, + // document’s length) + ErrorResult rv; + RefPtr<nsRange> searchRange = + nsRange::Create(mDocument, 0, mDocument, mDocument->Length(), rv); + if (rv.Failed()) { + return nullptr; + } + // 2. While searchRange is not collapsed: + while (!searchRange->Collapsed()) { + // 2.1. Let potentialMatch be null. + RefPtr<nsRange> potentialMatch; + // 2.2. If parsedValues’s prefix is not null: + if (!aTextDirective.prefix.IsEmpty()) { + // 2.2.1. Let prefixMatch be the the result of running the find a string + // in range steps with query parsedValues’s prefix, searchRange + // searchRange, wordStartBounded true and wordEndBounded false. + RefPtr<nsRange> prefixMatch = + FindStringInRange(searchRange, aTextDirective.prefix, true, false); + // 2.2.2. If prefixMatch is null, return null. + if (!prefixMatch) { + return nullptr; + } + // 2.2.3. Set searchRange’s start to the first boundary point after + // prefixMatch’s start + const RangeBoundary boundaryPoint = MoveRangeBoundaryOneWord( + {prefixMatch->GetStartContainer(), prefixMatch->StartOffset()}, + TextScanDirection::Right); + if (!boundaryPoint.IsSetAndValid()) { + return nullptr; + } + searchRange->SetStart(boundaryPoint.AsRaw(), rv); + if (rv.Failed()) { + return nullptr; + } + + // 2.2.4. Let matchRange be a range whose start is prefixMatch’s end and + // end is searchRange’s end. + RefPtr<nsRange> matchRange = nsRange::Create( + prefixMatch->GetEndContainer(), prefixMatch->EndOffset(), + searchRange->GetEndContainer(), searchRange->EndOffset(), rv); + if (rv.Failed()) { + return nullptr; + } + // 2.2.5. Advance matchRange’s start to the next non-whitespace position. + AdvanceStartToNextNonWhitespacePosition(*matchRange); + // 2.2.6. If matchRange is collapsed return null. + // (This can happen if prefixMatch’s end or its subsequent non-whitespace + // position is at the end of the document.) + if (matchRange->Collapsed()) { + return nullptr; + } + // 2.2.7. Assert: matchRange’s start node is a Text node. + // (matchRange’s start now points to the next non-whitespace text data + // following a matched prefix.) + MOZ_ASSERT(matchRange->GetStartContainer()->IsText()); + + // 2.2.8. Let mustEndAtWordBoundary be true if parsedValues’s end is + // non-null or parsedValues’s suffix is null, false otherwise. + const bool mustEndAtWordBoundary = + !aTextDirective.end.IsEmpty() || aTextDirective.suffix.IsEmpty(); + // 2.2.9. Set potentialMatch to the result of running the find a string in + // range steps with query parsedValues’s start, searchRange matchRange, + // wordStartBounded false, and wordEndBounded mustEndAtWordBoundary. + potentialMatch = FindStringInRange(matchRange, aTextDirective.start, + false, mustEndAtWordBoundary); + // 2.2.10. If potentialMatch is null, return null. + if (!potentialMatch) { + return nullptr; + } + // 2.2.11. If potentialMatch’s start is not matchRange’s start, then + // continue. + // (In this case, we found a prefix but it was followed by something other + // than a matching text so we’ll continue searching for the next instance + // of prefix.) + if (potentialMatch->GetStartContainer() != + matchRange->GetStartContainer()) { + continue; + } + } + // 2.3. Otherwise: + else { + // 2.3.1. Let mustEndAtWordBoundary be true if parsedValues’s end is + // non-null or parsedValues’s suffix is null, false otherwise. + const bool mustEndAtWordBoundary = + !aTextDirective.end.IsEmpty() || aTextDirective.suffix.IsEmpty(); + // 2.3.2. Set potentialMatch to the result of running the find a string in + // range steps with query parsedValues’s start, searchRange searchRange, + // wordStartBounded true, and wordEndBounded mustEndAtWordBoundary. + potentialMatch = FindStringInRange(searchRange, aTextDirective.start, + true, mustEndAtWordBoundary); + // 2.3.3. If potentialMatch is null, return null. + if (!potentialMatch) { + return nullptr; + } + // 2.3.4. Set searchRange’s start to the first boundary point after + // potentialMatch’s start + RangeBoundary newRangeBoundary = MoveRangeBoundaryOneWord( + {potentialMatch->GetStartContainer(), potentialMatch->StartOffset()}, + TextScanDirection::Right); + if (!newRangeBoundary.IsSetAndValid()) { + return nullptr; + } + searchRange->SetStart(newRangeBoundary.AsRaw(), rv); + if (rv.Failed()) { + return nullptr; + } + } + // 2.4. Let rangeEndSearchRange be a range whose start is potentialMatch’s + // end and whose end is searchRange’s end. + RefPtr<nsRange> rangeEndSearchRange = nsRange::Create( + potentialMatch->GetEndContainer(), potentialMatch->EndOffset(), + searchRange->GetEndContainer(), searchRange->EndOffset(), rv); + if (rv.Failed()) { + return nullptr; + } + // 2.5. While rangeEndSearchRange is not collapsed: + while (!rangeEndSearchRange->Collapsed()) { + // 2.5.1. If parsedValues’s end item is non-null, then: + if (!aTextDirective.end.IsEmpty()) { + // 2.5.1.1. Let mustEndAtWordBoundary be true if parsedValues’s suffix + // is null, false otherwise. + const bool mustEndAtWordBoundary = aTextDirective.suffix.IsEmpty(); + // 2.5.1.2. Let endMatch be the result of running the find a string in + // range steps with query parsedValues’s end, searchRange + // rangeEndSearchRange, wordStartBounded true, and wordEndBounded + // mustEndAtWordBoundary. + RefPtr<nsRange> endMatch = + FindStringInRange(rangeEndSearchRange, aTextDirective.end, true, + mustEndAtWordBoundary); + // 2.5.1.3. If endMatch is null then return null. + if (!endMatch) { + return nullptr; + } + // 2.5.1.4. Set potentialMatch’s end to endMatch’s end. + potentialMatch->SetEnd(endMatch->GetEndContainer(), + endMatch->EndOffset()); + } + // 2.5.2. Assert: potentialMatch is non-null, not collapsed and represents + // a range exactly containing an instance of matching text. + MOZ_ASSERT(potentialMatch && !potentialMatch->Collapsed()); + + // 2.5.3. If parsedValues’s suffix is null, return potentialMatch. + if (aTextDirective.suffix.IsEmpty()) { + return potentialMatch; + } + // 2.5.4. Let suffixRange be a range with start equal to potentialMatch’s + // end and end equal to searchRange’s end. + RefPtr<nsRange> suffixRange = nsRange::Create( + potentialMatch->GetEndContainer(), potentialMatch->EndOffset(), + searchRange->GetEndContainer(), searchRange->EndOffset(), rv); + if (rv.Failed()) { + return nullptr; + } + // 2.5.5. Advance suffixRange's start to the next non-whitespace position. + AdvanceStartToNextNonWhitespacePosition(*suffixRange); + + // 2.5.6. Let suffixMatch be result of running the find a string in range + // steps with query parsedValue's suffix, searchRange suffixRange, + // wordStartBounded false, and wordEndBounded true. + RefPtr<nsRange> suffixMatch = + FindStringInRange(suffixRange, aTextDirective.suffix, false, true); + + // 2.5.7. If suffixMatch is null, return null. + // (If the suffix doesn't appear in the remaining text of the document, + // there's no possible way to make a match.) + if (!suffixMatch) { + return nullptr; + } + // 2.5.8. If suffixMatch's start is suffixRange's start, return + // potentialMatch. + if (suffixMatch->GetStartContainer() == + suffixRange->GetStartContainer() && + suffixMatch->StartOffset() == suffixRange->StartOffset()) { + return potentialMatch; + } + // 2.5.9. If parsedValue's end item is null then break; + // (If this is an exact match and the suffix doesn’t match, start + // searching for the next range start by breaking out of this loop without + // rangeEndSearchRange being collapsed. If we’re looking for a range + // match, we’ll continue iterating this inner loop since the range start + // will already be correct.) + if (aTextDirective.end.IsEmpty()) { + break; + } + // 2.5.10. Set rangeEndSearchRange's start to potentialMatch's end. + // (Otherwise, it is possible that we found the correct range start, but + // not the correct range end. Continue the inner loop to keep searching + // for another matching instance of rangeEnd.) + rangeEndSearchRange->SetStart(potentialMatch->GetEndContainer(), + potentialMatch->EndOffset()); + } + // 2.6. If rangeEndSearchRange is collapsed then: + if (rangeEndSearchRange->Collapsed()) { + // 2.6.1. Assert parsedValue's end item is non-null. + // (This can only happen for range matches due to the break for exact + // matches in step 9 of the above loop. If we couldn’t find a valid + // rangeEnd+suffix pair anywhere in the doc then there’s no possible way + // to make a match.) + // XXX(:jjaschke): should this really assert? + MOZ_ASSERT(!aTextDirective.end.IsEmpty()); + } + } + // 3. Return null. + return nullptr; +} + +/** + * @brief Convenience function that returns true if the given position in a + * string is a word boundary. + * + * This is a thin wrapper around the `WordBreaker::FindWord()` function. + * + * @param aText The text input. + * @param aPosition The position to check. + * @return true if there is a word boundary at `aPosition`. + * @return false otherwise. + */ +bool IsAtWordBoundary(const nsAString& aText, uint32_t aPosition) { + const intl::WordRange wordRange = + intl::WordBreaker::FindWord(aText, aPosition); + return wordRange.mBegin == aPosition || wordRange.mEnd == aPosition; +} + +enum class IsEndIndex : bool { No, Yes }; +RangeBoundary GetBoundaryPointAtIndex( + uint32_t aIndex, const nsTArray<RefPtr<Text>>& aTextNodeList, + IsEndIndex aIsEndIndex) { + // 1. Let counted be 0. + uint32_t counted = 0; + // 2. For each curNode of nodes: + for (Text* curNode : aTextNodeList) { + // 2.1. Let nodeEnd be counted + curNode’s length. + uint32_t nodeEnd = counted + curNode->Length(); + // 2.2. If isEnd is true, add 1 to nodeEnd. + if (aIsEndIndex == IsEndIndex::Yes) { + ++nodeEnd; + } + // 2.3. If nodeEnd is greater than index then: + if (nodeEnd > aIndex) { + // 2.3.1. Return the boundary point (curNode, index − counted). + return RangeBoundary(curNode->AsNode(), aIndex - counted); + } + // 2.4. Increment counted by curNode’s length. + counted += curNode->Length(); + } + return {}; +} + +RefPtr<nsRange> FindRangeFromNodeList( + nsRange* aSearchRange, const nsAString& aQuery, + const nsTArray<RefPtr<Text>>& aTextNodeList, bool aWordStartBounded, + bool aWordEndBounded) { + // 1. Let searchBuffer be the concatenation of the data of each item in nodes. + // XXX(:jjaschke): There's an open issue here that deals with what + // data is supposed to be (text data vs. rendered text) + // https://github.com/WICG/scroll-to-text-fragment/issues/98 + uint32_t bufferLength = 0; + for (const Text* text : aTextNodeList) { + bufferLength += text->Length(); + } + // bail out if the search query is longer than the text data. + if (bufferLength < aQuery.Length()) { + return nullptr; + } + nsAutoString searchBuffer; + searchBuffer.SetCapacity(bufferLength); + for (Text* text : aTextNodeList) { + text->AppendTextTo(searchBuffer); + } + // 2. Let searchStart be 0. + // 3. If the first item in nodes is searchRange’s start node then set + // searchStart to searchRange’s start offset. + uint32_t searchStart = + aTextNodeList.SafeElementAt(0) == aSearchRange->GetStartContainer() + ? aSearchRange->StartOffset() + : 0; + + // 4. Let start and end be boundary points, initially null. + RangeBoundary start, end; + // 5. Let matchIndex be null. + // "null" here doesn't mean 0, instead "not set". 0 would be a valid index. + // Therefore, "null" is represented by the value -1. + int32_t matchIndex = -1; + + // 6. While matchIndex is null + // As explained above, "null" == -1 in this algorithm. + while (matchIndex == -1) { + // 6.1. Set matchIndex to the index of the first instance of queryString in + // searchBuffer, starting at searchStart. The string search must be + // performed using a base character comparison, or the primary level, as + // defined in [UTS10]. + // [UTS10] + // Ken Whistler; Markus Scherer.Unicode Collation Algorithm.26 August 2022. + // Unicode Technical Standard #10. + // URL : https://www.unicode.org/reports/tr10/tr10-47.html + + // XXX(:jjaschke): For the initial implementation, a standard case-sensitive + // find-in-string is used. + // See: https://github.com/WICG/scroll-to-text-fragment/issues/233 + matchIndex = searchBuffer.Find(aQuery, searchStart); + // 6.2. If matchIndex is null, return null. + if (matchIndex == -1) { + return nullptr; + } + + // 6.3. Let endIx be matchIndex + queryString’s length. + // endIx is the index of the last character in the match + 1. + const uint32_t endIx = matchIndex + aQuery.Length(); + + // 6.4. Set start to the boundary point result of get boundary point at + // index matchIndex run over nodes with isEnd false. + start = GetBoundaryPointAtIndex(matchIndex, aTextNodeList, IsEndIndex::No); + // 6.5. Set end to the boundary point result of get boundary point at index + // endIx run over nodes with isEnd true. + end = GetBoundaryPointAtIndex(endIx, aTextNodeList, IsEndIndex::Yes); + + // 6.6. If wordStartBounded is true and matchIndex is not at a word boundary + // in searchBuffer, given the language from start’s node as the locale; or + // wordEndBounded is true and matchIndex + queryString’s length is not at a + // word boundary in searchBuffer, given the language from end’s node as the + // locale: + if ((aWordStartBounded && !IsAtWordBoundary(searchBuffer, matchIndex)) || + (aWordEndBounded && !IsAtWordBoundary(searchBuffer, endIx))) { + // 6.6.1. Set searchStart to matchIndex + 1. + searchStart = matchIndex + 1; + // 6.6.2. Set matchIndex to null. + matchIndex = -1; + } + } + // 7. Let endInset be 0. + // 8. If the last item in nodes is searchRange’s end node then set endInset + // to (searchRange’s end node's length − searchRange’s end offset) + // (endInset is the offset from the last position in the last node in the + // reverse direction. Alternatively, it is the length of the node that’s not + // included in the range.) + uint32_t endInset = + aTextNodeList.LastElement() == aSearchRange->GetEndContainer() + ? aSearchRange->GetEndContainer()->Length() - + aSearchRange->EndOffset() + : 0; + + // 9. If matchIndex + queryString’s length is greater than searchBuffer’s + // length − endInset return null. + // (If the match runs past the end of the search range, return null.) + if (matchIndex + aQuery.Length() > searchBuffer.Length() - endInset) { + return nullptr; + } + + // 10. Assert: start and end are non-null, valid boundary points in + // searchRange. + MOZ_ASSERT(start.IsSetAndValid()); + MOZ_ASSERT(end.IsSetAndValid()); + + // 11. Return a range with start start and end end. + ErrorResult rv; + RefPtr<nsRange> range = nsRange::Create(start, end, rv); + if (rv.Failed()) { + return nullptr; + } + + return range; +} + +RefPtr<nsRange> FragmentDirective::FindStringInRange(nsRange* aSearchRange, + const nsAString& aQuery, + bool aWordStartBounded, + bool aWordEndBounded) { + MOZ_ASSERT(aSearchRange); + RefPtr<nsRange> searchRange = aSearchRange->CloneRange(); + // 1. While searchRange is not collapsed + while (searchRange && !searchRange->Collapsed()) { + // 1.1. Let curNode be searchRange’s start node. + RefPtr<nsINode> curNode = searchRange->GetStartContainer(); + + // 1.2. If curNode is part of a non-searchable subtree: + if (NodeIsPartOfNonSearchableSubTree(*curNode)) { + // 1.2.1. Set searchRange’s start node to the next node, in + // shadow-including tree order, that isn’t a shadow-including descendant + // of curNode. + RefPtr<nsINode> next = curNode; + while ((next = next->GetNextNode())) { + if (!next->IsShadowIncludingInclusiveDescendantOf(curNode)) { + break; + } + } + if (!next) { + return nullptr; + } + // 1.2.2. Set `searchRange`s `start offset` to 0 + searchRange->SetStart(next, 0); + // 1.2.3. continue. + continue; + } + // 1.3. If curNode is not a visible TextNode: + if (!NodeIsVisibleTextNode(*curNode)) { + // 1.3.1. Set searchRange’s start node to the next node, in + // shadow-including tree order, that is not a doctype. + RefPtr<nsINode> next = curNode; + while ((next = next->GetNextNode())) { + if (next->NodeType() != Node_Binding::DOCUMENT_TYPE_NODE) { + break; + } + } + if (!next) { + return nullptr; + } + // 1.3.2. Set searchRange’s start offset to 0. + searchRange->SetStart(next, 0); + // 1.3.3. continue. + continue; + } + // 1.4. Let blockAncestor be the nearest block ancestor of `curNode` + RefPtr<nsINode> blockAncestor = GetBlockAncestorForNode(curNode); + + // 1.5. Let textNodeList be a list of Text nodes, initially empty. + nsTArray<RefPtr<Text>> textNodeList; + // 1.6. While curNode is a shadow-including descendant of blockAncestor and + // the position of the boundary point (curNode,0) is not after searchRange's + // end: + while (curNode && + curNode->IsShadowIncludingInclusiveDescendantOf(blockAncestor)) { + Maybe<int32_t> comp = nsContentUtils::ComparePoints( + curNode, 0, searchRange->GetEndContainer(), searchRange->EndOffset()); + if (comp) { + if (*comp >= 0) { + break; + } + } else { + // This means that the compared nodes are disconnected. + return nullptr; + } + // 1.6.1. If curNode has block-level display, then break. + if (NodeHasBlockLevelDisplay(*curNode)) { + break; + } + // 1.6.2. If curNode is search invisible: + if (NodeIsSearchInvisible(*curNode)) { + // 1.6.2.1. Set curNode to the next node, in shadow-including tree + // order, that isn't a shadow-including descendant of curNode. + curNode = curNode->GetNextNode(); + // 1.6.2.2. Continue. + continue; + } + // 1.6.3. If curNode is a visible text node then append it to + // textNodeList. + if (NodeIsVisibleTextNode(*curNode)) { + textNodeList.AppendElement(curNode->AsText()); + } + // 1.6.4. Set curNode to the next node in shadow-including + // tree order. + curNode = curNode->GetNextNode(); + } + // 1.7. Run the find a range from a node list steps given + // query, searchRange, textNodeList, wordStartBounded, wordEndBounded as + // input. If the resulting Range is not null, then return it. + if (RefPtr<nsRange> range = + FindRangeFromNodeList(searchRange, aQuery, textNodeList, + aWordStartBounded, aWordEndBounded)) { + return range; + } + + // 1.8. If curNode is null, then break. + if (!curNode) { + break; + } + + // 1.9. Assert: curNode follows searchRange's start node. + + // 1.10. Set searchRange's start to the boundary point (curNode,0). + searchRange->SetStart(curNode, 0); + } + + // 2. Return null. + return nullptr; +} +} // namespace mozilla::dom diff --git a/dom/base/FragmentDirective.h b/dom/base/FragmentDirective.h new file mode 100644 index 0000000000..8972556d6c --- /dev/null +++ b/dom/base/FragmentDirective.h @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_FRAGMENTDIRECTIVE_H_ +#define DOM_FRAGMENTDIRECTIVE_H_ + +#include "js/TypeDecls.h" +#include "mozilla/dom/BindingDeclarations.h" + +#include "mozilla/dom/fragmentdirectives_ffi_generated.h" +#include "nsCycleCollectionParticipant.h" +#include "nsStringFwd.h" +#include "nsWrapperCache.h" + +class nsINode; +class nsIURI; +class nsRange; +namespace mozilla::dom { +class Document; +class Text; + +/** + * @brief The `FragmentDirective` class is the C++ representation of the + * `Document.fragmentDirective` webidl property. + * + * This class also serves as the main interface to interact with the fragment + * directive from the C++ side. It allows to find text fragment ranges from a + * given list of `TextDirective`s using + * `FragmentDirective::FindTextFragmentsInDocument()`. + * To avoid Text Directives being applied multiple times, this class implements + * the `uninvoked directive` mechanism, which in the spec is defined to be part + * of the `Document` [0]. + * + * [0] + * https://wicg.github.io/scroll-to-text-fragment/#document-uninvoked-directives + */ +class FragmentDirective final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(FragmentDirective) + + public: + explicit FragmentDirective(Document* aDocument); + FragmentDirective(Document* aDocument, + nsTArray<TextDirective>&& aTextDirectives) + : mDocument(aDocument), + mUninvokedTextDirectives(std::move(aTextDirectives)) {} + + protected: + ~FragmentDirective() = default; + + public: + Document* GetParentObject() const { return mDocument; }; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + /** + * @brief Sets Text Directives as "uninvoked directive". + */ + void SetTextDirectives(nsTArray<TextDirective>&& aTextDirectives) { + mUninvokedTextDirectives = std::move(aTextDirectives); + } + + /** Returns true if there are Text Directives that have not been applied to + * the `Document`. + */ + bool HasUninvokedDirectives() const { + return !mUninvokedTextDirectives.IsEmpty(); + }; + + /** Searches for the current uninvoked text directives and creates a range for + * each one that is found. + * + * When this method returns, the uninvoked directives for this document are + * cleared. + * + * This method tries to follow the specification as close as possible in how + * to find a matching range for a text directive. However, instead of using + * collator-based search, a standard case-insensitive search is used + * (`nsString::find()`). + */ + nsTArray<RefPtr<nsRange>> FindTextFragmentsInDocument(); + + /** Utility function which parses the fragment directive and removes it from + * the hash of the given URI. This operation happens in-place. + * + * If aTextDirectives is nullptr, the parsed fragment directive is discarded. + */ + static void ParseAndRemoveFragmentDirectiveFromFragment( + nsCOMPtr<nsIURI>& aURI, + nsTArray<TextDirective>* aTextDirectives = nullptr); + + private: + RefPtr<nsRange> FindRangeForTextDirective( + const TextDirective& aTextDirective); + RefPtr<nsRange> FindStringInRange(nsRange* aSearchRange, + const nsAString& aQuery, + bool aWordStartBounded, + bool aWordEndBounded); + + RefPtr<Document> mDocument; + nsTArray<TextDirective> mUninvokedTextDirectives; +}; + +} // namespace mozilla::dom + +#endif // DOM_FRAGMENTDIRECTIVE_H_ diff --git a/dom/base/Link.cpp b/dom/base/Link.cpp index 296a584ed1..1465ea5773 100644 --- a/dom/base/Link.cpp +++ b/dom/base/Link.cpp @@ -6,26 +6,21 @@ #include "Link.h" -#include "mozilla/MemoryReporting.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/BindContext.h" +#include "mozilla/dom/Document.h" #include "mozilla/dom/SVGAElement.h" #include "mozilla/dom/HTMLDNSPrefetch.h" #include "mozilla/IHistory.h" -#include "mozilla/StaticPrefs_layout.h" #include "nsLayoutUtils.h" -#include "nsIURL.h" #include "nsIURIMutator.h" #include "nsISizeOf.h" -#include "nsEscape.h" #include "nsGkAtoms.h" #include "nsString.h" -#include "mozAutoDocUpdate.h" #include "mozilla/Components.h" #include "nsAttrValueInlines.h" -#include "HTMLLinkElement.h" namespace mozilla::dom { @@ -133,7 +128,7 @@ nsIURI* Link::GetURI() const { return mCachedURI; } -void Link::SetProtocol(const nsAString& aProtocol) { +void Link::SetProtocol(const nsACString& aProtocol) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. @@ -146,111 +141,100 @@ void Link::SetProtocol(const nsAString& aProtocol) { SetHrefAttribute(uri); } -void Link::SetPassword(const nsAString& aPassword) { +void Link::SetPassword(const nsACString& aPassword) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv = NS_MutateURI(uri) - .SetPassword(NS_ConvertUTF16toUTF8(aPassword)) - .Finalize(uri); + nsresult rv = NS_MutateURI(uri).SetPassword(aPassword).Finalize(uri); if (NS_SUCCEEDED(rv)) { SetHrefAttribute(uri); } } -void Link::SetUsername(const nsAString& aUsername) { +void Link::SetUsername(const nsACString& aUsername) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv = NS_MutateURI(uri) - .SetUsername(NS_ConvertUTF16toUTF8(aUsername)) - .Finalize(uri); + nsresult rv = NS_MutateURI(uri).SetUsername(aUsername).Finalize(uri); if (NS_SUCCEEDED(rv)) { SetHrefAttribute(uri); } } -void Link::SetHost(const nsAString& aHost) { +void Link::SetHost(const nsACString& aHost) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv = - NS_MutateURI(uri).SetHostPort(NS_ConvertUTF16toUTF8(aHost)).Finalize(uri); + nsresult rv = NS_MutateURI(uri).SetHostPort(aHost).Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } -void Link::SetHostname(const nsAString& aHostname) { +void Link::SetHostname(const nsACString& aHostname) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv = - NS_MutateURI(uri).SetHost(NS_ConvertUTF16toUTF8(aHostname)).Finalize(uri); + nsresult rv = NS_MutateURI(uri).SetHost(aHostname).Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } -void Link::SetPathname(const nsAString& aPathname) { +void Link::SetPathname(const nsACString& aPathname) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv = NS_MutateURI(uri) - .SetFilePath(NS_ConvertUTF16toUTF8(aPathname)) - .Finalize(uri); + nsresult rv = NS_MutateURI(uri).SetFilePath(aPathname).Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } -void Link::SetSearch(const nsAString& aSearch) { +void Link::SetSearch(const nsACString& aSearch) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv = - NS_MutateURI(uri).SetQuery(NS_ConvertUTF16toUTF8(aSearch)).Finalize(uri); + nsresult rv = NS_MutateURI(uri).SetQuery(aSearch).Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } -void Link::SetPort(const nsAString& aPort) { +void Link::SetPort(const nsACString& aPort) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv; - nsAutoString portStr(aPort); - // nsIURI uses -1 as default value. + nsresult rv; int32_t port = -1; if (!aPort.IsEmpty()) { - port = portStr.ToInteger(&rv); + port = aPort.ToInteger(&rv); if (NS_FAILED(rv)) { return; } @@ -263,15 +247,14 @@ void Link::SetPort(const nsAString& aPort) { SetHrefAttribute(uri); } -void Link::SetHash(const nsAString& aHash) { +void Link::SetHash(const nsACString& aHash) { nsCOMPtr<nsIURI> uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } - nsresult rv = - NS_MutateURI(uri).SetRef(NS_ConvertUTF16toUTF8(aHash)).Finalize(uri); + nsresult rv = NS_MutateURI(uri).SetRef(aHash).Finalize(uri); if (NS_FAILED(rv)) { return; } @@ -279,121 +262,102 @@ void Link::SetHash(const nsAString& aHash) { SetHrefAttribute(uri); } -void Link::GetOrigin(nsAString& aOrigin) { +void Link::GetOrigin(nsACString& aOrigin) { aOrigin.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { return; } - nsString origin; - nsContentUtils::GetWebExposedOriginSerialization(uri, origin); - aOrigin.Assign(origin); + nsContentUtils::GetWebExposedOriginSerialization(uri, aOrigin); } -void Link::GetProtocol(nsAString& _protocol) { - nsCOMPtr<nsIURI> uri(GetURI()); - if (uri) { - nsAutoCString scheme; - (void)uri->GetScheme(scheme); - CopyASCIItoUTF16(scheme, _protocol); +void Link::GetProtocol(nsACString& aProtocol) { + if (nsIURI* uri = GetURI()) { + (void)uri->GetScheme(aProtocol); } - _protocol.Append(char16_t(':')); + aProtocol.Append(':'); } -void Link::GetUsername(nsAString& aUsername) { +void Link::GetUsername(nsACString& aUsername) { aUsername.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { return; } - nsAutoCString username; - uri->GetUsername(username); - CopyASCIItoUTF16(username, aUsername); + uri->GetUsername(aUsername); } -void Link::GetPassword(nsAString& aPassword) { +void Link::GetPassword(nsACString& aPassword) { aPassword.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { return; } - nsAutoCString password; - uri->GetPassword(password); - CopyASCIItoUTF16(password, aPassword); + uri->GetPassword(aPassword); } -void Link::GetHost(nsAString& _host) { - _host.Truncate(); +void Link::GetHost(nsACString& aHost) { + aHost.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { // Do not throw! Not having a valid URI should result in an empty string. return; } - nsAutoCString hostport; - nsresult rv = uri->GetHostPort(hostport); - if (NS_SUCCEEDED(rv)) { - CopyUTF8toUTF16(hostport, _host); - } + uri->GetHostPort(aHost); } -void Link::GetHostname(nsAString& _hostname) { - _hostname.Truncate(); +void Link::GetHostname(nsACString& aHostname) { + aHostname.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { // Do not throw! Not having a valid URI should result in an empty string. return; } - nsContentUtils::GetHostOrIPv6WithBrackets(uri, _hostname); + nsContentUtils::GetHostOrIPv6WithBrackets(uri, aHostname); } -void Link::GetPathname(nsAString& _pathname) { - _pathname.Truncate(); +void Link::GetPathname(nsACString& aPathname) { + aPathname.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { // Do not throw! Not having a valid URI should result in an empty string. return; } - nsAutoCString file; - nsresult rv = uri->GetFilePath(file); - if (NS_SUCCEEDED(rv)) { - CopyUTF8toUTF16(file, _pathname); - } + uri->GetFilePath(aPathname); } -void Link::GetSearch(nsAString& _search) { - _search.Truncate(); +void Link::GetSearch(nsACString& aSearch) { + aSearch.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { // Do not throw! Not having a valid URI or URL should result in an empty // string. return; } - nsAutoCString search; - nsresult rv = uri->GetQuery(search); - if (NS_SUCCEEDED(rv) && !search.IsEmpty()) { - _search.Assign(u'?'); - AppendUTF8toUTF16(search, _search); + nsresult rv = uri->GetQuery(aSearch); + if (NS_SUCCEEDED(rv) && !aSearch.IsEmpty()) { + aSearch.Insert('?', 0); } } -void Link::GetPort(nsAString& _port) { - _port.Truncate(); +void Link::GetPort(nsACString& aPort) { + aPort.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { // Do not throw! Not having a valid URI should result in an empty string. return; @@ -404,27 +368,23 @@ void Link::GetPort(nsAString& _port) { // Note that failure to get the port from the URI is not necessarily a bad // thing. Some URIs do not have a port. if (NS_SUCCEEDED(rv) && port != -1) { - nsAutoString portStr; - portStr.AppendInt(port, 10); - _port.Assign(portStr); + aPort.AppendInt(port, 10); } } -void Link::GetHash(nsAString& _hash) { - _hash.Truncate(); +void Link::GetHash(nsACString& aHash) { + aHash.Truncate(); - nsCOMPtr<nsIURI> uri(GetURI()); + nsIURI* uri = GetURI(); if (!uri) { // Do not throw! Not having a valid URI should result in an empty // string. return; } - nsAutoCString ref; - nsresult rv = uri->GetRef(ref); - if (NS_SUCCEEDED(rv) && !ref.IsEmpty()) { - _hash.Assign(char16_t('#')); - AppendUTF8toUTF16(ref, _hash); + nsresult rv = uri->GetRef(aHash); + if (NS_SUCCEEDED(rv) && !aHash.IsEmpty()) { + aHash.Insert('#', 0); } } diff --git a/dom/base/Link.h b/dom/base/Link.h index b604457126..cee1e1c5ca 100644 --- a/dom/base/Link.h +++ b/dom/base/Link.h @@ -64,25 +64,25 @@ class Link : public nsISupports { /** * Helper methods for modifying and obtaining parts of the URI of the Link. */ - void SetProtocol(const nsAString& aProtocol); - void SetUsername(const nsAString& aUsername); - void SetPassword(const nsAString& aPassword); - void SetHost(const nsAString& aHost); - void SetHostname(const nsAString& aHostname); - void SetPathname(const nsAString& aPathname); - void SetSearch(const nsAString& aSearch); - void SetPort(const nsAString& aPort); - void SetHash(const nsAString& aHash); - void GetOrigin(nsAString& aOrigin); - void GetProtocol(nsAString& _protocol); - void GetUsername(nsAString& aUsername); - void GetPassword(nsAString& aPassword); - void GetHost(nsAString& _host); - void GetHostname(nsAString& _hostname); - void GetPathname(nsAString& _pathname); - void GetSearch(nsAString& _search); - void GetPort(nsAString& _port); - void GetHash(nsAString& _hash); + void SetProtocol(const nsACString& aProtocol); + void SetUsername(const nsACString& aUsername); + void SetPassword(const nsACString& aPassword); + void SetHost(const nsACString& aHost); + void SetHostname(const nsACString& aHostname); + void SetPathname(const nsACString& aPathname); + void SetSearch(const nsACString& aSearch); + void SetPort(const nsACString& aPort); + void SetHash(const nsACString& aHash); + void GetOrigin(nsACString& aOrigin); + void GetProtocol(nsACString& aProtocol); + void GetUsername(nsACString& aUsername); + void GetPassword(nsACString& aPassword); + void GetHost(nsACString& aHost); + void GetHostname(nsACString& aHostname); + void GetPathname(nsACString& aPathname); + void GetSearch(nsACString& aSearch); + void GetPort(nsACString& aPort); + void GetHash(nsACString& aHash); /** * Invalidates any link caching, and resets the state to the default. diff --git a/dom/base/Location.cpp b/dom/base/Location.cpp index cdf25abf56..9edb9e0b6f 100644 --- a/dom/base/Location.cpp +++ b/dom/base/Location.cpp @@ -34,6 +34,7 @@ #include "mozilla/Unused.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/FragmentDirective.h" #include "mozilla/dom/LocationBinding.h" #include "mozilla/dom/ScriptSettings.h" #include "ReferrerInfo.h" @@ -105,6 +106,9 @@ nsresult Location::GetURI(nsIURI** aURI, bool aGetInnermostURI) { } NS_ASSERTION(uri, "nsJARURI screwed up?"); + + // Remove the fragment directive from the url hash. + FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragment(uri); nsCOMPtr<nsIURI> exposableURI = net::nsIOService::CreateExposableURI(uri); exposableURI.forget(aURI); return NS_OK; @@ -549,26 +553,6 @@ void Location::Reload(bool aForceget, nsIPrincipal& aSubjectPrincipal, return aRv.Throw(NS_ERROR_FAILURE); } - if (StaticPrefs::dom_block_reload_from_resize_event_handler()) { - nsCOMPtr<nsPIDOMWindowOuter> window = docShell->GetWindow(); - if (window && window->IsHandlingResizeEvent()) { - // location.reload() was called on a window that is handling a - // resize event. Sites do this since Netscape 4.x needed it, but - // we don't, and it's a horrible experience for nothing. In stead - // of reloading the page, just clear style data and reflow the - // page since some sites may use this trick to work around gecko - // reflow bugs, and this should have the same effect. - RefPtr<Document> doc = window->GetExtantDoc(); - - nsPresContext* pcx; - if (doc && (pcx = doc->GetPresContext())) { - pcx->RebuildAllStyleData(NS_STYLE_HINT_REFLOW, - RestyleHint::RestyleSubtree()); - } - return; - } - } - RefPtr<BrowsingContext> bc = GetBrowsingContext(); if (!bc || bc->IsDiscarded()) { return; diff --git a/dom/base/PlacesEventCounts.cpp b/dom/base/PlacesEventCounts.cpp new file mode 100644 index 0000000000..be02e4c39b --- /dev/null +++ b/dom/base/PlacesEventCounts.cpp @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "PlacesEventCounts.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/PlacesEventCounts.h" +#include "mozilla/dom/PlacesEventBinding.h" +#include "mozilla/dom/PlacesObserversBinding.h" + +namespace mozilla::dom { + +// Only needed for refcounted objects. +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(PlacesEventCounts) +NS_IMPL_CYCLE_COLLECTING_ADDREF(PlacesEventCounts) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PlacesEventCounts) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PlacesEventCounts) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +PlacesEventCounts::PlacesEventCounts() { + ErrorResult rv; + for (auto eventType : MakeWebIDLEnumeratedRange<PlacesEventType>()) { + PlacesEventCounts_Binding::MaplikeHelpers::Set( + this, NS_ConvertUTF8toUTF16(GetEnumString(eventType)), 0, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return; + } + } +} + +nsresult PlacesEventCounts::Increment(PlacesEventType aEventType) { + ErrorResult rv; + nsAutoCString eventName(GetEnumString(aEventType)); + uint64_t count = PlacesEventCounts_Binding::MaplikeHelpers::Get( + this, NS_ConvertUTF8toUTF16(eventName), rv); + if (MOZ_UNLIKELY(rv.Failed())) { + return rv.StealNSResult(); + } + PlacesEventCounts_Binding::MaplikeHelpers::Set( + this, NS_ConvertUTF8toUTF16(eventName), ++count, rv); + if (MOZ_UNLIKELY(rv.Failed())) { + return rv.StealNSResult(); + } + return NS_OK; +} + +JSObject* PlacesEventCounts::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return PlacesEventCounts_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/base/PlacesEventCounts.h b/dom/base/PlacesEventCounts.h new file mode 100644 index 0000000000..69abeac7b3 --- /dev/null +++ b/dom/base/PlacesEventCounts.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_PLACESEVENTCOUNTS_H_ +#define DOM_PLACESEVENTCOUNTS_H_ + +#include "js/TypeDecls.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/PlacesEventBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class PlacesEventCounts final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PlacesEventCounts) + + public: + PlacesEventCounts(); + + nsresult Increment(PlacesEventType aEventType); + + nsISupports* GetParentObject() const { return nullptr; }; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + ~PlacesEventCounts() = default; +}; + +} // namespace mozilla::dom + +#endif // DOM_PLACESEVENTCOUNTS_H_ diff --git a/dom/base/PlacesObservers.cpp b/dom/base/PlacesObservers.cpp index bb92e4a072..b5211733ea 100644 --- a/dom/base/PlacesObservers.cpp +++ b/dom/base/PlacesObservers.cpp @@ -22,7 +22,7 @@ struct Flagged { Flagged(const Flagged& aOther) = default; ~Flagged() = default; - uint32_t flags; + uint32_t flags = 0; T value; }; @@ -123,6 +123,14 @@ MOZ_CAN_RUN_SCRIPT void CallListeners( } } +StaticRefPtr<PlacesEventCounts> PlacesObservers::sCounts; +static void EnsureCountsInitialized() { + if (!PlacesObservers::sCounts) { + PlacesObservers::sCounts = new PlacesEventCounts(); + ClearOnShutdown(&PlacesObservers::sCounts); + } +} + void PlacesObservers::AddListener(GlobalObject& aGlobal, const nsTArray<PlacesEventType>& aEventTypes, PlacesEventCallback& aCallback, @@ -304,7 +312,11 @@ void PlacesObservers::NotifyListeners( if (aEvents.Length() == 0) { return; } - + EnsureCountsInitialized(); + for (const auto& event : aEvents) { + DebugOnly<nsresult> rv = sCounts->Increment(event->Type()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } #ifdef DEBUG if (!gNotificationQueue.IsEmpty()) { NS_WARNING( @@ -389,4 +401,10 @@ void PlacesObservers::NotifyNext() { } } +already_AddRefed<PlacesEventCounts> PlacesObservers::Counts( + const GlobalObject& global) { + EnsureCountsInitialized(); + return do_AddRef(sCounts); +}; + } // namespace mozilla::dom diff --git a/dom/base/PlacesObservers.h b/dom/base/PlacesObservers.h index e7aca305d8..434380e11f 100644 --- a/dom/base/PlacesObservers.h +++ b/dom/base/PlacesObservers.h @@ -11,6 +11,7 @@ #include "mozilla/dom/BindingDeclarations.h" #include "mozilla/dom/PlacesObserversBinding.h" #include "mozilla/dom/PlacesEvent.h" +#include "mozilla/dom/PlacesEventCounts.h" #include "mozilla/places/INativePlacesEventCallback.h" #include "nsIWeakReferenceUtils.h" @@ -49,6 +50,9 @@ class PlacesObservers { static void NotifyListeners( const Sequence<OwningNonNull<PlacesEvent>>& aEvents); + static StaticRefPtr<PlacesEventCounts> sCounts; + static already_AddRefed<PlacesEventCounts> Counts(const GlobalObject& global); + private: static void RemoveListener(uint32_t aFlags, PlacesEventCallback& aCallback); static void RemoveListener(uint32_t aFlags, diff --git a/dom/base/RangeBoundary.h b/dom/base/RangeBoundary.h index 2e4ab42397..4c5a70fb6a 100644 --- a/dom/base/RangeBoundary.h +++ b/dom/base/RangeBoundary.h @@ -9,6 +9,7 @@ #include "nsCOMPtr.h" #include "nsIContent.h" +#include "mozilla/dom/ShadowRoot.h" #include "mozilla/Assertions.h" #include "mozilla/Maybe.h" @@ -351,6 +352,34 @@ class RangeBoundaryBase { } public: + void NotifyParentBecomesShadowHost() { + MOZ_ASSERT(mParent); + MOZ_ASSERT(mParent->IsContainerNode(), + "Range is positioned on a text node!"); + if (!StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) { + return; + } + + if (!mIsMutationObserved) { + // RangeBoundaries that are not used in the context of a + // `MutationObserver` use the offset as main source of truth to compute + // `mRef`. Therefore, it must not be updated or invalidated. + return; + } + + if (!mRef) { + MOZ_ASSERT(mOffset.isSome() && mOffset.value() == 0, + "Invalidating offset of invalid RangeBoundary?"); + return; + } + + if (dom::ShadowRoot* shadowRoot = mParent->GetShadowRootForSelection()) { + mParent = shadowRoot; + } + + mOffset = Some(0); + } + bool IsSet() const { return mParent && (mRef || mOffset.isSome()); } bool IsSetAndValid() const { diff --git a/dom/base/RangeUtils.cpp b/dom/base/RangeUtils.cpp index 28283054b8..2383cc18aa 100644 --- a/dom/base/RangeUtils.cpp +++ b/dom/base/RangeUtils.cpp @@ -147,9 +147,10 @@ nsresult RangeUtils::CompareNodeToRange(nsINode* aNode, NS_WARN_IF(!aAbstractRange->IsPositioned())) { return NS_ERROR_INVALID_ARG; } - return CompareNodeToRangeBoundaries(aNode, aAbstractRange->StartRef(), - aAbstractRange->EndRef(), - aNodeIsBeforeRange, aNodeIsAfterRange); + return CompareNodeToRangeBoundaries( + aNode, aAbstractRange->MayCrossShadowBoundaryStartRef(), + aAbstractRange->MayCrossShadowBoundaryEndRef(), aNodeIsBeforeRange, + aNodeIsAfterRange); } template <typename SPT, typename SRT, typename EPT, typename ERT> nsresult RangeUtils::CompareNodeToRangeBoundaries( diff --git a/dom/base/ScriptableContentIterator.cpp b/dom/base/ScriptableContentIterator.cpp index d10c4e68f8..ab04053c29 100644 --- a/dom/base/ScriptableContentIterator.cpp +++ b/dom/base/ScriptableContentIterator.cpp @@ -109,6 +109,22 @@ ScriptableContentIterator::InitWithRange(IteratorType aType, nsRange* aRange) { } NS_IMETHODIMP +ScriptableContentIterator::InitWithRangeAllowCrossShadowBoundary( + IteratorType aType, nsRange* aRange) { + if (aType == NOT_INITIALIZED || + (mIteratorType != NOT_INITIALIZED && aType != mIteratorType) || + aType != SUBTREE_ITERATOR) { + return NS_ERROR_INVALID_ARG; + } + + mIteratorType = aType; + MOZ_ASSERT(mIteratorType == SUBTREE_ITERATOR); + EnsureContentIterator(); + return static_cast<ContentSubtreeIterator*>(mContentIterator.get()) + ->InitWithAllowCrossShadowBoundary(aRange); +} + +NS_IMETHODIMP ScriptableContentIterator::InitWithPositions(IteratorType aType, nsINode* aStartContainer, uint32_t aStartOffset, diff --git a/dom/base/Selection.cpp b/dom/base/Selection.cpp index 69986e6b78..7983ef98f9 100644 --- a/dom/base/Selection.cpp +++ b/dom/base/Selection.cpp @@ -22,9 +22,11 @@ #include "mozilla/CaretAssociationHint.h" #include "mozilla/ContentIterator.h" #include "mozilla/dom/Element.h" +#include "mozilla/dom/ChildIterator.h" #include "mozilla/dom/SelectionBinding.h" #include "mozilla/dom/ShadowRoot.h" #include "mozilla/dom/StaticRange.h" +#include "mozilla/dom/ShadowIncludingTreeIterator.h" #include "mozilla/ErrorResult.h" #include "mozilla/HTMLEditor.h" #include "mozilla/IntegerRange.h" @@ -778,30 +780,39 @@ NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(Selection) NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(Selection, Disconnect()) -const RangeBoundary& Selection::AnchorRef() const { +const RangeBoundary& Selection::AnchorRef( + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) const { if (!mAnchorFocusRange) { static RangeBoundary sEmpty; return sEmpty; } if (GetDirection() == eDirNext) { - return mAnchorFocusRange->StartRef(); + return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes + ? mAnchorFocusRange->MayCrossShadowBoundaryStartRef() + : mAnchorFocusRange->StartRef(); } - return mAnchorFocusRange->EndRef(); + return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes + ? mAnchorFocusRange->MayCrossShadowBoundaryEndRef() + : mAnchorFocusRange->EndRef(); } -const RangeBoundary& Selection::FocusRef() const { +const RangeBoundary& Selection::FocusRef( + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) const { if (!mAnchorFocusRange) { static RangeBoundary sEmpty; return sEmpty; } if (GetDirection() == eDirNext) { - return mAnchorFocusRange->EndRef(); + return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes + ? mAnchorFocusRange->MayCrossShadowBoundaryEndRef() + : mAnchorFocusRange->EndRef(); } - - return mAnchorFocusRange->StartRef(); + return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes + ? mAnchorFocusRange->MayCrossShadowBoundaryStartRef() + : mAnchorFocusRange->StartRef(); } void Selection::SetAnchorFocusRange(size_t aIndex) { @@ -818,8 +829,8 @@ static int32_t CompareToRangeStart(const nsINode& aCompareNode, uint32_t aCompareOffset, const AbstractRange& aRange, nsContentUtils::NodeIndexCache* aCache) { - MOZ_ASSERT(aRange.GetStartContainer()); - nsINode* start = aRange.GetStartContainer(); + MOZ_ASSERT(aRange.GetMayCrossShadowBoundaryStartContainer()); + nsINode* start = aRange.GetMayCrossShadowBoundaryStartContainer(); // If the nodes that we're comparing are not in the same document, assume that // aCompareNode will fall at the end of the ranges. if (aCompareNode.GetComposedDoc() != start->GetComposedDoc() || @@ -830,8 +841,9 @@ static int32_t CompareToRangeStart(const nsINode& aCompareNode, } // The points are in the same subtree, hence there has to be an order. - return *nsContentUtils::ComparePoints(&aCompareNode, aCompareOffset, start, - aRange.StartOffset(), aCache); + return *nsContentUtils::ComparePoints( + &aCompareNode, aCompareOffset, start, + aRange.MayCrossShadowBoundaryStartOffset(), aCache); } static int32_t CompareToRangeStart(const nsINode& aCompareNode, @@ -844,7 +856,7 @@ static int32_t CompareToRangeEnd(const nsINode& aCompareNode, uint32_t aCompareOffset, const AbstractRange& aRange) { MOZ_ASSERT(aRange.IsPositioned()); - nsINode* end = aRange.GetEndContainer(); + nsINode* end = aRange.GetMayCrossShadowBoundaryEndContainer(); // If the nodes that we're comparing are not in the same document or in the // same subtree, assume that aCompareNode will fall at the end of the ranges. if (aCompareNode.GetComposedDoc() != end->GetComposedDoc() || @@ -855,8 +867,9 @@ static int32_t CompareToRangeEnd(const nsINode& aCompareNode, } // The points are in the same subtree, hence there has to be an order. - return *nsContentUtils::ComparePoints(&aCompareNode, aCompareOffset, end, - aRange.EndOffset()); + return *nsContentUtils::ComparePoints( + &aCompareNode, aCompareOffset, end, + aRange.MayCrossShadowBoundaryEndOffset()); } // static @@ -1323,7 +1336,18 @@ nsresult Selection::RemoveCollapsedRanges() { nsresult Selection::StyledRanges::RemoveCollapsedRanges() { uint32_t i = 0; while (i < mRanges.Length()) { - if (mRanges[i].mRange->Collapsed()) { + const AbstractRange* range = mRanges[i].mRange; + // If nsRange::mCrossShadowBoundaryRange exists, it means + // there's a cross boundary selection, so obviously + // we shouldn't remove this range. + const bool collapsed = + range->Collapsed() && !range->MayCrossShadowBoundary(); + // Cross boundary range should always be uncollapsed. + MOZ_ASSERT_IF( + range->MayCrossShadowBoundary(), + !range->AsDynamicRange()->CrossShadowBoundaryRangeCollapsed()); + + if (collapsed) { nsresult rv = RemoveRangeAndUnregisterSelection(*mRanges[i].mRange); NS_ENSURE_SUCCESS(rv, rv); } else { @@ -1616,7 +1640,8 @@ nsresult Selection::StyledRanges::GetIndicesForInterval( // the given interval's start point, but that range isn't collapsed (a // collapsed range should be included in the returned results). const AbstractRange* beginRange = mRanges[beginsAfterIndex].mRange; - if (beginRange->EndRef().Equals(aBeginNode, aBeginOffset) && + if (beginRange->MayCrossShadowBoundaryEndRef().Equals(aBeginNode, + aBeginOffset) && !beginRange->Collapsed()) { beginsAfterIndex++; } @@ -1627,7 +1652,8 @@ nsresult Selection::StyledRanges::GetIndicesForInterval( // included if (endsBeforeIndex < mRanges.Length()) { const AbstractRange* endRange = mRanges[endsBeforeIndex].mRange; - if (endRange->StartRef().Equals(aEndNode, aEndOffset) && + if (endRange->MayCrossShadowBoundaryStartRef().Equals(aEndNode, + aEndOffset) && endRange->Collapsed()) { endsBeforeIndex++; } @@ -1710,6 +1736,16 @@ nsresult Selection::SelectFramesOfInclusiveDescendantsOfContent( return NS_OK; } +void Selection::SelectFramesOfShadowIncludingDescendantsOfContent( + nsIContent* aContent, bool aSelected) const { + MOZ_ASSERT(aContent); + MOZ_ASSERT(StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()); + for (nsINode* node : ShadowIncludingTreeIterator(*aContent)) { + nsIContent* innercontent = node->IsContent() ? node->AsContent() : nullptr; + SelectFramesOf(innercontent, aSelected); + } +} + void Selection::SelectFramesInAllRanges(nsPresContext* aPresContext) { // this method is currently only called in a user-initiated context. // therefore it is safe to assume that we are not in a Highlight selection @@ -1748,16 +1784,22 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, if (mFrameSelection->IsInTableSelectionMode()) { const nsIContent* const commonAncestorContent = - nsIContent::FromNodeOrNull(aRange.GetClosestCommonInclusiveAncestor()); + nsIContent::FromNodeOrNull(aRange.GetClosestCommonInclusiveAncestor( + StaticPrefs::dom_select_events_textcontrols_selectstart_enabled() + ? AllowRangeCrossShadowBoundary::Yes + : AllowRangeCrossShadowBoundary::No)); nsIFrame* const frame = commonAncestorContent ? commonAncestorContent->GetPrimaryFrame() : aPresContext->PresShell()->GetRootFrame(); if (frame) { if (frame->IsTextFrame()) { - MOZ_ASSERT(commonAncestorContent == aRange.GetStartContainer()); - MOZ_ASSERT(commonAncestorContent == aRange.GetEndContainer()); + MOZ_ASSERT(commonAncestorContent == + aRange.GetMayCrossShadowBoundaryStartContainer()); + MOZ_ASSERT(commonAncestorContent == + aRange.GetMayCrossShadowBoundaryEndContainer()); static_cast<nsTextFrame*>(frame)->SelectionStateChanged( - aRange.StartOffset(), aRange.EndOffset(), aSelect, mSelectionType); + aRange.MayCrossShadowBoundaryStartOffset(), + aRange.MayCrossShadowBoundaryEndOffset(), aSelect, mSelectionType); } else { frame->SelectionStateChanged(); } @@ -1768,8 +1810,8 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, // Loop through the content iterator for each content node; for each text // node, call SetSelected on it: - nsIContent* const startContent = - nsIContent::FromNodeOrNull(aRange.GetStartContainer()); + nsIContent* const startContent = nsIContent::FromNodeOrNull( + aRange.GetMayCrossShadowBoundaryStartContainer()); if (MOZ_UNLIKELY(!startContent)) { // Don't warn, bug 1055722 // XXX The range can start from a document node and such range can be @@ -1780,7 +1822,7 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, MOZ_DIAGNOSTIC_ASSERT(startContent->IsInComposedDoc()); // We must call first one explicitly - nsINode* const endNode = aRange.GetEndContainer(); + nsINode* const endNode = aRange.GetMayCrossShadowBoundaryEndContainer(); if (NS_WARN_IF(!endNode)) { // We null-checked start node above, therefore, end node should also be // non-null here. @@ -1792,10 +1834,10 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, // The frame could be an SVG text frame, in which case we don't treat it // as a text frame. if (frame->IsTextFrame()) { - const uint32_t startOffset = aRange.StartOffset(); - const uint32_t endOffset = endNode == startContent - ? aRange.EndOffset() - : startContent->Length(); + const uint32_t startOffset = aRange.MayCrossShadowBoundaryStartOffset(); + const uint32_t endOffset = + endNode == startContent ? aRange.MayCrossShadowBoundaryEndOffset() + : startContent->Length(); static_cast<nsTextFrame*>(frame)->SelectionStateChanged( startOffset, endOffset, aSelect, mSelectionType); } else { @@ -1806,7 +1848,7 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, // If the range is in a node and the node is a leaf node, we don't need to // walk the subtree. - if (aRange.Collapsed() || + if ((aRange.Collapsed() && !aRange.MayCrossShadowBoundary()) || (startContent == endNode && !startContent->HasChildren())) { if (!isFirstContentTextNode) { SelectFramesOf(startContent, aSelect); @@ -1815,7 +1857,7 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, } ContentSubtreeIterator subtreeIter; - subtreeIter.Init(&aRange); + subtreeIter.InitWithAllowCrossShadowBoundary(&aRange); if (isFirstContentTextNode && !subtreeIter.IsDone() && subtreeIter.GetCurrentNode() == startContent) { subtreeIter.Next(); // first content has already been handled. @@ -1825,8 +1867,12 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, MOZ_DIAGNOSTIC_ASSERT(subtreeIter.GetCurrentNode()); if (nsIContent* const content = nsIContent::FromNodeOrNull(subtreeIter.GetCurrentNode())) { - SelectFramesOfInclusiveDescendantsOfContent(postOrderIter, content, - aSelect); + if (StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) { + SelectFramesOfShadowIncludingDescendantsOfContent(content, aSelect); + } else { + SelectFramesOfInclusiveDescendantsOfContent(postOrderIter, content, + aSelect); + } } } @@ -1839,7 +1885,7 @@ nsresult Selection::SelectFrames(nsPresContext* aPresContext, // The frame could be an SVG text frame, in which case we'll ignore it. if (frame->IsTextFrame()) { static_cast<nsTextFrame*>(frame)->SelectionStateChanged( - 0, aRange.EndOffset(), aSelect, mSelectionType); + 0, aRange.MayCrossShadowBoundaryEndOffset(), aSelect, mSelectionType); } } return NS_OK; @@ -1901,10 +1947,11 @@ UniquePtr<SelectionDetails> Selection::LookUpSelection( if (range->IsStaticRange() && !range->AsStaticRange()->IsValid()) { continue; } - nsINode* startNode = range->GetStartContainer(); - nsINode* endNode = range->GetEndContainer(); - uint32_t startOffset = range->StartOffset(); - uint32_t endOffset = range->EndOffset(); + + nsINode* startNode = range->GetMayCrossShadowBoundaryStartContainer(); + nsINode* endNode = range->GetMayCrossShadowBoundaryEndContainer(); + uint32_t startOffset = range->MayCrossShadowBoundaryStartOffset(); + uint32_t endOffset = range->MayCrossShadowBoundaryEndOffset(); Maybe<uint32_t> start, end; if (startNode == aContent && endNode == aContent) { @@ -2184,6 +2231,67 @@ void Selection::RemoveAllRanges(ErrorResult& aRv) { RemoveAllRangesInternal(aRv); } +already_AddRefed<StaticRange> Selection::GetComposedRange( + const AbstractRange* aRange, + const Sequence<OwningNonNull<ShadowRoot>>& aShadowRoots) const { + // If aIsEndNode is true, this method does the Step 5.1 and 5.2 + // in https://www.w3.org/TR/selection-api/#dom-selection-getcomposedranges, + // otherwise it does the Step 3.1 and 3.2. + auto reScope = [&aShadowRoots](nsINode*& aNode, uint32_t& aOffset, + bool aIsEndNode) { + MOZ_ASSERT(aNode); + while (aNode) { + const ShadowRoot* shadowRootOfNode = aNode->GetContainingShadow(); + if (!shadowRootOfNode) { + return; + } + + for (const OwningNonNull<ShadowRoot>& shadowRoot : aShadowRoots) { + if (shadowRoot->IsShadowIncludingInclusiveDescendantOf( + shadowRootOfNode)) { + return; + } + } + + const nsIContent* host = aNode->GetContainingShadowHost(); + const Maybe<uint32_t> maybeIndex = host->ComputeIndexInParentContent(); + MOZ_ASSERT(maybeIndex.isSome(), "not parent or anonymous child?"); + if (MOZ_UNLIKELY(maybeIndex.isNothing())) { + // Unlikely to happen, but still set aNode to nullptr to avoid + // leaking information about the shadow tree. + aNode = nullptr; + return; + } + aOffset = maybeIndex.value(); + if (aIsEndNode) { + aOffset += 1; + } + aNode = host->GetParentNode(); + } + }; + + nsINode* startNode = aRange->GetMayCrossShadowBoundaryStartContainer(); + uint32_t startOffset = aRange->MayCrossShadowBoundaryStartOffset(); + nsINode* endNode = aRange->GetMayCrossShadowBoundaryEndContainer(); + uint32_t endOffset = aRange->MayCrossShadowBoundaryEndOffset(); + + reScope(startNode, startOffset, false /* aIsEndNode */); + reScope(endNode, endOffset, true /* aIsEndNode */); + + RefPtr<StaticRange> composedRange = StaticRange::Create( + startNode, startOffset, endNode, endOffset, IgnoreErrors()); + return composedRange.forget(); +} + +void Selection::GetComposedRanges( + const Sequence<OwningNonNull<ShadowRoot>>& aShadowRoots, + nsTArray<RefPtr<StaticRange>>& aComposedRanges) { + aComposedRanges.SetCapacity(mStyledRanges.mRanges.Length()); + for (const auto& range : mStyledRanges.mRanges) { + aComposedRanges.AppendElement(GetComposedRange(range.mRange, aShadowRoots)); + } +} + void Selection::RemoveAllRangesInternal(ErrorResult& aRv) { if (!mFrameSelection) { aRv.Throw(NS_ERROR_NOT_INITIALIZED); @@ -2501,11 +2609,15 @@ void Selection::CollapseInternal(InLimiter aInLimiter, // Hack to display the caret on the right line (bug 1237236). if (frameSelection->GetHint() == CaretAssociationHint::Before && aPoint.Container()->IsContent()) { - int32_t frameOffset; - nsTextFrame* f = do_QueryFrame(nsCaret::GetFrameAndOffset( - this, aPoint.Container(), - *aPoint.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets), - &frameOffset)); + const nsCaret::CaretPosition pos{ + aPoint.Container(), + int32_t(*aPoint.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets)), + frameSelection->GetHint(), frameSelection->GetCaretBidiLevel()}; + CaretFrameData frameData = nsCaret::GetFrameAndOffset(pos); + if (frameData.mFrame) { + frameSelection->SetHint(frameData.mHint); + } + nsTextFrame* f = do_QueryFrame(frameData.mFrame); if (f && f->IsAtEndOfLine() && f->HasSignificantTerminalNewline()) { // RawRangeBounary::Offset() causes computing offset if it's not been // done yet. However, it's called only when the container is a text @@ -2672,6 +2784,19 @@ AbstractRange* Selection::GetAbstractRangeAt(uint32_t aIndex) const { return mStyledRanges.mRanges.SafeElementAt(aIndex, empty).mRange; } +void Selection::GetDirection(nsAString& aDirection) const { + if (mStyledRanges.mRanges.IsEmpty() || + (mFrameSelection && (mFrameSelection->IsDoubleClickSelection() || + mFrameSelection->IsTripleClickSelection()))) { + // Empty range and double/triple clicks result a directionless selection. + aDirection.AssignLiteral("none"); + } else if (mDirection == nsDirection::eDirPrevious) { + aDirection.AssignLiteral("backward"); + } else { + aDirection.AssignLiteral("forward"); + } +} + nsRange* Selection::GetRangeAt(uint32_t aIndex) const { // This method per IDL spec returns a dynamic range. // Therefore, it must be ensured that it is only called @@ -2836,17 +2961,17 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, #ifdef DEBUG_SELECTION nsDirection oldDirection = GetDirection(); #endif - nsINode* anchorNode = GetAnchorNode(); - nsINode* focusNode = GetFocusNode(); - const uint32_t anchorOffset = AnchorOffset(); - const uint32_t focusOffset = FocusOffset(); + nsINode* anchorNode = GetMayCrossShadowBoundaryAnchorNode(); + nsINode* focusNode = GetMayCrossShadowBoundaryFocusNode(); + const uint32_t anchorOffset = MayCrossShadowBoundaryAnchorOffset(); + const uint32_t focusOffset = MayCrossShadowBoundaryFocusOffset(); RefPtr<nsRange> range = mAnchorFocusRange->CloneRange(); - nsINode* startNode = range->GetStartContainer(); - nsINode* endNode = range->GetEndContainer(); - const uint32_t startOffset = range->StartOffset(); - const uint32_t endOffset = range->EndOffset(); + nsINode* startNode = range->GetMayCrossShadowBoundaryStartContainer(); + nsINode* endNode = range->GetMayCrossShadowBoundaryEndContainer(); + const uint32_t startOffset = range->MayCrossShadowBoundaryStartOffset(); + const uint32_t endOffset = range->MayCrossShadowBoundaryEndOffset(); bool shouldClearRange = false; const Maybe<int32_t> anchorOldFocusOrder = nsContentUtils::ComparePoints( @@ -2882,7 +3007,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, (*anchorOldFocusOrder <= 0 && *oldFocusNewFocusOrder < 0)) { // a1,2 a,1,2 // select from 1 to 2 unless they are collapsed - range->SetEnd(aContainer, aOffset, aRv); + range->SetEnd(aContainer, aOffset, aRv, + AllowRangeCrossShadowBoundary::Yes); if (aRv.Failed()) { return; } @@ -2903,7 +3029,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, *anchorNewFocusOrder > 0) { // 2, a1 // select from 2 to 1a SetDirection(eDirPrevious); - range->SetStart(aContainer, aOffset, aRv); + range->SetStart(aContainer, aOffset, aRv, + AllowRangeCrossShadowBoundary::Yes); if (aRv.Failed()) { return; } @@ -2923,7 +3050,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, return; } - range->SetEnd(aContainer, aOffset, aRv); + range->SetEnd(aContainer, aOffset, aRv, + AllowRangeCrossShadowBoundary::Yes); if (aRv.Failed()) { return; } @@ -2933,27 +3061,33 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, return; } SelectFrames(presContext, *difRange, false); // deselect now - difRange->SetEnd(range->GetEndContainer(), range->EndOffset()); + difRange->SetEnd(range->GetMayCrossShadowBoundaryEndContainer(), + range->MayCrossShadowBoundaryEndOffset(), + AllowRangeCrossShadowBoundary::Yes); SelectFrames(presContext, *difRange, true); // must reselect last node // maybe more } else if (*anchorOldFocusOrder >= 0 && *anchorNewFocusOrder <= 0) { // 1,a,2 or 1a,2 or 1,a2 or 1a2 if (GetDirection() == eDirPrevious) { - res = range->SetStart(endNode, endOffset); + res = range->SetStart(endNode, endOffset, + AllowRangeCrossShadowBoundary::Yes); if (NS_FAILED(res)) { aRv.Throw(res); return; } } SetDirection(eDirNext); - range->SetEnd(aContainer, aOffset, aRv); + range->SetEnd(aContainer, aOffset, aRv, + AllowRangeCrossShadowBoundary::Yes); if (aRv.Failed()) { return; } if (focusNode != anchorNode || focusOffset != anchorOffset) { // if collapsed diff dont do anything - res = difRange->SetStart(focusNode, focusOffset); - nsresult tmp = difRange->SetEnd(anchorNode, anchorOffset); + res = difRange->SetStart(focusNode, focusOffset, + AllowRangeCrossShadowBoundary::Yes); + nsresult tmp = difRange->SetEnd(anchorNode, anchorOffset, + AllowRangeCrossShadowBoundary::Yes); if (NS_FAILED(tmp)) { res = tmp; } @@ -2987,7 +3121,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, return; } SetDirection(eDirPrevious); - range->SetStart(aContainer, aOffset, aRv); + range->SetStart(aContainer, aOffset, aRv, + AllowRangeCrossShadowBoundary::Yes); if (aRv.Failed()) { return; } @@ -2998,15 +3133,19 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, return; } SelectFrames(presContext, *difRange, false); - difRange->SetStart(range->GetStartContainer(), range->StartOffset()); + difRange->SetStart(range->GetMayCrossShadowBoundaryStartContainer(), + range->MayCrossShadowBoundaryStartOffset(), + AllowRangeCrossShadowBoundary::Yes); SelectFrames(presContext, *difRange, true); // must reselect last node } else if (*anchorNewFocusOrder >= 0 && *anchorOldFocusOrder <= 0) { // 2,a,1 or 2a,1 or 2,a1 or 2a1 if (GetDirection() == eDirNext) { - range->SetEnd(startNode, startOffset); + range->SetEnd(startNode, startOffset, + AllowRangeCrossShadowBoundary::Yes); } SetDirection(eDirPrevious); - range->SetStart(aContainer, aOffset, aRv); + range->SetStart(aContainer, aOffset, aRv, + AllowRangeCrossShadowBoundary::Yes); if (aRv.Failed()) { return; } @@ -3036,7 +3175,8 @@ void Selection::Extend(nsINode& aContainer, uint32_t aOffset, } else if (*oldFocusNewFocusOrder >= 0 && *anchorOldFocusOrder >= 0) { // 2,1,a or 21,a or 2,1a or 21a // select from 2 to 1 - range->SetStart(aContainer, aOffset, aRv); + range->SetStart(aContainer, aOffset, aRv, + AllowRangeCrossShadowBoundary::Yes); if (aRv.Failed()) { return; } @@ -3604,9 +3744,10 @@ void Selection::NotifySelectionListeners() { RefPtr<nsFrameSelection> frameSelection = mFrameSelection; - // This flag will be set to true if a selection by double click is detected. - // As soon as the selection is modified, it needs to be set to false. - frameSelection->SetIsDoubleClickSelection(false); + // This flag will be set to Double or Triple if a selection by double click or + // triple click is detected. As soon as the selection is modified, it needs to + // be reset to NotApplicable. + frameSelection->SetClickSelectionType(ClickSelectionType::NotApplicable); if (frameSelection->IsBatching()) { frameSelection->SetChangesDuringBatchingFlag(); diff --git a/dom/base/Selection.h b/dom/base/Selection.h index 9f031ab3cf..08563993ac 100644 --- a/dom/base/Selection.h +++ b/dom/base/Selection.h @@ -64,6 +64,9 @@ namespace dom { class Selection final : public nsSupportsWeakReference, public nsWrapperCache, public SupportsWeakPtr { + using AllowRangeCrossShadowBoundary = + mozilla::dom::AllowRangeCrossShadowBoundary; + protected: virtual ~Selection(); @@ -205,6 +208,10 @@ class Selection final : public nsSupportsWeakReference, nsRange* aRange, Maybe<size_t>* aOutIndex, DispatchSelectstartEvent aDispatchSelectstartEvent); + already_AddRefed<StaticRange> GetComposedRange( + const AbstractRange* aRange, + const Sequence<OwningNonNull<ShadowRoot>>& aShadowRoots) const; + public: nsresult RemoveCollapsedRanges(); void Clear(nsPresContext* aPresContext); @@ -246,6 +253,8 @@ class Selection final : public nsSupportsWeakReference, // anchor and which end is focus. const nsRange* GetAnchorFocusRange() const { return mAnchorFocusRange; } + void GetDirection(nsAString& aDirection) const; + nsDirection GetDirection() const { return mDirection; } void SetDirection(nsDirection aDir) { mDirection = aDir; } @@ -321,6 +330,30 @@ class Selection final : public nsSupportsWeakReference, return offset ? *offset : 0; } + nsINode* GetMayCrossShadowBoundaryAnchorNode() const { + const RangeBoundary& anchor = AnchorRef(AllowRangeCrossShadowBoundary::Yes); + return anchor.IsSet() ? anchor.Container() : nullptr; + } + + uint32_t MayCrossShadowBoundaryAnchorOffset() const { + const RangeBoundary& anchor = AnchorRef(AllowRangeCrossShadowBoundary::Yes); + const Maybe<uint32_t> offset = + anchor.Offset(RangeBoundary::OffsetFilter::kValidOffsets); + return offset ? *offset : 0; + } + + nsINode* GetMayCrossShadowBoundaryFocusNode() const { + const RangeBoundary& focus = FocusRef(AllowRangeCrossShadowBoundary::Yes); + return focus.IsSet() ? focus.Container() : nullptr; + } + + uint32_t MayCrossShadowBoundaryFocusOffset() const { + const RangeBoundary& focus = FocusRef(AllowRangeCrossShadowBoundary::Yes); + const Maybe<uint32_t> offset = + focus.Offset(RangeBoundary::OffsetFilter::kValidOffsets); + return offset ? *offset : 0; + } + nsIContent* GetChildAtAnchorOffset() { const RangeBoundary& anchor = AnchorRef(); return anchor.IsSet() ? anchor.GetChildAtOffset() : nullptr; @@ -330,8 +363,12 @@ class Selection final : public nsSupportsWeakReference, return focus.IsSet() ? focus.GetChildAtOffset() : nullptr; } - const RangeBoundary& AnchorRef() const; - const RangeBoundary& FocusRef() const; + const RangeBoundary& AnchorRef( + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No) const; + const RangeBoundary& FocusRef( + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No) const; /* * IsCollapsed -- is the whole selection just one point, or unset? @@ -385,6 +422,10 @@ class Selection final : public nsSupportsWeakReference, MOZ_CAN_RUN_SCRIPT void RemoveAllRanges(mozilla::ErrorResult& aRv); + void GetComposedRanges( + const Sequence<OwningNonNull<ShadowRoot>>& aShadowRoots, + nsTArray<RefPtr<StaticRange>>& aComposedRanges); + /** * Whether Stringify should flush layout or not. */ @@ -810,6 +851,12 @@ class Selection final : public nsSupportsWeakReference, PostContentIterator& aPostOrderIter, nsIContent* aContent, bool aSelected) const; + /** + * https://dom.spec.whatwg.org/#concept-shadow-including-descendant + */ + void SelectFramesOfShadowIncludingDescendantsOfContent(nsIContent* aContent, + bool aSelected) const; + nsresult SelectFrames(nsPresContext* aPresContext, AbstractRange& aRange, bool aSelect) const; diff --git a/dom/base/StaticRange.cpp b/dom/base/StaticRange.cpp index 0946e8f9bf..73ff04c038 100644 --- a/dom/base/StaticRange.cpp +++ b/dom/base/StaticRange.cpp @@ -100,6 +100,13 @@ bool StaticRange::IsValid() const { return false; } + MOZ_ASSERT(mAreStartAndEndInSameTree == + (RangeUtils::ComputeRootNode(mStart.Container()) == + RangeUtils::ComputeRootNode(mEnd.Container()))); + if (!mAreStartAndEndInSameTree) { + return false; + } + const Maybe<int32_t> pointOrder = nsContentUtils::ComparePoints(mStart, mEnd); return pointOrder.isSome() && *pointOrder <= 0; } @@ -119,6 +126,9 @@ void StaticRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary, if (checkCommonAncestor) { UpdateCommonAncestorIfNecessary(); } + + mAreStartAndEndInSameTree = RangeUtils::ComputeRootNode(mStart.Container()) == + RangeUtils::ComputeRootNode(mEnd.Container()); } /* static */ diff --git a/dom/base/StaticRange.h b/dom/base/StaticRange.h index a6f677130d..af7054f843 100644 --- a/dom/base/StaticRange.h +++ b/dom/base/StaticRange.h @@ -8,6 +8,7 @@ #define mozilla_dom_StaticRange_h #include "mozilla/RangeBoundary.h" +#include "mozilla/RangeUtils.h" #include "mozilla/dom/AbstractRange.h" #include "mozilla/dom/StaticRangeBinding.h" #include "nsTArray.h" @@ -70,6 +71,21 @@ class StaticRange final : public AbstractRange { */ bool IsValid() const; + void NotifyNodeBecomesShadowHost(nsINode* aNode) { + if (aNode == mStart.Container()) { + mStart.NotifyParentBecomesShadowHost(); + } + + if (aNode == mEnd.Container()) { + mEnd.NotifyParentBecomesShadowHost(); + } + } + + private: + // Whether the start and end points are in the same tree. + // They could be in different trees, i.e, cross shadow boundaries. + bool mAreStartAndEndInSameTree = false; + protected: explicit StaticRange(nsINode* aNode) : AbstractRange(aNode, /* aIsDynamicRange = */ false) {} diff --git a/dom/base/ThirdPartyUtil.cpp b/dom/base/ThirdPartyUtil.cpp index 2c71ab3d94..0ced8aa7d8 100644 --- a/dom/base/ThirdPartyUtil.cpp +++ b/dom/base/ThirdPartyUtil.cpp @@ -200,9 +200,9 @@ ThirdPartyUtil::IsThirdPartyWindow(mozIDOMWindowProxy* aWindow, nsIURI* aURI, bool result; - // Ignore about:blank URIs here since they have no domain and attempting to - // compare against them will fail. - if (aURI && !NS_IsAboutBlank(aURI)) { + // Ignore about:blank and about:srcdoc URIs here since they have no domain + // and attempting to compare against them will fail. + if (aURI && !NS_IsAboutBlank(aURI) && !NS_IsAboutSrcdoc(aURI)) { nsCOMPtr<nsIPrincipal> prin; nsresult rv = GetPrincipalFromWindow(aWindow, getter_AddRefs(prin)); NS_ENSURE_SUCCESS(rv, rv); @@ -320,10 +320,10 @@ ThirdPartyUtil::IsThirdPartyChannel(nsIChannel* aChannel, nsIURI* aURI, } } - // Special consideration must be done for about:blank URIs because those - // inherit the principal from the parent context. For them, let's consider the - // principal URI. - if (NS_IsAboutBlank(channelURI)) { + // Special consideration must be done for about:blank and about:srcdoc URIs + // because those inherit the principal from the parent context. For them, + // let's consider the principal URI. + if (NS_IsAboutBlank(channelURI) || NS_IsAboutSrcdoc(channelURI)) { nsCOMPtr<nsIPrincipal> principalToInherit = loadInfo->FindPrincipalToInherit(aChannel); if (!principalToInherit) { diff --git a/dom/base/UseCounters.conf b/dom/base/UseCounters.conf index 86d782b476..66507b6d49 100644 --- a/dom/base/UseCounters.conf +++ b/dom/base/UseCounters.conf @@ -62,11 +62,6 @@ method DataTransfer.mozGetDataAt attribute DataTransfer.mozUserCancelled attribute DataTransfer.mozSourceNode -// Marquee events -custom onstart sets a <marquee> onstart event listener -custom onbounce sets a <marquee> onbounce event listener -custom onfinish sets a <marquee> onfinish event listener - // Element non-standard events custom onoverflow sets an element onoverflow event listener custom onunderflow sets an element onunderflow event listener diff --git a/dom/base/crashtests/1697256.html b/dom/base/crashtests/1697256.html index 25024083e3..3d6634e952 100644 --- a/dom/base/crashtests/1697256.html +++ b/dom/base/crashtests/1697256.html @@ -4,8 +4,9 @@ <script> window.onload = () => { window.requestIdleCallback(() => { - SpecialPowers.wrap(self).printPreview() + let pp = SpecialPowers.wrap(self).printPreview() setTimeout(() => { + try { pp.close(); } catch (e) { } document.documentElement.classList.remove("reftest-wait"); }, 250) }) diff --git a/dom/base/crashtests/1887930.html b/dom/base/crashtests/1887930.html new file mode 100644 index 0000000000..04a89de8d2 --- /dev/null +++ b/dom/base/crashtests/1887930.html @@ -0,0 +1,7 @@ +<script> +document.addEventListener("DOMContentLoaded", () => { + document.getSelection().extend(a) +}) +</script> +<dialog id="a"></dialog> +<input type="datetime-local" autofocus="autofocus"> diff --git a/dom/base/crashtests/1887963_1.html b/dom/base/crashtests/1887963_1.html new file mode 100644 index 0000000000..fbc30dc587 --- /dev/null +++ b/dom/base/crashtests/1887963_1.html @@ -0,0 +1,15 @@ +<script> +document.addEventListener("DOMContentLoaded", () => { + let c = a.attachShadow({mode: "open"}) + let b = document.getElementById("host").shadowRoot.getElementById("b"); + window.parent.getSelection().setBaseAndExtent(b, 0, c, 0) +}) +</script> +<div id="a">A</div> +<video> + <div id="host"> + <template shadowrootmode="open"> + <video id="b"> + </template> + </div> +</video> diff --git a/dom/base/crashtests/1887963_2.html b/dom/base/crashtests/1887963_2.html new file mode 100644 index 0000000000..53f512173b --- /dev/null +++ b/dom/base/crashtests/1887963_2.html @@ -0,0 +1,15 @@ +<script> +document.addEventListener("DOMContentLoaded", () => { + let c = a.attachShadow({mode: "open"}) + let b = document.getElementById("not-slotted"); + window.parent.getSelection().setBaseAndExtent(b, 0, c, 0) +}) +</script> +<div id="a">A</div> +<video> + <div id="host"> + <template shadowrootmode="open"> + </template> + <span id="not-slotted">NotSlotted</span> + </div> +</video> diff --git a/dom/base/crashtests/1887974.html b/dom/base/crashtests/1887974.html new file mode 100644 index 0000000000..85ffd2b02a --- /dev/null +++ b/dom/base/crashtests/1887974.html @@ -0,0 +1,18 @@ +<script> +document.addEventListener("DOMContentLoaded", () => { + c.add(d, 1) + b.addEventListener("DOMNodeRemoved", () => { + a.appendChild(f) + }) + let r = document.createRange() + r.setEndBefore(d) + r.deleteContents() +}) +</script> +<audio> +<canvas id="b"> +</canvas> +<select id="c"> +<option id="d">A</option> +<optgroup id="f"> +<input id="a"> diff --git a/dom/base/crashtests/1890888.html b/dom/base/crashtests/1890888.html new file mode 100644 index 0000000000..006768ae14 --- /dev/null +++ b/dom/base/crashtests/1890888.html @@ -0,0 +1,13 @@ +<script> +window.addEventListener("DOMContentLoaded", () => { + o1.prepend(o4) + document.getSelection().setBaseAndExtent(o5.attachShadow({mode: "closed"}), 0, o3, 0) + o5.replaceWith(o2) + setTimeout(window.close, 500) +}) +</script> +<iframe id="o1"></iframe> +<picture id="o2"></picture> +<area id="o3"> +<meter id="o4"> +<p id="o5"> diff --git a/dom/base/crashtests/crashtests.list b/dom/base/crashtests/crashtests.list index 864538ddf5..22aaf50e6b 100644 --- a/dom/base/crashtests/crashtests.list +++ b/dom/base/crashtests/crashtests.list @@ -271,3 +271,8 @@ load 1835886.html load 1836824.html skip-if(Android) load 1838484.html load 1840191.html +load 1887930.html +load 1887963_1.html +load 1887963_2.html +asserts(0-1) load 1887974.html +load 1890888.html diff --git a/dom/base/fragmentdirectives/Cargo.toml b/dom/base/fragmentdirectives/Cargo.toml new file mode 100644 index 0000000000..7b3b589668 --- /dev/null +++ b/dom/base/fragmentdirectives/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dom_fragmentdirectives" +version = "0.1.0" +authors = ["Jan Jaeschke <jjaschke@mozilla.com>"] +edition = "2021" +license = "MPL-2.0" + +[dependencies] +nsstring = { path = "../../../xpcom/rust/nsstring/" } +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +percent-encoding = { version = "2.3.1" } +[lib] +path = "lib.rs" diff --git a/dom/base/fragmentdirectives/cbindgen.toml b/dom/base/fragmentdirectives/cbindgen.toml new file mode 100644 index 0000000000..ec54ebc02d --- /dev/null +++ b/dom/base/fragmentdirectives/cbindgen.toml @@ -0,0 +1,15 @@ +header = """/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */""" +autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */ +""" +include_version = true +braces = "SameLine" +line_length = 100 +tab_width = 2 +language = "C++" +include_guard = "fragmentdirectives_ffi_generated_h" +includes = ["nsStringFwd.h", "nsTArrayForwardDeclare.h"] + +[export.rename] +"ThinVec" = "nsTArray" diff --git a/dom/base/fragmentdirectives/fragment_directive_impl.rs b/dom/base/fragmentdirectives/fragment_directive_impl.rs new file mode 100644 index 0000000000..dfbdb37415 --- /dev/null +++ b/dom/base/fragmentdirectives/fragment_directive_impl.rs @@ -0,0 +1,342 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +use percent_encoding::{percent_decode, percent_encode, NON_ALPHANUMERIC}; +use std::str; + +/// The `FragmentDirectiveParameter` represents one of +/// `[prefix-,]start[,end][,-suffix]` without any surrounding `-` or `,`. +/// +/// The token is stored as percent-decoded string. +/// Therefore, interfaces exist to +/// - create a `FragmentDirectiveParameter` from a percent-encoded string. +/// This function will determine from occurrence and position of a dash +/// if the token represents a `prefix`, `suffix` or either `start` or `end`. +/// - create a percent-encoded string from the value the token holds. +pub enum TextDirectiveParameter { + Prefix(String), + StartOrEnd(String), + Suffix(String), +} + +impl TextDirectiveParameter { + /// Creates a token from a percent-encoded string. + /// Based on position of a dash the correct token type is determined. + /// Returns `None` in case of an ill-formed token: + /// - starts and ends with a dash (i.e. `-token-`) + /// - only consists of a dash (i.e. `-`) or is empty + /// - conversion from percent-encoded string to utf8 fails. + pub fn from_percent_encoded(token: &[u8]) -> Option<Self> { + if token.is_empty() { + return None; + } + let starts_with_dash = *token.first().unwrap() == b'-'; + let ends_with_dash = *token.last().unwrap() == b'-'; + if starts_with_dash && ends_with_dash { + // `-token-` is not valid. + return None; + } + if token.len() == 1 && starts_with_dash { + // `-` is not valid. + return None; + } + // Note: Trimming of the raw strings is currently not mentioned in the spec. + // However, it looks as it is implicitly expected. + if starts_with_dash { + if let Ok(decoded_suffix) = percent_decode(&token[1..]).decode_utf8() { + return Some(TextDirectiveParameter::Suffix(String::from( + decoded_suffix.trim(), + ))); + } + return None; + } + if ends_with_dash { + if let Ok(decoded_prefix) = percent_decode(&token[..token.len() - 1]).decode_utf8() { + return Some(TextDirectiveParameter::Prefix(String::from( + decoded_prefix.trim(), + ))); + } + return None; + } + if let Ok(decoded_text) = percent_decode(&token).decode_utf8() { + return Some(TextDirectiveParameter::StartOrEnd(String::from( + decoded_text.trim(), + ))); + } + None + } + + /// Returns the value of the token as percent-decoded `String`. + pub fn value(&self) -> &String { + match self { + TextDirectiveParameter::Prefix(value) => &value, + TextDirectiveParameter::StartOrEnd(value) => &value, + TextDirectiveParameter::Suffix(value) => &value, + } + } + + /// Creates a percent-encoded string of the token's value. + /// This includes placing a dash appropriately + /// to indicate whether this token is prefix, suffix or start/end. + /// + /// This method always returns a new object. + pub fn to_percent_encoded_string(&self) -> String { + let encode = |text: &String| percent_encode(text.as_bytes(), NON_ALPHANUMERIC).to_string(); + match self { + Self::Prefix(text) => encode(text) + "-", + Self::StartOrEnd(text) => encode(text), + Self::Suffix(text) => { + let encoded = encode(text); + let mut result = String::with_capacity(encoded.len() + 1); + result.push_str("-"); + result.push_str(&encoded); + result + } + } + } +} + +/// This struct represents one parsed text directive using Rust types. +/// +/// A text fragment is encoded into a URL fragment like this: +/// `text=[prefix-,]start[,end][,-suffix]` +/// +/// The text directive is considered valid if at least `start` is not None. +/// (see `Self::is_valid()`). +#[derive(Default)] +pub struct TextDirective { + prefix: Option<TextDirectiveParameter>, + start: Option<TextDirectiveParameter>, + end: Option<TextDirectiveParameter>, + suffix: Option<TextDirectiveParameter>, +} +impl TextDirective { + /// Creates an instance from string parts. + /// This function is intended to be used when a fragment directive string should be created. + /// Returns `None` if `start` is empty. + pub fn from_parts(prefix: String, start: String, end: String, suffix: String) -> Option<Self> { + if !start.is_empty() { + Some(Self { + prefix: if !prefix.is_empty() { + Some(TextDirectiveParameter::Prefix(prefix.trim().into())) + } else { + None + }, + start: Some(TextDirectiveParameter::StartOrEnd(start.trim().into())), + end: if !end.is_empty() { + Some(TextDirectiveParameter::StartOrEnd(end.trim().into())) + } else { + None + }, + suffix: if !suffix.is_empty() { + Some(TextDirectiveParameter::Suffix(suffix.trim().into())) + } else { + None + }, + }) + } else { + None + } + } + + /// Creates an instance from a percent-encoded string + /// that originates from a fragment directive. + /// + /// `text_fragment` is supposed to have this format: + /// ``` + /// text=[prefix-,]start[,end][,-suffix] + /// ``` + /// This function returns `None` if `text_fragment` + /// does not start with `text=`, it contains 0 or more + /// than 4 elements or prefix/suffix/start or end + /// occur too many times. + /// It also returns `None` if any of the tokens parses to fail. + pub fn from_percent_encoded_string(text_directive: &str) -> Option<Self> { + // first check if the string starts with `text=` + if text_directive.len() < 6 { + return None; + } + if !text_directive.starts_with("text=") { + return None; + } + + let mut parsed_text_directive = Self::default(); + let valid = text_directive[5..] + .split(",") + // Parse the substrings into `TextDirectiveParameter`s. This will determine + // for each substring if it is a Prefix, Suffix or Start/End, + // or if it is invalid. + .map(|token| TextDirectiveParameter::from_percent_encoded(token.as_bytes())) + // populate `parsed_text_directive` and check its validity by inserting the parameters + // one by one. Given that the parameters are sorted by their position in the source, + // the validity of the text directive can be determined while adding the parameters. + .map(|token| match token { + Some(TextDirectiveParameter::Prefix(..)) => { + if !parsed_text_directive.is_empty() { + // `prefix-` must be the first result. + return false; + } + parsed_text_directive.prefix = token; + return true; + } + Some(TextDirectiveParameter::StartOrEnd(..)) => { + if parsed_text_directive.suffix.is_some() { + // start or end must come before `-suffix`. + return false; + } + if parsed_text_directive.start.is_none() { + parsed_text_directive.start = token; + return true; + } + if parsed_text_directive.end.is_none() { + parsed_text_directive.end = token; + return true; + } + // if `start` and `end` is already filled, + // this is invalid as well. + return false; + } + Some(TextDirectiveParameter::Suffix(..)) => { + if parsed_text_directive.start.is_some() + && parsed_text_directive.suffix.is_none() + { + // `start` must be present and `-suffix` must not be present. + // `end` may be present. + parsed_text_directive.suffix = token; + return true; + } + return false; + } + // empty or invalid token renders the whole text directive invalid. + None => false, + }) + .all(|valid| valid); + if valid { + return Some(parsed_text_directive); + } + None + } + + /// Creates a percent-encoded string for the current `TextDirective`. + /// In the unlikely case that the `TextDirective` is invalid (i.e. `start` is None), + /// which should have been caught earlier,this method returns an empty string. + pub fn to_percent_encoded_string(&self) -> String { + if !self.is_valid() { + return String::default(); + } + String::from("text=") + + &[&self.prefix, &self.start, &self.end, &self.suffix] + .iter() + .filter_map(|&token| token.as_ref()) + .map(|token| token.to_percent_encoded_string()) + .collect::<Vec<_>>() + .join(",") + } + + pub fn start(&self) -> &Option<TextDirectiveParameter> { + &self.start + } + + pub fn end(&self) -> &Option<TextDirectiveParameter> { + &self.end + } + + pub fn prefix(&self) -> &Option<TextDirectiveParameter> { + &self.prefix + } + + pub fn suffix(&self) -> &Option<TextDirectiveParameter> { + &self.suffix + } + + fn is_empty(&self) -> bool { + self.prefix.is_none() && self.start.is_none() && self.end.is_none() && self.suffix.is_none() + } + + /// A `TextDirective` object is valid if it contains the `start` token. + /// All other tokens are optional. + fn is_valid(&self) -> bool { + self.start.is_some() + } +} +/// Parses a fragment directive into a list of `TextDirective` objects and removes +/// the fragment directive from the input url. +/// +/// If the hash does not contain a fragment directive, `url` is not modified +/// and this function returns `None`. +/// Otherwise, the fragment directive is removed from `url` and parsed. +/// If parsing fails, this function returns `None`. +pub fn parse_fragment_directive_and_remove_it_from_hash( + url: &str, +) -> Option<(&str, &str, Vec<TextDirective>)> { + // The Fragment Directive is preceded by a `:~:`, + // which is only allowed to appear in the hash once. + // However (even if unlikely), it might appear outside of the hash, + // so this code only considers it when it is after the #. + let maybe_first_hash_pos = url.find("#"); + // If there is no # in url, it is considered to be only the hash (and not a full url). + let first_hash_pos = maybe_first_hash_pos.unwrap_or_default(); + let mut fragment_directive_iter = url[first_hash_pos..].split(":~:"); + let url_with_stripped_fragment_directive = + &url[..first_hash_pos + fragment_directive_iter.next().unwrap_or_default().len()]; + + if let Some(fragment_directive) = fragment_directive_iter.next() { + if fragment_directive_iter.next().is_some() { + // There are multiple occurrences of `:~:`, which is not allowed. + return None; + } + // - fragments are separated by `&`. + // - if a fragment does not start with `text=`, it is not a text fragment and will be ignored. + // - if parsing of the text fragment fails (for whatever reason), it will be ignored. + let text_directives: Vec<_> = fragment_directive + .split("&") + .map(|maybe_text_fragment| { + TextDirective::from_percent_encoded_string(&maybe_text_fragment) + }) + .filter_map(|maybe_text_directive| maybe_text_directive) + .collect(); + if !text_directives.is_empty() { + return Some(( + url_with_stripped_fragment_directive + .strip_suffix("#") + .unwrap_or(url_with_stripped_fragment_directive), + fragment_directive, + text_directives, + )); + } + } + None +} + +/// Creates a percent-encoded text fragment string. +/// +/// The returned string starts with `:~:`, so that it can be appended +/// to a normal fragment. +/// Text directives which are not valid (ie., they are missing the `start` parameter), +/// are skipped. +/// +/// Returns `None` if `fragment_directives` is empty. +pub fn create_fragment_directive_string(text_directives: &Vec<TextDirective>) -> Option<String> { + if text_directives.is_empty() { + return None; + } + let encoded_fragment_directives: Vec<_> = text_directives + .iter() + .filter(|&fragment_directive| fragment_directive.is_valid()) + .map(|fragment_directive| fragment_directive.to_percent_encoded_string()) + .filter(|text_directive| !text_directive.is_empty()) + .collect(); + if encoded_fragment_directives.is_empty() { + return None; + } + Some(String::from(":~:") + &encoded_fragment_directives.join("&")) +} + +/// Creates the percent-encoded text directive string for a single text directive. +pub fn create_text_directive_string(text_directive: &TextDirective) -> Option<String> { + if text_directive.is_valid() { + Some(text_directive.to_percent_encoded_string()) + } else { + None + } +} diff --git a/dom/base/fragmentdirectives/lib.rs b/dom/base/fragmentdirectives/lib.rs new file mode 100644 index 0000000000..0003849eb7 --- /dev/null +++ b/dom/base/fragmentdirectives/lib.rs @@ -0,0 +1,158 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use nsstring::{nsCString, nsString}; +use thin_vec::ThinVec; +pub mod fragment_directive_impl; +mod test; + +/// This struct contains the percent-decoded parts of a text directive. +/// All parts besides `start` are optional (which is indicated by an empty string). +/// +/// This struct uses Gecko String types, whereas the parser internally uses Rust types. +/// Therefore, conversion functions are provided. +#[repr(C)] +pub struct TextDirective { + prefix: nsString, + start: nsString, + end: nsString, + suffix: nsString, +} + +impl TextDirective { + /// Creates a `FragmentDirectiveElement` object from a `FragmentDirectiveElementInternal` object + /// (which uses Rust string types). + fn from_rust_type(element: &fragment_directive_impl::TextDirective) -> Self { + Self { + prefix: element + .prefix() + .as_ref() + .map_or_else(nsString::new, |token| nsString::from(token.value())), + start: element + .start() + .as_ref() + .map_or_else(nsString::new, |token| nsString::from(token.value())), + end: element + .end() + .as_ref() + .map_or_else(nsString::new, |token| nsString::from(token.value())), + suffix: element + .suffix() + .as_ref() + .map_or_else(nsString::new, |token| nsString::from(token.value())), + } + } + + /// Converts the contents of this object into Rust types. + /// Returns `None` if the given fragment is not valid. + /// The only invalid condition is a fragment that is missing the `start` token. + fn to_rust_type(&self) -> Option<fragment_directive_impl::TextDirective> { + fragment_directive_impl::TextDirective::from_parts( + self.prefix.to_string(), + self.start.to_string(), + self.end.to_string(), + self.suffix.to_string(), + ) + } +} + +/// Result of the `parse_fragment_directive()` function. +/// +/// The result contains the original given URL without the fragment directive, +/// a unsanitized string version of the extracted fragment directive, +/// and an array of the parsed text directives. +#[repr(C)] +pub struct ParsedFragmentDirectiveResult { + url_without_fragment_directive: nsCString, + fragment_directive: nsCString, + text_directives: ThinVec<TextDirective>, +} + +/// Parses the fragment directive from a given URL. +/// +/// This function writes the result data into `result`. +/// The result consists of +/// - the input url without the fragment directive, +/// - the fragment directive as unparsed string, +/// - a list of the parsed and percent-decoded text directives. +/// +/// Directives which are unknown will be ignored. +/// If new directive types are added in the future, they should also be considered here. +/// This function returns false if no fragment directive is found, or it could not be parsed. +#[no_mangle] +pub extern "C" fn parse_fragment_directive( + url: &nsCString, + result: &mut ParsedFragmentDirectiveResult, +) -> bool { + // sanitize inputs + result.url_without_fragment_directive = nsCString::new(); + result.fragment_directive = nsCString::new(); + result.text_directives.clear(); + + let url_as_rust_string = url.to_utf8(); + if let Some((stripped_url, fragment_directive, text_directives)) = + fragment_directive_impl::parse_fragment_directive_and_remove_it_from_hash( + &url_as_rust_string, + ) + { + result + .url_without_fragment_directive + .assign(&stripped_url); + result.fragment_directive.assign(&fragment_directive); + result.text_directives.extend( + text_directives + .iter() + .map(|text_directive| TextDirective::from_rust_type(text_directive)), + ); + return true; + } + false +} + +/// Creates a percent-encoded fragment directive string from a given list of `FragmentDirectiveElement`s. +/// +/// The returned string has this form: +/// `:~:text=[prefix1-,]start1[,end1][,-suffix1]&text=[prefix2-,]start2[,end2][,-suffix2]` +/// +/// Invalid `FragmentDirectiveElement`s are ignored, where "invalid" means that no `start` token is provided. +/// If there are no valid `FragmentDirectiveElement`s, an empty string is returned. +#[no_mangle] +pub extern "C" fn create_fragment_directive( + text_directives: &ThinVec<TextDirective>, + fragment_directive: &mut nsCString, +) -> bool { + let directives_rust = Vec::from_iter( + text_directives + .iter() + .filter_map(|fragment| fragment.to_rust_type()), + ); + if let Some(fragment_directive_rust) = + fragment_directive_impl::create_fragment_directive_string(&directives_rust) + { + fragment_directive.assign(&fragment_directive_rust); + return true; + } + + false +} + +/// Creates a percent-encoded text directive string for a single text directive. +/// The returned string has the form `text=[prefix-,]start[,end][,-suffix]`. +/// If the provided `TextDirective` is invalid (i.e. it has no `start` attribute), +/// the outparam `directive_string` is empty and the function returns false. +#[no_mangle] +pub extern "C" fn create_text_directive( + text_directive: &TextDirective, + directive_string: &mut nsCString, +) -> bool { + if let Some(text_directive_rust) = text_directive.to_rust_type() { + if let Some(text_directive_string_rust) = + fragment_directive_impl::create_text_directive_string(&text_directive_rust) + { + directive_string.assign(&text_directive_string_rust); + return true; + } + } + false +} diff --git a/dom/base/fragmentdirectives/test.rs b/dom/base/fragmentdirectives/test.rs new file mode 100644 index 0000000000..d4509cb033 --- /dev/null +++ b/dom/base/fragmentdirectives/test.rs @@ -0,0 +1,599 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#[cfg(test)] +mod test { + use crate::fragment_directive_impl::{ + create_fragment_directive_string, parse_fragment_directive_and_remove_it_from_hash, + TextDirective, + }; + + /// This test verifies that valid combinations of [prefix-,]start[,end][,-suffix] are parsed correctly. + #[test] + fn test_parse_fragment_directive_with_one_text_directive() { + let test_cases = vec![ + ("#:~:text=start", (None, Some("start"), None, None)), + ( + "#:~:text=start,end", + (None, Some("start"), Some("end"), None), + ), + ( + "#:~:text=prefix-,start", + (Some("prefix"), Some("start"), None, None), + ), + ( + "#:~:text=prefix-,start,end", + (Some("prefix"), Some("start"), Some("end"), None), + ), + ( + "#:~:text=prefix-,start,end,-suffix", + (Some("prefix"), Some("start"), Some("end"), Some("suffix")), + ), + ( + "#:~:text=start,-suffix", + (None, Some("start"), None, Some("suffix")), + ), + ( + "#:~:text=start,end,-suffix", + (None, Some("start"), Some("end"), Some("suffix")), + ), + ("#:~:text=text=", (None, Some("text="), None, None)), + ]; + for (url, (prefix, start, end, suffix)) in test_cases { + let (stripped_url, fragment_directive, result) = + parse_fragment_directive_and_remove_it_from_hash(&url) + .expect("The parser must find a result."); + assert_eq!( + fragment_directive, + &url[4..], + "The extracted fragment directive string + should be unsanitized and therefore match the input string." + ); + assert_eq!(result.len(), 1, "There must be one parsed text fragment."); + assert_eq!( + stripped_url, "", + "The fragment directive must be removed from the url hash." + ); + let text_directive = result.first().unwrap(); + if prefix.is_none() { + assert!( + text_directive.prefix().is_none(), + "There must be no `prefix` token (test case `{}`).", + url + ); + } else { + assert!( + text_directive + .prefix() + .as_ref() + .expect("There must be a `prefix` token.") + .value() + == prefix.unwrap(), + "Wrong value for `prefix` (test case `{}`).", + url + ); + } + if start.is_none() { + assert!( + text_directive.start().is_none(), + "There must be no `start` token (test case `{}`).", + url + ); + } else { + assert!( + text_directive + .start() + .as_ref() + .expect("There must be a `start` token.") + .value() + == start.unwrap(), + "Wrong value for `start` (test case `{}`).", + url + ); + } + if end.is_none() { + assert!( + text_directive.end().is_none(), + "There must be no `end` token (test case `{}`).", + url + ); + } else { + assert!( + text_directive + .end() + .as_ref() + .expect("There must be a `end` token.") + .value() + == end.unwrap(), + "Wrong value for `end` (test case `{}`).", + url + ); + } + if suffix.is_none() { + assert!( + text_directive.suffix().is_none(), + "There must be no `suffix` token (test case `{}`).", + url + ); + } else { + assert!( + text_directive + .suffix() + .as_ref() + .expect("There must be a `suffix` token.") + .value() + == suffix.unwrap(), + "Wrong value for `suffix` (test case `{}`).", + url + ); + } + } + } + + #[test] + fn test_parse_full_url() { + for (url, stripped_url_ref) in [ + ("https://example.com#:~:text=foo", "https://example.com"), + ( + "https://example.com/some/page.html?query=answer#:~:text=foo", + "https://example.com/some/page.html?query=answer", + ), + ( + "https://example.com/some/page.html?query=answer#fragment:~:text=foo", + "https://example.com/some/page.html?query=answer#fragment", + ), + ( + "http://example.com/page.html?query=irrelevant:~:#bar:~:text=foo", + "http://example.com/page.html?query=irrelevant:~:#bar" + ) + ] { + let (stripped_url, fragment_directive, _) = + parse_fragment_directive_and_remove_it_from_hash(&url) + .expect("The parser must find a result"); + assert_eq!(stripped_url, stripped_url_ref, "The stripped url is not correct."); + assert_eq!(fragment_directive, "text=foo"); + } + } + + /// This test verifies that a text fragment is parsed correctly if it is preceded + /// or followed by a fragment (i.e. `#foo:~:text=bar`). + #[test] + fn test_parse_text_fragment_after_fragments() { + let url = "#foo:~:text=start"; + let (stripped_url, fragment_directive, result) = + parse_fragment_directive_and_remove_it_from_hash(&url) + .expect("The parser must find a result."); + assert_eq!( + result.len(), + 1, + "There must be exactly one parsed text fragment." + ); + assert_eq!( + stripped_url, "#foo", + "The fragment directive was not removed correctly." + ); + assert_eq!( + fragment_directive, "text=start", + "The fragment directive was not extracted correctly." + ); + let fragment = result.first().unwrap(); + assert!(fragment.prefix().is_none(), "There is no `prefix` token."); + assert_eq!( + fragment + .start() + .as_ref() + .expect("There must be a `start` token.") + .value(), + "start" + ); + assert!(fragment.end().is_none(), "There is no `end` token."); + assert!(fragment.suffix().is_none(), "There is no `suffix` token."); + } + + /// Ensure that multiple text fragments are parsed correctly. + #[test] + fn test_parse_multiple_text_fragments() { + let url = "#:~:text=prefix-,start,-suffix&text=foo&text=bar,-suffix"; + let (_, _, text_directives) = + parse_fragment_directive_and_remove_it_from_hash(&url) + .expect("The parser must find a result."); + assert_eq!( + text_directives.len(), + 3, + "There must be exactly two parsed text fragments." + ); + let first_text_directive = &text_directives[0]; + assert_eq!( + first_text_directive + .prefix() + .as_ref() + .expect("There must be a `prefix` token.") + .value(), + "prefix" + ); + assert_eq!( + first_text_directive + .start() + .as_ref() + .expect("There must be a `start` token.") + .value(), + "start" + ); + assert!( + first_text_directive.end().is_none(), + "There is no `end` token." + ); + assert_eq!( + first_text_directive + .suffix() + .as_ref() + .expect("There must be a `suffix` token.") + .value(), + "suffix" + ); + + let second_text_directive = &text_directives[1]; + assert!( + second_text_directive.prefix().is_none(), + "There is no `prefix` token." + ); + assert_eq!( + second_text_directive + .start() + .as_ref() + .expect("There must be a `start` token.") + .value(), + "foo" + ); + assert!( + second_text_directive.end().is_none(), + "There is no `end` token." + ); + assert!( + second_text_directive.suffix().is_none(), + "There is no `suffix` token." + ); + let third_text_directive = &text_directives[2]; + assert!( + third_text_directive.prefix().is_none(), + "There is no `prefix` token." + ); + assert_eq!( + third_text_directive + .start() + .as_ref() + .expect("There must be a `start` token.") + .value(), + "bar" + ); + assert!( + third_text_directive.end().is_none(), + "There is no `end` token." + ); + assert_eq!( + third_text_directive + .suffix() + .as_ref() + .expect("There must be a `suffix` token.") + .value(), + "suffix" + ); + } + + /// Multiple text directives should be parsed correctly + /// if they are surrounded or separated by unknown directives. + #[test] + fn test_parse_multiple_text_directives_with_unknown_directive_in_between() { + for url in [ + "#:~:foo&text=start1&text=start2", + "#:~:text=start1&foo&text=start2", + "#:~:text=start1&text=start2&foo", + ] { + let (_, fragment_directive, text_directives) = + parse_fragment_directive_and_remove_it_from_hash(&url) + .expect("The parser must find a result."); + assert_eq!( + fragment_directive, + &url[4..], + "The extracted fragment directive string is unsanitized + and should contain the unknown directive." + ); + assert_eq!( + text_directives.len(), + 2, + "There must be exactly two parsed text fragments." + ); + let first_text_directive = &text_directives[0]; + assert_eq!( + first_text_directive + .start() + .as_ref() + .expect("There must be a `start` token.") + .value(), + "start1" + ); + let second_text_directive = &text_directives[1]; + assert_eq!( + second_text_directive + .start() + .as_ref() + .expect("There must be a `start` token.") + .value(), + "start2" + ); + } + } + + /// Ensures that input that doesn't contain a text fragment does not produce a result. + /// This includes the use of partial identifying tokens necessary for a text fragment + /// (e.g. `:~:` without `text=`, `text=foo` without the `:~:` or multiple occurrences of `:~:`) + /// In these cases, the parser must return `None` to indicate that there are no valid text fragments. + #[test] + fn test_parse_invalid_or_unknown_fragment_directive() { + for url in [ + "#foo", + "#foo:", + "#foo:~:", + "#foo:~:bar", + "text=prefix-,start", + "#:~:text=foo-,bar,-baz:~:text=foo", + ] { + let text_directives = + parse_fragment_directive_and_remove_it_from_hash(&url); + assert!( + text_directives.is_none(), + "The fragment `{}` does not contain a valid or known fragment directive.", + url + ); + } + } + + /// Ensures that ill-formed text directives (but valid fragment directives) + /// (starting correctly with `:~:text=`) are not parsed. + /// Instead `None` must be returned. + /// Test cases include invalid combinations of `prefix`/`suffix`es, + /// additional `,`s, too many `start`/`end` tokens, or empty text fragments. + #[test] + fn test_parse_invalid_text_fragments() { + for url in [ + "#:~:text=start,start,start", + "#:~:text=prefix-,prefix-", + "#:~:text=prefix-,-suffix", + "#:~:text=prefix-,start,start,start", + "#:~:text=prefix-,start,start,start,-suffix", + "#:~:text=start,start,start,-suffix", + "#:~:text=prefix-,start,end,-suffix,foo", + "#:~:text=foo,prefix-,start", + "#:~:text=prefix-,,start,", + "#:~:text=,prefix,start", + "#:~:text=", + ] { + let text_directives = + parse_fragment_directive_and_remove_it_from_hash(&url); + assert!( + text_directives.is_none(), + "The fragment directive `{}` does not contain a valid text directive.", + url + ); + } + } + + /// Ensure that out of multiple text fragments only the invalid ones are ignored + /// while valid text fragments are still returned. + /// Since correct parsing of multiple text fragments as well as + /// several forms of invalid text fragments are already tested in + /// `test_parse_multiple_text_fragments` and `test_parse_invalid_text_fragments()`, + /// it should be enough to test this with only one fragment directive + /// that contains two text fragments, one of them being invalid. + #[test] + fn test_valid_and_invalid_text_directives() { + for url in [ + "#:~:text=start&text=,foo,", + "#:~:text=foo,foo,foo&text=start", + ] { + let (_, fragment_directive, text_directives) = + parse_fragment_directive_and_remove_it_from_hash(&url) + .expect("The parser must find a result."); + assert_eq!( + fragment_directive, + &url[4..], + "The extracted fragment directive string is unsanitized + and should contain invalid text directives." + ); + assert_eq!( + text_directives.len(), + 1, + "There must be exactly one parsed text fragment." + ); + let text_directive = text_directives.first().unwrap(); + assert_eq!( + text_directive + .start() + .as_ref() + .expect("There must be a `start` value.") + .value(), + "start", + "The `start` value of the text directive has the wrong value." + ); + } + } + + /// Ensures that a fragment directive that contains percent-encoded characters + /// is decoded correctly. This explicitly includes characters which are used + /// for identifying text fragments, i.e. `#`, `, `, `&`, `:`, `~` and `-`. + #[test] + fn test_parse_percent_encoding_tokens() { + let url = "#:~:text=prefix%26-,start%20and%2C,end%23,-%26suffix%2D"; + let (_, fragment_directive, text_directives) = + parse_fragment_directive_and_remove_it_from_hash(&url) + .expect("The parser must find a result."); + assert_eq!( + fragment_directive, + &url[4..], + "The extracted fragment directive string is unsanitized + and should contain the original and percent-decoded string." + ); + let text_directive = text_directives.first().unwrap(); + assert_eq!( + text_directive + .prefix() + .as_ref() + .expect("There must be a prefix.") + .value(), + "prefix&", + "" + ); + assert_eq!( + text_directive + .start() + .as_ref() + .expect("There must be a prefix.") + .value(), + "start and,", + "" + ); + assert_eq!( + text_directive + .end() + .as_ref() + .expect("There must be a prefix.") + .value(), + "end#", + "" + ); + assert_eq!( + text_directive + .suffix() + .as_ref() + .expect("There must be a prefix.") + .value(), + "&suffix-", + "" + ); + } + + /// Ensures that a text fragment is created correctly, + /// based on a given combination of tokens. + /// This includes all sorts of combinations of + /// `prefix`, `suffix`, `start` and `end`, + /// als well as values for these tokens which contain + /// characters that need to be encoded because they are + /// identifiers for text fragments + /// (#`, `, `, `&`, `:`, `~` and `-`). + #[test] + fn test_create_fragment_directive() { + for (text_directive, expected_fragment_directive) in [ + ( + TextDirective::from_parts( + String::new(), + String::from("start"), + String::new(), + String::new(), + ) + .unwrap(), + ":~:text=start", + ), + ( + TextDirective::from_parts( + String::new(), + String::from("start"), + String::from("end"), + String::new(), + ) + .unwrap(), + ":~:text=start,end", + ), + ( + TextDirective::from_parts( + String::from("prefix"), + String::from("start"), + String::from("end"), + String::new(), + ) + .unwrap(), + ":~:text=prefix-,start,end", + ), + ( + TextDirective::from_parts( + String::from("prefix"), + String::from("start"), + String::from("end"), + String::from("suffix"), + ) + .unwrap(), + ":~:text=prefix-,start,end,-suffix", + ), + ( + TextDirective::from_parts( + String::new(), + String::from("start"), + String::from("end"), + String::from("suffix"), + ) + .unwrap(), + ":~:text=start,end,-suffix", + ), + ( + TextDirective::from_parts( + String::from("prefix"), + String::from("start"), + String::new(), + String::from("suffix"), + ) + .unwrap(), + ":~:text=prefix-,start,-suffix", + ), + ( + TextDirective::from_parts( + String::from("prefix-"), + String::from("start and,"), + String::from("&end"), + String::from("#:~:suffix"), + ) + .unwrap(), + ":~:text=prefix%2D-,start%20and%2C,%26end,-%23%3A%7E%3Asuffix", + ), + ] { + let fragment_directive = create_fragment_directive_string(&vec![text_directive]) + .expect("The given input must produce a valid fragment directive."); + assert_eq!(fragment_directive, expected_fragment_directive); + } + } + + /// Ensures that a fragment directive is created correctly if multiple text fragments are given. + /// The resulting fragment must start with `:~:` + /// and each text fragment must be separated using `&text=`. + #[test] + fn test_create_fragment_directive_from_multiple_text_directives() { + let text_directives = vec![ + TextDirective::from_parts( + String::new(), + String::from("start1"), + String::new(), + String::new(), + ) + .unwrap(), + TextDirective::from_parts( + String::new(), + String::from("start2"), + String::new(), + String::new(), + ) + .unwrap(), + TextDirective::from_parts( + String::new(), + String::from("start3"), + String::new(), + String::new(), + ) + .unwrap(), + ]; + let fragment_directive = create_fragment_directive_string(&text_directives) + .expect("The given input must produce a valid fragment directive."); + assert_eq!( + fragment_directive, ":~:text=start1&text=start2&text=start3", + "The created fragment directive is wrong for multiple fragments." + ); + } +} diff --git a/dom/base/moz.build b/dom/base/moz.build index ef1780f161..ffcfb0aaf6 100644 --- a/dom/base/moz.build +++ b/dom/base/moz.build @@ -194,6 +194,7 @@ EXPORTS.mozilla.dom += [ "External.h", "FilteredNodeIterator.h", "FormData.h", + "FragmentDirective.h", "FragmentOrElement.h", "FromParser.h", "GeneratedImageContent.h", @@ -237,6 +238,7 @@ EXPORTS.mozilla.dom += [ "PlacesBookmarkTitle.h", "PlacesBookmarkUrl.h", "PlacesEvent.h", + "PlacesEventCounts.h", "PlacesFavicon.h", "PlacesHistoryCleared.h", "PlacesObservers.h", @@ -296,6 +298,7 @@ if CONFIG["FUZZING"]: if CONFIG["COMPILE_ENVIRONMENT"]: EXPORTS.mozilla.dom += [ + "!fragmentdirectives_ffi_generated.h", "!GeneratedElementDocumentState.h", "RustTypes.h", ] @@ -305,6 +308,11 @@ if CONFIG["COMPILE_ENVIRONMENT"]: inputs=["rust"], ) + CbindgenHeader( + "fragmentdirectives_ffi_generated.h", + inputs=["fragmentdirectives"], + ) + UNIFIED_SOURCES += [ "!UseCounterMetrics.cpp", "AbstractRange.cpp", @@ -349,6 +357,7 @@ UNIFIED_SOURCES += [ "EventSourceEventService.cpp", "External.cpp", "FormData.cpp", + "FragmentDirective.cpp", "FragmentOrElement.cpp", "GeneratedImageContent.cpp", "GlobalTeardownObserver.cpp", @@ -489,6 +498,7 @@ if CONFIG["FUZZING"]: if CONFIG["MOZ_PLACES"]: UNIFIED_SOURCES += [ "PlacesEvent.cpp", + "PlacesEventCounts.cpp", "PlacesObservers.cpp", "PlacesWeakCallbackWrapper.cpp", ] diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index c6f1687f73..d2c863bd65 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -1767,6 +1767,17 @@ bool nsContentUtils::IsAlphanumericOrSymbol(uint32_t aChar) { cat == nsUGenCategory::kSymbol; } +// static +bool nsContentUtils::IsHyphen(uint32_t aChar) { + // Characters treated as hyphens for the purpose of "emergency" breaking + // when the content would otherwise overflow. + return aChar == uint32_t('-') || // HYPHEN-MINUS + aChar == 0x2010 || // HYPHEN + aChar == 0x2012 || // FIGURE DASH + aChar == 0x2013 || // EN DASH + aChar == 0x058A; // ARMENIAN HYPHEN +} + /* static */ bool nsContentUtils::IsHTMLWhitespace(char16_t aChar) { return aChar == char16_t(0x0009) || aChar == char16_t(0x000A) || @@ -3008,6 +3019,15 @@ nsIContent* nsContentUtils::GetCommonFlattenedTreeAncestorHelper( } /* static */ +nsIContent* nsContentUtils::GetCommonFlattenedTreeAncestorForSelection( + nsIContent* aContent1, nsIContent* aContent2) { + return GetCommonAncestorInternal( + aContent1, aContent2, [](nsIContent* aContent) { + return aContent->GetFlattenedTreeParentNodeForSelection(); + }); +} + +/* static */ Element* nsContentUtils::GetCommonFlattenedTreeAncestorForStyle( Element* aElement1, Element* aElement2) { return GetCommonAncestorInternal(aElement1, aElement2, [](Element* aElement) { @@ -11341,7 +11361,7 @@ int32_t nsContentUtils::CompareTreePosition(const nsINode* aNode1, MOZ_ASSERT(aNode1, "aNode1 must not be null"); MOZ_ASSERT(aNode2, "aNode2 must not be null"); - if (MOZ_UNLIKELY(NS_WARN_IF(aNode1 == aNode2))) { + if (NS_WARN_IF(aNode1 == aNode2)) { return 0; } @@ -11439,7 +11459,8 @@ nsIContent* nsContentUtils::AttachDeclarativeShadowRoot(nsIContent* aHost, bool aIsClonable, bool aDelegatesFocus) { RefPtr<Element> host = mozilla::dom::Element::FromNodeOrNull(aHost); - if (!host) { + if (!host || host->GetShadowRoot()) { + // https://html.spec.whatwg.org/#parsing-main-inhead:shadow-host return nullptr; } @@ -11449,9 +11470,10 @@ nsIContent* nsContentUtils::AttachDeclarativeShadowRoot(nsIContent* aHost, init.mSlotAssignment = SlotAssignmentMode::Named; init.mClonable = aIsClonable; - RefPtr shadowRoot = host->AttachShadow(init, IgnoreErrors(), - Element::ShadowRootDeclarative::Yes); + RefPtr shadowRoot = host->AttachShadow(init, IgnoreErrors()); if (shadowRoot) { + shadowRoot->SetIsDeclarative( + nsGenericHTMLFormControlElement::ShadowRootDeclarative::Yes); // https://html.spec.whatwg.org/#parsing-main-inhead:available-to-element-internals shadowRoot->SetAvailableToElementInternals(); } diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h index 338fc097de..4291d2c5d1 100644 --- a/dom/base/nsContentUtils.h +++ b/dom/base/nsContentUtils.h @@ -225,7 +225,6 @@ enum EventNameType { EventNameType_SVGSVG = 0x0008, // the svg element EventNameType_SMIL = 0x0010, // smil elements EventNameType_HTMLBodyOrFramesetOnly = 0x0020, - EventNameType_HTMLMarqueeOnly = 0x0040, EventNameType_HTMLXUL = 0x0003, EventNameType_All = 0xFFFF @@ -516,6 +515,13 @@ class nsContentUtils { } /** + * Returns the common flattened tree ancestor from the point of view of + * the selection system, if any, for two given content nodes. + */ + static nsIContent* GetCommonFlattenedTreeAncestorForSelection( + nsIContent* aContent1, nsIContent* aContent2); + + /** * Returns the common flattened tree ancestor from the point of view of the * style system, if any, for two given content nodes. */ @@ -781,6 +787,10 @@ class nsContentUtils { * Returns true if aChar is of class L*, N* or S* (for first-letter). */ static bool IsAlphanumericOrSymbol(uint32_t aChar); + /** + * Returns true if aChar is a kind of hyphen. + */ + static bool IsHyphen(uint32_t aChar); /* * Is the character an HTML whitespace character? diff --git a/dom/base/nsFocusManager.cpp b/dom/base/nsFocusManager.cpp index 5a4cf78d65..4e2d604693 100644 --- a/dom/base/nsFocusManager.cpp +++ b/dom/base/nsFocusManager.cpp @@ -846,24 +846,27 @@ nsresult nsFocusManager::ContentRemoved(Document* aDocument, NS_ENSURE_ARG(aDocument); NS_ENSURE_ARG(aContent); - RefPtr<nsPIDOMWindowOuter> window = aDocument->GetWindow(); - if (!window) { + nsPIDOMWindowOuter* windowPtr = aDocument->GetWindow(); + if (!windowPtr) { return NS_OK; } // if the content is currently focused in the window, or is an // shadow-including inclusive ancestor of the currently focused element, // reset the focus within that window. - RefPtr<Element> previousFocusedElement = window->GetFocusedElement(); - if (!previousFocusedElement) { + Element* previousFocusedElementPtr = windowPtr->GetFocusedElement(); + if (!previousFocusedElementPtr) { return NS_OK; } if (!nsContentUtils::ContentIsHostIncludingDescendantOf( - previousFocusedElement, aContent)) { + previousFocusedElementPtr, aContent)) { return NS_OK; } + RefPtr<nsPIDOMWindowOuter> window = windowPtr; + RefPtr<Element> previousFocusedElement = previousFocusedElementPtr; + RefPtr<Element> newFocusedElement = [&]() -> Element* { if (auto* sr = ShadowRoot::FromNode(aContent)) { if (sr->IsUAWidget() && sr->Host()->IsHTMLElement(nsGkAtoms::input)) { @@ -1802,7 +1805,8 @@ Maybe<uint64_t> nsFocusManager::SetFocusInner(Element* aNewContent, // focus, update the node in the window, and raise the window if desired. if (allowFrameSwitch) { AdjustWindowFocus(newBrowsingContext, true, IsWindowVisible(newWindow), - actionId); + actionId, false /* aShouldClearAncestorFocus */, + nullptr /* aAncestorBrowsingContextToFocus */); } // set the focus node and method as needed @@ -1975,7 +1979,9 @@ mozilla::dom::BrowsingContext* nsFocusManager::GetCommonAncestor( bool nsFocusManager::AdjustInProcessWindowFocus( BrowsingContext* aBrowsingContext, bool aCheckPermission, bool aIsVisible, - uint64_t aActionId) { + uint64_t aActionId, bool aShouldClearAncestorFocus, + BrowsingContext* aAncestorBrowsingContextToFocus) { + MOZ_ASSERT_IF(aAncestorBrowsingContextToFocus, aShouldClearAncestorFocus); if (ActionIdComparableAndLower(aActionId, mActionIdForFocusedBrowsingContextInContent)) { LOGFOCUS( @@ -2026,6 +2032,17 @@ bool nsFocusManager::AdjustInProcessWindowFocus( break; } + if (aShouldClearAncestorFocus) { + // This is the BrowsingContext that receives the focus, no need to clear + // its focused element and the rest of the ancestors. + if (window->GetBrowsingContext() == aAncestorBrowsingContextToFocus) { + break; + } + + window->SetFocusedElement(nullptr); + continue; + } + if (frameElement != window->GetFocusedElement()) { window->SetFocusedElement(frameElement); @@ -2041,18 +2058,22 @@ bool nsFocusManager::AdjustInProcessWindowFocus( return needToNotifyOtherProcess; } -void nsFocusManager::AdjustWindowFocus(BrowsingContext* aBrowsingContext, - bool aCheckPermission, bool aIsVisible, - uint64_t aActionId) { +void nsFocusManager::AdjustWindowFocus( + BrowsingContext* aBrowsingContext, bool aCheckPermission, bool aIsVisible, + uint64_t aActionId, bool aShouldClearAncestorFocus, + BrowsingContext* aAncestorBrowsingContextToFocus) { + MOZ_ASSERT_IF(aAncestorBrowsingContextToFocus, aShouldClearAncestorFocus); if (AdjustInProcessWindowFocus(aBrowsingContext, aCheckPermission, aIsVisible, - aActionId)) { + aActionId, aShouldClearAncestorFocus, + aAncestorBrowsingContextToFocus)) { // Some ancestors of aBrowsingContext isn't in this process, so notify other // processes to adjust their focused element. mozilla::dom::ContentChild* contentChild = mozilla::dom::ContentChild::GetSingleton(); MOZ_ASSERT(contentChild); - contentChild->SendAdjustWindowFocus(aBrowsingContext, aIsVisible, - aActionId); + contentChild->SendAdjustWindowFocus(aBrowsingContext, aIsVisible, aActionId, + aShouldClearAncestorFocus, + aAncestorBrowsingContextToFocus); } } @@ -2423,6 +2444,22 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear, if (ancestorWindowToFocus) { ancestorWindowToFocus->SetFocusedElement(nullptr, 0, true); } + + // When the focus of aBrowsingContextToClear is cleared, it should + // also clear its ancestors's focus because ancestors should no longer + // be considered aBrowsingContextToClear is focused. + // + // We don't need to do this when aBrowsingContextToClear and + // aAncestorBrowsingContextToFocus is equal because ancestors don't + // care about this. + if (aBrowsingContextToClear && + aBrowsingContextToClear != aAncestorBrowsingContextToFocus) { + AdjustWindowFocus( + aBrowsingContextToClear, false, + IsWindowVisible(aBrowsingContextToClear->GetDOMWindow()), aActionId, + true /* aShouldClearAncestorFocus */, + aAncestorBrowsingContextToFocus); + } } SetFocusedWindowInternal(nullptr, aActionId); @@ -2569,7 +2606,9 @@ void nsFocusManager::Focus( // focus can be traversed from the top level down to the newly focused // window. RefPtr<BrowsingContext> bc = aWindow->GetBrowsingContext(); - AdjustWindowFocus(bc, false, IsWindowVisible(aWindow), aActionId); + AdjustWindowFocus(bc, false, IsWindowVisible(aWindow), aActionId, + false /* aShouldClearAncestorFocus */, + nullptr /* aAncestorBrowsingContextToFocus */); } // indicate that the window has taken focus. diff --git a/dom/base/nsFocusManager.h b/dom/base/nsFocusManager.h index 4fb9d05e1c..9815ab9b98 100644 --- a/dom/base/nsFocusManager.h +++ b/dom/base/nsFocusManager.h @@ -349,17 +349,21 @@ class nsFocusManager final : public nsIFocusManager, nsPIDOMWindowOuter* aWindow, mozilla::dom::BrowsingContext* aContext); /** - * When aBrowsingContext is focused, adjust the ancestors of aBrowsingContext - * so that they also have their corresponding frames focused. Thus, one can - * start at the active top-level window and navigate down the currently - * focused elements for each frame in the tree to get to aBrowsingContext. + * When aBrowsingContext is focused or blurred, adjust the ancestors of + * aBrowsingContext so that they also have their corresponding frames focused + * or blurred. Thus, one can start at the active top-level window and navigate + * down the currently focused elements for each frame in the tree to get to + * aBrowsingContext. */ MOZ_CAN_RUN_SCRIPT bool AdjustInProcessWindowFocus( mozilla::dom::BrowsingContext* aBrowsingContext, bool aCheckPermission, - bool aIsVisible, uint64_t aActionId); + bool aIsVisible, uint64_t aActionId, bool aShouldClearAncestorFocus, + mozilla::dom::BrowsingContext* aAncestorBrowsingContextToFocus); + MOZ_CAN_RUN_SCRIPT void AdjustWindowFocus( mozilla::dom::BrowsingContext* aBrowsingContext, bool aCheckPermission, - bool aIsVisible, uint64_t aActionId); + bool aIsVisible, uint64_t aActionId, bool aShouldClearAncestorFocus, + mozilla::dom::BrowsingContext* aAncestorBrowsingContextToFocus); /** * Returns true if aWindow is visible. diff --git a/dom/base/nsFrameLoader.cpp b/dom/base/nsFrameLoader.cpp index eca528f258..1e3fb93aa8 100644 --- a/dom/base/nsFrameLoader.cpp +++ b/dom/base/nsFrameLoader.cpp @@ -3388,7 +3388,8 @@ already_AddRefed<Promise> nsFrameLoader::PrintPreview( /* aListener = */ nullptr, docShellToCloneInto, nsGlobalWindowOuter::IsPreview::Yes, nsGlobalWindowOuter::IsForWindowDotPrint::No, - [resolve](const PrintPreviewResultInfo& aInfo) { resolve(aInfo); }, rv); + [resolve](const PrintPreviewResultInfo& aInfo) { resolve(aInfo); }, + nullptr, rv); if (NS_WARN_IF(rv.Failed())) { promise->MaybeReject(std::move(rv)); } diff --git a/dom/base/nsFrameLoaderOwner.cpp b/dom/base/nsFrameLoaderOwner.cpp index 03945975dd..0e67f723d2 100644 --- a/dom/base/nsFrameLoaderOwner.cpp +++ b/dom/base/nsFrameLoaderOwner.cpp @@ -81,13 +81,7 @@ nsFrameLoaderOwner::ShouldPreserveBrowsingContext( } } - // We will preserve our browsing context if either fission is enabled, or the - // `preserve_browsing_contexts` pref is active. - if (UseRemoteSubframes() || - StaticPrefs::fission_preserve_browsing_contexts()) { - return ChangeRemotenessContextType::PRESERVE; - } - return ChangeRemotenessContextType::DONT_PRESERVE; + return ChangeRemotenessContextType::PRESERVE; } void nsFrameLoaderOwner::ChangeRemotenessCommon( diff --git a/dom/base/nsFrameMessageManager.cpp b/dom/base/nsFrameMessageManager.cpp index da275095b1..4c110cc55e 100644 --- a/dom/base/nsFrameMessageManager.cpp +++ b/dom/base/nsFrameMessageManager.cpp @@ -506,7 +506,7 @@ void nsFrameMessageManager::SendSyncMessage(JSContext* aCx, "Should not have parent manager in content!"); AUTO_PROFILER_LABEL_DYNAMIC_LOSSY_NSSTRING( - "nsFrameMessageManager::SendMessage", OTHER, aMessageName); + "nsFrameMessageManager::SendSyncMessage", OTHER, aMessageName); profiler_add_marker("SendSyncMessage", geckoprofiler::category::IPC, {}, FrameMessageMarker{}, aMessageName, true); @@ -1428,8 +1428,7 @@ class SameParentProcessMessageManagerCallback : public MessageManagerCallback { bool aRunInGlobalScope) override { auto* global = ContentProcessMessageManager::Get(); MOZ_ASSERT(!aRunInGlobalScope); - global->LoadScript(aURL); - return true; + return global && global->LoadScript(aURL); } nsresult DoSendAsyncMessage(const nsAString& aMessage, diff --git a/dom/base/nsGlobalWindowInner.cpp b/dom/base/nsGlobalWindowInner.cpp index 5337e1588f..7dcd265ca4 100644 --- a/dom/base/nsGlobalWindowInner.cpp +++ b/dom/base/nsGlobalWindowInner.cpp @@ -3703,7 +3703,7 @@ bool nsGlobalWindowInner::Confirm(const nsAString& aMessage, } already_AddRefed<Promise> nsGlobalWindowInner::Fetch( - const RequestOrUSVString& aInput, const RequestInit& aInit, + const RequestOrUTF8String& aInput, const RequestInit& aInit, CallerType aCallerType, ErrorResult& aRv) { return FetchRequest(this, aInput, aInit, aCallerType, aRv); } @@ -3752,7 +3752,7 @@ Nullable<WindowProxyHolder> nsGlobalWindowInner::PrintPreview( /* aRemotePrintJob = */ nullptr, aListener, aDocShellToCloneInto, nsGlobalWindowOuter::IsPreview::Yes, nsGlobalWindowOuter::IsForWindowDotPrint::No, - /* aPrintPreviewCallback = */ nullptr, aError), + /* aPrintPreviewCallback = */ nullptr, nullptr, aError), aError, nullptr); } diff --git a/dom/base/nsGlobalWindowInner.h b/dom/base/nsGlobalWindowInner.h index 15fbc4259f..215e362dad 100644 --- a/dom/base/nsGlobalWindowInner.h +++ b/dom/base/nsGlobalWindowInner.h @@ -120,7 +120,7 @@ class OwningExternalOrWindowProxy; class Promise; class PostMessageEvent; struct RequestInit; -class RequestOrUSVString; +class RequestOrUTF8String; class SharedWorker; class Selection; struct SizeToContentConstraints; @@ -671,7 +671,7 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget, already_AddRefed<mozilla::dom::cache::CacheStorage> GetCaches( mozilla::ErrorResult& aRv); already_AddRefed<mozilla::dom::Promise> Fetch( - const mozilla::dom::RequestOrUSVString& aInput, + const mozilla::dom::RequestOrUTF8String& aInput, const mozilla::dom::RequestInit& aInit, mozilla::dom::CallerType aCallerType, mozilla::ErrorResult& aRv); MOZ_CAN_RUN_SCRIPT void Print(mozilla::ErrorResult& aError); diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp index e28dcdb092..c678a0a941 100644 --- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp @@ -2414,6 +2414,11 @@ nsresult nsGlobalWindowOuter::SetNewDocument(Document* aDocument, MOZ_RELEASE_ASSERT(newInnerWindow->mDoc == aDocument); + if (mBrowsingContext->IsTopContent()) { + net::CookieJarSettings::Cast(aDocument->CookieJarSettings()) + ->SetTopLevelWindowContextId(aDocument->InnerWindowID()); + } + newInnerWindow->RefreshReduceTimerPrecisionCallerType(); if (!aState) { @@ -3468,9 +3473,17 @@ nsresult nsGlobalWindowOuter::GetInnerSize(CSSSize& aSize) { aSize = CSSPixel::FromAppUnits(viewportSize); - if (StaticPrefs::dom_innerSize_rounded()) { - aSize.width = std::roundf(aSize.width); - aSize.height = std::roundf(aSize.height); + switch (StaticPrefs::dom_innerSize_rounding()) { + case 1: + aSize.width = std::roundf(aSize.width); + aSize.height = std::roundf(aSize.height); + break; + case 2: + aSize.width = std::truncf(aSize.width); + aSize.height = std::truncf(aSize.height); + break; + default: + break; } return NS_OK; @@ -4993,7 +5006,7 @@ void nsGlobalWindowOuter::PrintOuter(ErrorResult& aError) { const bool forPreview = !StaticPrefs::print_always_print_silent(); Print(nullptr, nullptr, nullptr, nullptr, IsPreview(forPreview), - IsForWindowDotPrint::Yes, nullptr, aError); + IsForWindowDotPrint::Yes, nullptr, nullptr, aError); #endif } @@ -5015,7 +5028,8 @@ Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print( nsIPrintSettings* aPrintSettings, RemotePrintJobChild* aRemotePrintJob, nsIWebProgressListener* aListener, nsIDocShell* aDocShellToCloneInto, IsPreview aIsPreview, IsForWindowDotPrint aForWindowDotPrint, - PrintPreviewResolver&& aPrintPreviewCallback, ErrorResult& aError) { + PrintPreviewResolver&& aPrintPreviewCallback, + RefPtr<BrowsingContext>* aCachedBrowsingContext, ErrorResult& aError) { #ifdef NS_PRINTING nsCOMPtr<nsIPrintSettingsService> printSettingsService = do_GetService("@mozilla.org/gfx/printsettings-service;1"); @@ -5051,16 +5065,36 @@ Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print( nsCOMPtr<nsIDocumentViewer> viewer; RefPtr<BrowsingContext> bc; bool hasPrintCallbacks = false; - if (docToPrint->IsStaticDocument()) { + bool wasStaticDocument = docToPrint->IsStaticDocument(); + bool usingCachedBrowsingContext = false; + if (aCachedBrowsingContext && *aCachedBrowsingContext) { + MOZ_ASSERT(!wasStaticDocument, + "Why pass in non-empty aCachedBrowsingContext if original " + "document is already static?"); + if (!wasStaticDocument) { + // The passed in document is not a static clone and the caller passed in a + // static clone to reuse, so swap it in. + docToPrint = (*aCachedBrowsingContext)->GetDocument(); + MOZ_ASSERT(docToPrint); + MOZ_ASSERT(docToPrint->IsStaticDocument()); + wasStaticDocument = true; + usingCachedBrowsingContext = true; + } + } + if (wasStaticDocument) { if (aForWindowDotPrint == IsForWindowDotPrint::Yes) { aError.ThrowNotSupportedError( "Calling print() from a print preview is unsupported, did you intend " "to call printPreview() instead?"); return nullptr; } - // We're already a print preview window, just reuse our browsing context / - // content viewer. - bc = sourceBC; + if (usingCachedBrowsingContext) { + bc = docToPrint->GetBrowsingContext(); + } else { + // We're already a print preview window, just reuse our browsing context / + // content viewer. + bc = sourceBC; + } nsCOMPtr<nsIDocShell> docShell = bc->GetDocShell(); if (!docShell) { aError.ThrowNotSupportedError("No docshell"); @@ -5102,6 +5136,10 @@ Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print( if (NS_WARN_IF(aError.Failed())) { return nullptr; } + if (aCachedBrowsingContext) { + MOZ_ASSERT(!*aCachedBrowsingContext); + *aCachedBrowsingContext = bc; + } } if (!bc) { aError.ThrowNotAllowedError("No browsing context"); @@ -5157,6 +5195,24 @@ Nullable<WindowProxyHolder> nsGlobalWindowOuter::Print( "Content viewer didn't implement nsIWebBrowserPrint"); return nullptr; } + bool closeWindowAfterPrint; + if (wasStaticDocument) { + // Here the document was a static clone to begin with that this code did not + // create, so we should not clean it up. + // The exception is if we're using the passed-in aCachedBrowsingContext, in + // which case this is the second print with this static document clone that + // we created the first time through, and we are responsible for cleaning it + // up. + closeWindowAfterPrint = usingCachedBrowsingContext; + } else { + // In this case the document was not a static clone, so we made a static + // clone for printing purposes and must clean it up after the print is done. + // The exception is if aCachedBrowsingContext is non-NULL, meaning the + // caller is intending to print this document again, so we need to defer the + // cleanup until after the second print. + closeWindowAfterPrint = !aCachedBrowsingContext; + } + webBrowserPrint->SetCloseWindowAfterPrint(closeWindowAfterPrint); // For window.print(), we postpone making these calls until the round-trip to // the parent process (triggered by the OpenInternal call above) calls us @@ -5953,10 +6009,11 @@ void nsGlobalWindowOuter::CloseOuter(bool aTrustedCaller) { if (!allowClose) { // We're blocking the close operation // report localized error msg in JS console - nsContentUtils::ReportToConsole( - nsIScriptError::warningFlag, "DOM Window"_ns, - mDoc, // Better name for the category? - nsContentUtils::eDOM_PROPERTIES, "WindowCloseBlockedWarning"); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + "DOM Window"_ns, + mDoc, // Better name for the category? + nsContentUtils::eDOM_PROPERTIES, + "WindowCloseByScriptBlockedWarning"); return; } diff --git a/dom/base/nsGlobalWindowOuter.h b/dom/base/nsGlobalWindowOuter.h index 8337a7353f..3c26344c3d 100644 --- a/dom/base/nsGlobalWindowOuter.h +++ b/dom/base/nsGlobalWindowOuter.h @@ -105,7 +105,6 @@ class PostMessageData; class PostMessageEvent; class PrintPreviewResultInfo; struct RequestInit; -class RequestOrUSVString; class Selection; struct SizeToContentConstraints; class SpeechSynthesis; @@ -581,7 +580,8 @@ class nsGlobalWindowOuter final : public mozilla::dom::EventTarget, Print(nsIPrintSettings*, mozilla::layout::RemotePrintJobChild* aRemotePrintJob, nsIWebProgressListener*, nsIDocShell*, IsPreview, IsForWindowDotPrint, - PrintPreviewResolver&&, mozilla::ErrorResult&); + PrintPreviewResolver&&, RefPtr<mozilla::dom::BrowsingContext>*, + mozilla::ErrorResult&); mozilla::dom::Selection* GetSelectionOuter(); already_AddRefed<mozilla::dom::Selection> GetSelection() override; nsScreen* GetScreen(); diff --git a/dom/base/nsIContentInlines.h b/dom/base/nsIContentInlines.h index 89cc7b9c30..04e73b75e1 100644 --- a/dom/base/nsIContentInlines.h +++ b/dom/base/nsIContentInlines.h @@ -82,10 +82,27 @@ static inline nsINode* GetFlattenedTreeParentNode(const nsINode* aNode) { return parent; } - if (parentAsContent->GetShadowRoot()) { - // If it's not assigned to any slot it's not part of the flat tree, and thus - // we return null. - return content->GetAssignedSlot(); + // Use GetShadowRootForSelection for the selection case such that + // if the content is slotted into a UA shadow tree, use + // the parent of content as the flattened tree parent (instead of + // the slot element). + const nsINode* shadowRootForParent = + aType == nsINode::eForSelection + ? parentAsContent->GetShadowRootForSelection() + : parentAsContent->GetShadowRoot(); + + if (shadowRootForParent) { + // When aType is not nsINode::eForSelection, If it's not assigned to any + // slot it's not part of the flat tree, and thus we return null. + auto* assignedSlot = content->GetAssignedSlot(); + if (assignedSlot || aType != nsINode::eForSelection) { + return assignedSlot; + } + + MOZ_ASSERT(aType == nsINode::eForSelection); + // When aType is nsINode::eForSelection, we use the parent of the + // content even if it's not assigned to any slot. + return parent; } if (parentAsContent->IsInShadowTree()) { @@ -106,7 +123,7 @@ static inline nsINode* GetFlattenedTreeParentNode(const nsINode* aNode) { } inline nsINode* nsINode::GetFlattenedTreeParentNode() const { - return ::GetFlattenedTreeParentNode<nsINode::eNotForStyle>(this); + return ::GetFlattenedTreeParentNode<nsINode::eNormal>(this); } inline nsIContent* nsIContent::GetFlattenedTreeParent() const { @@ -127,6 +144,11 @@ inline nsINode* nsINode::GetFlattenedTreeParentNodeForStyle() const { return ::GetFlattenedTreeParentNode<nsINode::eForStyle>(this); } +inline nsIContent* nsINode::GetFlattenedTreeParentNodeForSelection() const { + nsINode* parent = ::GetFlattenedTreeParentNode<nsINode::eForSelection>(this); + return (parent && parent->IsContent()) ? parent->AsContent() : nullptr; +} + inline bool nsINode::NodeOrAncestorHasDirAuto() const { return AncestorHasDirAuto() || (IsElement() && AsElement()->HasDirAuto()); } diff --git a/dom/base/nsIEventSourceEventService.idl b/dom/base/nsIEventSourceEventService.idl index 6131424669..be96d58260 100644 --- a/dom/base/nsIEventSourceEventService.idl +++ b/dom/base/nsIEventSourceEventService.idl @@ -31,5 +31,5 @@ interface nsIEventSourceEventService : nsISupports [must_use] void removeListener(in unsigned long long aInnerWindowID, in nsIEventSourceEventListener aListener); - [must_use] bool hasListenerFor(in unsigned long long aInnerWindowID); + [must_use] boolean hasListenerFor(in unsigned long long aInnerWindowID); }; diff --git a/dom/base/nsINode.cpp b/dom/base/nsINode.cpp index d5455e5596..6c77574df7 100644 --- a/dom/base/nsINode.cpp +++ b/dom/base/nsINode.cpp @@ -287,11 +287,17 @@ static const nsINode* GetClosestCommonInclusiveAncestorForRangeInSelection( const nsINode* aNode) { while (aNode && !aNode->IsClosestCommonInclusiveAncestorForRangeInSelection()) { + const bool isNodeInShadowTree = + StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() && + aNode->IsInShadowTree(); if (!aNode - ->IsDescendantOfClosestCommonInclusiveAncestorForRangeInSelection()) { + ->IsDescendantOfClosestCommonInclusiveAncestorForRangeInSelection() && + !isNodeInShadowTree) { return nullptr; } - aNode = aNode->GetParentNode(); + aNode = StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() + ? aNode->GetParentOrShadowHostNode() + : aNode->GetParentNode(); } return aNode; } @@ -315,12 +321,12 @@ class IsItemInRangeComparator { int operator()(const AbstractRange* const aRange) const { int32_t cmp = nsContentUtils::ComparePoints_Deprecated( - &mNode, mEndOffset, aRange->GetStartContainer(), aRange->StartOffset(), - nullptr, mCache); + &mNode, mEndOffset, aRange->GetMayCrossShadowBoundaryStartContainer(), + aRange->MayCrossShadowBoundaryStartOffset(), nullptr, mCache); if (cmp == 1) { cmp = nsContentUtils::ComparePoints_Deprecated( - &mNode, mStartOffset, aRange->GetEndContainer(), aRange->EndOffset(), - nullptr, mCache); + &mNode, mStartOffset, aRange->GetMayCrossShadowBoundaryEndContainer(), + aRange->MayCrossShadowBoundaryEndOffset(), nullptr, mCache); if (cmp == -1) { return 0; } @@ -386,6 +392,18 @@ bool nsINode::IsSelected(const uint32_t aStartOffset, return true; } + if (range->MayCrossShadowBoundary()) { + MOZ_ASSERT(range->IsDynamicRange(), + "range->MayCrossShadowBoundary() can only return true for " + "dynamic range"); + StaticRange* crossBoundaryRange = + range->AsDynamicRange()->GetCrossShadowBoundaryRange(); + MOZ_ASSERT(crossBoundaryRange); + if (!crossBoundaryRange->Collapsed()) { + return true; + } + } + const AbstractRange* middlePlus1; const AbstractRange* middleMinus1; // if node end > start of middle+1, result = 1 @@ -552,7 +570,8 @@ static nsIContent* GetRootForContentSubtree(nsIContent* aContent) { return nsIContent::FromNode(aContent->SubtreeRoot()); } -nsIContent* nsINode::GetSelectionRootContent(PresShell* aPresShell) { +nsIContent* nsINode::GetSelectionRootContent(PresShell* aPresShell, + bool aAllowCrossShadowBoundary) { NS_ENSURE_TRUE(aPresShell, nullptr); if (IsDocument()) return AsDocument()->GetRootElement(); @@ -596,7 +615,7 @@ nsIContent* nsINode::GetSelectionRootContent(PresShell* aPresShell) { } RefPtr<nsFrameSelection> fs = aPresShell->FrameSelection(); - nsIContent* content = fs->GetLimiter(); + nsCOMPtr<nsIContent> content = fs->GetLimiter(); if (!content) { content = fs->GetAncestorLimiter(); if (!content) { @@ -616,6 +635,10 @@ nsIContent* nsINode::GetSelectionRootContent(PresShell* aPresShell) { // Use the host as the root. if (ShadowRoot* shadowRoot = ShadowRoot::FromNode(content)) { content = shadowRoot->GetHost(); + if (content && aAllowCrossShadowBoundary) { + content = content->GetSelectionRootContent(aPresShell, + aAllowCrossShadowBoundary); + } } } @@ -3826,6 +3849,33 @@ void nsINode::FireNodeRemovedForChildren() { } } +ShadowRoot* nsINode::GetShadowRoot() const { + return IsContent() ? AsContent()->GetShadowRoot() : nullptr; +} + +ShadowRoot* nsINode::GetShadowRootForSelection() const { + if (!StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) { + return nullptr; + } + + ShadowRoot* shadowRoot = GetShadowRoot(); + if (!shadowRoot) { + return nullptr; + } + + // ie. <details> and <video> + if (shadowRoot->IsUAWidget()) { + return nullptr; + } + + // ie. <use> element + if (IsElement() && !AsElement()->CanAttachShadowDOM()) { + return nullptr; + } + + return shadowRoot; +} + NS_IMPL_ISUPPORTS(nsNodeWeakReference, nsIWeakReference) nsNodeWeakReference::nsNodeWeakReference(nsINode* aNode) diff --git a/dom/base/nsINode.h b/dom/base/nsINode.h index 3a47992cc8..d2a2fd008d 100644 --- a/dom/base/nsINode.h +++ b/dom/base/nsINode.h @@ -1099,7 +1099,7 @@ class nsINode : public mozilla::dom::EventTarget { : nullptr; } - enum FlattenedParentType { eNotForStyle, eForStyle }; + enum FlattenedParentType { eNormal, eForStyle, eForSelection }; /** * Returns the node that is the parent of this node in the flattened @@ -1121,6 +1121,15 @@ class nsINode : public mozilla::dom::EventTarget { */ inline nsINode* GetFlattenedTreeParentNodeForStyle() const; + /** + * Similar to GetFlattenedTreeParentNode, it does two things differently + * 1. For contents that are not in the flattened tree, use its + * parent rather than nullptr. + * 2. For contents that are slotted into a UA shadow tree, use its + * parent rather than the slot element. + */ + inline nsIContent* GetFlattenedTreeParentNodeForSelection() const; + inline mozilla::dom::Element* GetFlattenedTreeParentElement() const; inline mozilla::dom::Element* GetFlattenedTreeParentElementForStyle() const; @@ -1629,7 +1638,7 @@ class nsINode : public mozilla::dom::EventTarget { * not in same subtree, this returns the root content of the closeset subtree. */ MOZ_CAN_RUN_SCRIPT nsIContent* GetSelectionRootContent( - mozilla::PresShell* aPresShell); + mozilla::PresShell* aPresShell, bool aAllowCrossShadowBoundary = false); nsINodeList* ChildNodes(); @@ -2074,6 +2083,14 @@ class nsINode : public mozilla::dom::EventTarget { ClearBoolFlag(ElementCreatedFromPrototypeAndHasUnmodifiedL10n); } + mozilla::dom::ShadowRoot* GetShadowRoot() const; + + // Return the shadow root of the node if it is a shadow host and + // it meets the requirements for being a shadow host of a selection. + // For example, <details>, <video> and <use> elements are not valid + // shadow host for selection. + mozilla::dom::ShadowRoot* GetShadowRootForSelection() const; + protected: void SetParentIsContent(bool aValue) { SetBoolFlag(ParentIsContent, aValue); } void SetIsInDocument() { SetBoolFlag(IsInDocument); } diff --git a/dom/base/nsIScriptableContentIterator.idl b/dom/base/nsIScriptableContentIterator.idl index 370cd8c8a7..80208ac983 100644 --- a/dom/base/nsIScriptableContentIterator.idl +++ b/dom/base/nsIScriptableContentIterator.idl @@ -38,6 +38,11 @@ interface nsIScriptableContentIterator : nsISupports void initWithRange(in nsIScriptableContentIterator_IteratorType aType, in Range aRange); + // See ContentSubtreeIterator::InitWithAllowCrossShadowBoundary(nsRange*) + void initWithRangeAllowCrossShadowBoundary( + in nsIScriptableContentIterator_IteratorType aType, + in Range aRange); + // See ContentIteratorBase::Init(nsINode*, uint32_t, nsINode*, uint32_t) void initWithPositions(in nsIScriptableContentIterator_IteratorType aType, in Node aStartContainer, in unsigned long aStartOffset, @@ -59,7 +64,7 @@ interface nsIScriptableContentIterator : nsISupports readonly attribute Node currentNode; // See ContentIteratorBase::IsDone() - readonly attribute bool isDone; + readonly attribute boolean isDone; // See ContentIteratorBase::PositionAt(nsINode*) void positionAt(in Node aNode); diff --git a/dom/base/nsISelectionController.idl b/dom/base/nsISelectionController.idl index f2d5ebe65f..9c76e45f43 100644 --- a/dom/base/nsISelectionController.idl +++ b/dom/base/nsISelectionController.idl @@ -41,11 +41,12 @@ interface nsISelectionController : nsISelectionDisplay const short SELECTION_FIND = 8; const short SELECTION_URLSECONDARY = 9; const short SELECTION_URLSTRIKEOUT = 10; + const short SELECTION_TARGET_TEXT = 11; // Custom Highlight API // (see https://drafts.csswg.org/css-highlight-api-1/#enumdef-highlighttype) - const short SELECTION_HIGHLIGHT = 11; + const short SELECTION_HIGHLIGHT = 12; // End of RawSelectionType values. - const short NUM_SELECTIONTYPES = 12; + const short NUM_SELECTIONTYPES = 13; // SelectionRegion values: const short SELECTION_ANCHOR_REGION = 0; @@ -311,6 +312,7 @@ enum class SelectionType : RawSelectionType eFind = nsISelectionController::SELECTION_FIND, eURLSecondary = nsISelectionController::SELECTION_URLSECONDARY, eURLStrikeout = nsISelectionController::SELECTION_URLSTRIKEOUT, + eTargetText = nsISelectionController::SELECTION_TARGET_TEXT, eHighlight = nsISelectionController::SELECTION_HIGHLIGHT, }; @@ -327,6 +329,7 @@ static const SelectionType kPresentSelectionTypes[] = { SelectionType::eFind, SelectionType::eURLSecondary, SelectionType::eURLStrikeout, + SelectionType::eTargetText, SelectionType::eHighlight, }; diff --git a/dom/base/nsJSEnvironment.cpp b/dom/base/nsJSEnvironment.cpp index 1397bd25b5..a14a22bcf0 100644 --- a/dom/base/nsJSEnvironment.cpp +++ b/dom/base/nsJSEnvironment.cpp @@ -1707,38 +1707,38 @@ void nsJSContext::MaybeRunNextCollectorSlice(nsIDocShell* aDocShell, return; } - if (!sScheduler->IsUserActive()) { - if (sScheduler->InIncrementalGC() || sScheduler->IsCollectingCycles()) { - Maybe<TimeStamp> next = nsRefreshDriver::GetNextTickHint(); - if (next.isSome()) { - // Try to not delay the next RefreshDriver tick, so give a reasonable - // deadline for collectors. - sScheduler->RunNextCollectorTimer(aReason, next.value()); - } - } else { - nsCOMPtr<nsIDocShell> shell = aDocShell; - NS_DispatchToCurrentThreadQueue( - NS_NewRunnableFunction( - "nsJSContext::MaybeRunNextCollectorSlice", - [shell] { - nsIDocShell::BusyFlags busyFlags = nsIDocShell::BUSY_FLAGS_NONE; - shell->GetBusyFlags(&busyFlags); - if (busyFlags == nsIDocShell::BUSY_FLAGS_NONE) { - return; - } - - // In order to improve performance on the next page, run a minor - // GC. The 16ms limit ensures it isn't called all the time if - // there are for example multiple iframes loading at the same - // time. - JS::RunNurseryCollection( - CycleCollectedJSRuntime::Get()->Runtime(), - JS::GCReason::PREPARE_FOR_PAGELOAD, - mozilla::TimeDuration::FromMilliseconds(16)); - }), - EventQueuePriority::Idle); + if (!sScheduler->IsUserActive() && + (sScheduler->InIncrementalGC() || sScheduler->IsCollectingCycles())) { + Maybe<TimeStamp> next = nsRefreshDriver::GetNextTickHint(); + if (next.isSome()) { + // Try to not delay the next RefreshDriver tick, so give a reasonable + // deadline for collectors. + sScheduler->RunNextCollectorTimer(aReason, next.value()); } } + + nsCOMPtr<nsIDocShell> shell = aDocShell; + NS_DispatchToCurrentThreadQueue( + NS_NewRunnableFunction("nsJSContext::MaybeRunNextCollectorSlice", + [shell] { + nsIDocShell::BusyFlags busyFlags = + nsIDocShell::BUSY_FLAGS_NONE; + shell->GetBusyFlags(&busyFlags); + if (busyFlags == nsIDocShell::BUSY_FLAGS_NONE) { + return; + } + + // In order to improve performance on the next + // page, run a minor GC. The 16ms limit ensures + // it isn't called all the time if there are for + // example multiple iframes loading at the same + // time. + JS::RunNurseryCollection( + CycleCollectedJSRuntime::Get()->Runtime(), + JS::GCReason::PREPARE_FOR_PAGELOAD, + mozilla::TimeDuration::FromMilliseconds(16)); + }), + EventQueuePriority::Idle); } // static @@ -2121,6 +2121,13 @@ void nsJSContext::EnsureStatics() { "javascript.options.mem.gc_compacting", (void*)JSGC_COMPACTING_ENABLED); +#ifdef NIGHTLY_BUILD + Preferences::RegisterCallbackAndCall( + SetMemoryPrefChangedCallbackBool, + "javascript.options.mem.gc_experimental_semispace_nursery", + (void*)JSGC_SEMISPACE_NURSERY_ENABLED); +#endif + Preferences::RegisterCallbackAndCall( SetMemoryPrefChangedCallbackBool, "javascript.options.mem.gc_parallel_marking", diff --git a/dom/base/nsMimeTypeArray.cpp b/dom/base/nsMimeTypeArray.cpp index 4d15e1d8a2..376c7e1065 100644 --- a/dom/base/nsMimeTypeArray.cpp +++ b/dom/base/nsMimeTypeArray.cpp @@ -94,3 +94,7 @@ JSObject* nsMimeType::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { return MimeType_Binding::Wrap(aCx, this, aGivenProto); } + +already_AddRefed<nsPluginElement> nsMimeType::EnabledPlugin() const { + return do_AddRef(mPluginElement); +} diff --git a/dom/base/nsMimeTypeArray.h b/dom/base/nsMimeTypeArray.h index aaeedba3ae..207f5dc542 100644 --- a/dom/base/nsMimeTypeArray.h +++ b/dom/base/nsMimeTypeArray.h @@ -83,9 +83,7 @@ class nsMimeType final : public nsWrapperCache { retval.SetKnownLiveString(kMimeDescription); } - already_AddRefed<nsPluginElement> EnabledPlugin() const { - return do_AddRef(mPluginElement); - } + already_AddRefed<nsPluginElement> EnabledPlugin() const; void GetSuffixes(mozilla::dom::DOMString& retval) const { retval.SetKnownLiveString(kMimeSuffix); diff --git a/dom/base/nsRange.cpp b/dom/base/nsRange.cpp index 3121ed86c3..cf15f239c5 100644 --- a/dom/base/nsRange.cpp +++ b/dom/base/nsRange.cpp @@ -105,16 +105,30 @@ template nsresult nsRange::SetStartAndEnd( template void nsRange::DoSetRange(const RangeBoundary& aStartBoundary, const RangeBoundary& aEndBoundary, - nsINode* aRootNode, bool aNotInsertedYet); + nsINode* aRootNode, bool aNotInsertedYet, + CollapsePolicy aCollapsePolicy); template void nsRange::DoSetRange(const RangeBoundary& aStartBoundary, const RawRangeBoundary& aEndBoundary, - nsINode* aRootNode, bool aNotInsertedYet); + nsINode* aRootNode, bool aNotInsertedYet, + CollapsePolicy aCollapsePolicy); template void nsRange::DoSetRange(const RawRangeBoundary& aStartBoundary, const RangeBoundary& aEndBoundary, - nsINode* aRootNode, bool aNotInsertedYet); + nsINode* aRootNode, bool aNotInsertedYet, + CollapsePolicy aCollapsePolicy); template void nsRange::DoSetRange(const RawRangeBoundary& aStartBoundary, const RawRangeBoundary& aEndBoundary, - nsINode* aRootNode, bool aNotInsertedYet); + nsINode* aRootNode, bool aNotInsertedYet, + CollapsePolicy aCollapsePolicy); + +template void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + const RangeBoundary& aStartBoundary, const RangeBoundary& aEndBoundary); +template void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + const RangeBoundary& aStartBoundary, const RawRangeBoundary& aEndBoundary); +template void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + const RawRangeBoundary& aStartBoundary, const RangeBoundary& aEndBoundary); +template void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + const RawRangeBoundary& aStartBoundary, + const RawRangeBoundary& aEndBoundary); JSObject* nsRange::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { @@ -171,7 +185,7 @@ nsRange::nsRange(nsINode* aNode) mNextEndRef(nullptr) { // printf("Size of nsRange: %zu\n", sizeof(nsRange)); - static_assert(sizeof(nsRange) <= 240, + static_assert(sizeof(nsRange) <= 248, "nsRange size shouldn't be increased as far as possible"); } @@ -201,6 +215,109 @@ already_AddRefed<nsRange> nsRange::Create( return range.forget(); } +/* + * When a new boundary is given to a nsRange, compare its position with other + * existing boundaries to see if we need to collapse the end points. + * + * aRange: The nsRange that aNewBoundary is being set to. + * aNewRoot: The shadow-including root of the container of aNewBoundary + * aNewBoundary: The new boundary + * aIsSetStart: true if ShouldCollapseBoundary is called by nsRange::SetStart, + * false otherwise + * aAllowCrossShadowBoundary: Indicates whether the boundaries allowed to cross + * shadow boundary or not + */ +static CollapsePolicy ShouldCollapseBoundary( + const nsRange* aRange, const nsINode* aNewRoot, + const RawRangeBoundary& aNewBoundary, const bool aIsSetStart, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { + if (!aRange->IsPositioned()) { + return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges; + } + + MOZ_ASSERT(aRange->GetRoot()); + if (aNewRoot != aRange->GetRoot()) { + // Boundaries are in different document (or not connected), so collapse + // the both the default range and the crossBoundaryRange range. + if (aNewRoot->GetComposedDoc() != aRange->GetRoot()->GetComposedDoc()) { + return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges; + } + + // Always collapse both ranges if the one of the roots is an UA widget + // regardless whether the boundaries are allowed to cross shadow boundary + // or not. + if (AbstractRange::IsRootUAWidget(aNewRoot) || + AbstractRange::IsRootUAWidget(aRange->GetRoot())) { + return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges; + } + + // Different root, but same document. So we only collapse the + // default range if boundaries are allowed to cross shadow boundary. + return aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes + ? CollapsePolicy::DefaultRange + : CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges; + } + + const RangeBoundary& otherSideExistingBoundary = + aIsSetStart ? aRange->EndRef() : aRange->StartRef(); + + // Both bondaries are in the same root, now check for their position + const Maybe<int32_t> order = + aIsSetStart ? nsContentUtils::ComparePoints(aNewBoundary, + otherSideExistingBoundary) + : nsContentUtils::ComparePoints(otherSideExistingBoundary, + aNewBoundary); + + if (order) { + if (*order != 1) { + // aNewBoundary is at a valid position. + // + // If aIsSetStart is true, this means + // aNewBoundary <= otherSideExistingBoundary which is + // good because aNewBoundary intends to be the start. + // + // If aIsSetStart is false, this means + // otherSideExistingBoundary <= aNewBoundary which is good because + // aNewBoundary intends to be the end. + // + // So no collapse for above cases. + return CollapsePolicy::No; + } + + if (!aRange->MayCrossShadowBoundary() || + aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::No) { + return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges; + } + + const RangeBoundary& otherSideExistingCrossShadowBoundaryBoundary = + aIsSetStart ? aRange->MayCrossShadowBoundaryEndRef() + : aRange->MayCrossShadowBoundaryStartRef(); + + // Please see the comment for (*order != 1) to see what "valid" means. + // + // We reach to this line when (*order == 1), it means aNewBoundary is + // at an invalid position, so we need to collapse aNewBoundary with + // otherSideExistingBoundary. However, it's possible that aNewBoundary + // is valid with the otherSideExistingCrossShadowBoundaryBoundary. + const Maybe<int32_t> withCrossShadowBoundaryOrder = + aIsSetStart + ? nsContentUtils::ComparePoints( + aNewBoundary, otherSideExistingCrossShadowBoundaryBoundary) + : nsContentUtils::ComparePoints( + otherSideExistingCrossShadowBoundaryBoundary, aNewBoundary); + + // Valid to the cross boundary boundary. + if (withCrossShadowBoundaryOrder && *withCrossShadowBoundaryOrder != 1) { + return CollapsePolicy::DefaultRange; + } + + // Not valid to both existing boundaries. + return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges; + } + + MOZ_ASSERT_UNREACHABLE(); + return CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges; +} /****************************************************** * nsISupports ******************************************************/ @@ -219,11 +336,13 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(nsRange) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(nsRange, AbstractRange) // `Reset()` unlinks `mStart`, `mEnd` and `mRoot`. + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCrossShadowBoundaryRange); tmp->Reset(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(nsRange, AbstractRange) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCrossShadowBoundaryRange); NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(nsRange, AbstractRange) @@ -231,6 +350,7 @@ NS_IMPL_CYCLE_COLLECTION_TRACE_END bool nsRange::MaybeInterruptLastRelease() { bool interrupt = AbstractRange::MaybeCacheToReuse(*this); + ResetCrossShadowBoundaryRange(); MOZ_ASSERT(!interrupt || IsCleared()); return interrupt; } @@ -573,6 +693,21 @@ void nsRange::ContentRemoved(nsIContent* aChild, nsIContent* aPreviousSibling) { nsINode* startContainer = mStart.Container(); nsINode* endContainer = mEnd.Container(); + // FIXME(sefeng): Temporary Solution for ContentRemoved + // editing/crashtests/removeformat-from-DOMNodeRemoved.html can be used to + // verify this. + if (mCrossShadowBoundaryRange) { + if (mCrossShadowBoundaryRange->GetStartContainer() == aChild || + mCrossShadowBoundaryRange->GetEndContainer() == aChild) { + ResetCrossShadowBoundaryRange(); + } else if (ShadowRoot* shadowRoot = aChild->GetShadowRoot()) { + if (mCrossShadowBoundaryRange->GetStartContainer() == shadowRoot || + mCrossShadowBoundaryRange->GetEndContainer() == shadowRoot) { + ResetCrossShadowBoundaryRange(); + } + } + } + RawRangeBoundary newStart; RawRangeBoundary newEnd; Maybe<bool> gravitateStart; @@ -881,10 +1016,12 @@ void nsRange::AssertIfMismatchRootAndRangeBoundaries( // Calling DoSetRange with either parent argument null will collapse // the range to have both endpoints point to the other node template <typename SPT, typename SRT, typename EPT, typename ERT> -void nsRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary, - const RangeBoundaryBase<EPT, ERT>& aEndBoundary, - nsINode* aRootNode, - bool aNotInsertedYet /* = false */) { +void nsRange::DoSetRange( + const RangeBoundaryBase<SPT, SRT>& aStartBoundary, + const RangeBoundaryBase<EPT, ERT>& aEndBoundary, nsINode* aRootNode, + bool aNotInsertedYet /* = false */, + CollapsePolicy + aCollapsePolicy /* = DEFAULT_RANGE_AND_CROSS_BOUNDARY_RANGES */) { mIsPositioned = aStartBoundary.IsSetAndValid() && aEndBoundary.IsSetAndValid() && aRootNode; MOZ_ASSERT_IF(!mIsPositioned, !aStartBoundary.IsSet()); @@ -913,6 +1050,11 @@ void nsRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary, mStart.CopyFrom(aStartBoundary, RangeBoundaryIsMutationObserved::Yes); mEnd.CopyFrom(aEndBoundary, RangeBoundaryIsMutationObserved::Yes); + if (aCollapsePolicy == + CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges) { + ResetCrossShadowBoundaryRange(); + } + if (checkCommonAncestor) { UpdateCommonAncestorIfNecessary(); } @@ -965,17 +1107,21 @@ bool nsRange::CanAccess(const nsINode& aNode) const { return nsContentUtils::CanCallerAccess(&aNode); } -void nsRange::SetStart(nsINode& aNode, uint32_t aOffset, ErrorResult& aRv) { +void nsRange::SetStart( + nsINode& aNode, uint32_t aOffset, ErrorResult& aRv, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { if (!CanAccess(aNode)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return; } AutoInvalidateSelection atEndOfBlock(this); - SetStart(RawRangeBoundary(&aNode, aOffset), aRv); + SetStart(RawRangeBoundary(&aNode, aOffset), aRv, aAllowCrossShadowBoundary); } -void nsRange::SetStart(const RawRangeBoundary& aPoint, ErrorResult& aRv) { +void nsRange::SetStart( + const RawRangeBoundary& aPoint, ErrorResult& aRv, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { nsINode* newRoot = RangeUtils::ComputeRootNode(aPoint.Container()); if (!newRoot) { aRv.Throw(NS_ERROR_DOM_INVALID_NODE_TYPE_ERR); @@ -987,28 +1133,46 @@ void nsRange::SetStart(const RawRangeBoundary& aPoint, ErrorResult& aRv) { return; } - // Collapse if not positioned yet, if positioned in another doc or - // if the new start is after end. - const bool collapse = [&]() { - if (!mIsPositioned || (newRoot != mRoot)) { - return true; - } - - const Maybe<int32_t> order = nsContentUtils::ComparePoints(aPoint, mEnd); - if (order) { - return *order == 1; - } - - MOZ_ASSERT_UNREACHABLE(); - return true; - }(); + CollapsePolicy policy = + ShouldCollapseBoundary(this, newRoot, aPoint, true /* aIsSetStart= */, + aAllowCrossShadowBoundary); - if (collapse) { - DoSetRange(aPoint, aPoint, newRoot); - return; + switch (policy) { + case CollapsePolicy::No: + // EndRef(..) may be same as mStart or not, depends on + // the value of mCrossShadowBoundaryRange->mEnd, We need to update + // mCrossShadowBoundaryRange and the default boundaries separately + if (aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) { + if (MayCrossShadowBoundaryEndRef() != mEnd) { + CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + aPoint, MayCrossShadowBoundaryEndRef()); + } else { + // The normal range is good enough for this case, just use that. + ResetCrossShadowBoundaryRange(); + } + } + DoSetRange(aPoint, mEnd, mRoot, false, policy); + break; + case CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges: + DoSetRange(aPoint, aPoint, newRoot, false, policy); + break; + case CollapsePolicy::DefaultRange: + MOZ_ASSERT(aAllowCrossShadowBoundary == + AllowRangeCrossShadowBoundary::Yes); + CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + aPoint, MayCrossShadowBoundaryEndRef()); + DoSetRange(aPoint, aPoint, newRoot, false, policy); + break; + default: + MOZ_ASSERT_UNREACHABLE(); } +} - DoSetRange(aPoint, mEnd, mRoot); +void nsRange::SetStartAllowCrossShadowBoundary(nsINode& aNode, uint32_t aOffset, + ErrorResult& aErr) { + AutoCalledByJSRestore calledByJSRestorer(*this); + mCalledByJS = true; + SetStart(aNode, aOffset, aErr, AllowRangeCrossShadowBoundary::Yes); } void nsRange::SetStartBeforeJS(nsINode& aNode, ErrorResult& aErr) { @@ -1017,7 +1181,9 @@ void nsRange::SetStartBeforeJS(nsINode& aNode, ErrorResult& aErr) { SetStartBefore(aNode, aErr); } -void nsRange::SetStartBefore(nsINode& aNode, ErrorResult& aRv) { +void nsRange::SetStartBefore( + nsINode& aNode, ErrorResult& aRv, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { if (!CanAccess(aNode)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return; @@ -1027,7 +1193,8 @@ void nsRange::SetStartBefore(nsINode& aNode, ErrorResult& aRv) { // If the node is being removed from its parent, GetRawRangeBoundaryBefore() // returns unset instance. Then, SetStart() will throw // NS_ERROR_DOM_INVALID_NODE_TYPE_ERR. - SetStart(RangeUtils::GetRawRangeBoundaryBefore(&aNode), aRv); + SetStart(RangeUtils::GetRawRangeBoundaryBefore(&aNode), aRv, + aAllowCrossShadowBoundary); } void nsRange::SetStartAfterJS(nsINode& aNode, ErrorResult& aErr) { @@ -1055,16 +1222,18 @@ void nsRange::SetEndJS(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr) { SetEnd(aNode, aOffset, aErr); } -void nsRange::SetEnd(nsINode& aNode, uint32_t aOffset, ErrorResult& aRv) { +void nsRange::SetEnd(nsINode& aNode, uint32_t aOffset, ErrorResult& aRv, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { if (!CanAccess(aNode)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return; } AutoInvalidateSelection atEndOfBlock(this); - SetEnd(RawRangeBoundary(&aNode, aOffset), aRv); + SetEnd(RawRangeBoundary(&aNode, aOffset), aRv, aAllowCrossShadowBoundary); } -void nsRange::SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aRv) { +void nsRange::SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aRv, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { nsINode* newRoot = RangeUtils::ComputeRootNode(aPoint.Container()); if (!newRoot) { aRv.Throw(NS_ERROR_DOM_INVALID_NODE_TYPE_ERR); @@ -1076,28 +1245,47 @@ void nsRange::SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aRv) { return; } - // Collapse if not positioned yet, if positioned in another doc or - // if the new end is before start. - const bool collapse = [&]() { - if (!mIsPositioned || (newRoot != mRoot)) { - return true; - } + CollapsePolicy policy = + ShouldCollapseBoundary(this, newRoot, aPoint, false /* aIsStartStart */, + aAllowCrossShadowBoundary); - const Maybe<int32_t> order = nsContentUtils::ComparePoints(mStart, aPoint); - if (order) { - return *order == 1; - } - - MOZ_ASSERT_UNREACHABLE(); - return true; - }(); - - if (collapse) { - DoSetRange(aPoint, aPoint, newRoot); - return; + switch (policy) { + case CollapsePolicy::No: + // StartRef(..) may be same as mStart or not, depends on + // the value of mCrossShadowBoundaryRange->mStart, so we need to update + // mCrossShadowBoundaryRange and the default boundaries separately + if (aAllowCrossShadowBoundary == AllowRangeCrossShadowBoundary::Yes) { + if (MayCrossShadowBoundaryStartRef() != mStart) { + CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + MayCrossShadowBoundaryStartRef(), aPoint); + } else { + // The normal range is good enough for this case, just use that. + ResetCrossShadowBoundaryRange(); + } + } + DoSetRange(mStart, aPoint, mRoot, false, policy); + break; + case CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges: + DoSetRange(aPoint, aPoint, newRoot, false, policy); + break; + case CollapsePolicy::DefaultRange: + MOZ_ASSERT(aAllowCrossShadowBoundary == + AllowRangeCrossShadowBoundary::Yes); + CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + MayCrossShadowBoundaryStartRef(), aPoint); + DoSetRange(aPoint, aPoint, newRoot, false, policy); + break; + default: + MOZ_ASSERT_UNREACHABLE(); } +} - DoSetRange(mStart, aPoint, mRoot); +void nsRange::SetEndAllowCrossShadowBoundary(nsINode& aNode, uint32_t aOffset, + ErrorResult& aErr) { + AutoCalledByJSRestore calledByJSRestorer(*this); + mCalledByJS = true; + SetEnd(aNode, aOffset, aErr, + AllowRangeCrossShadowBoundary::Yes /* aAllowCrossShadowBoundary */); } void nsRange::SelectNodesInContainer(nsINode* aContainer, @@ -1127,7 +1315,9 @@ void nsRange::SetEndBeforeJS(nsINode& aNode, ErrorResult& aErr) { SetEndBefore(aNode, aErr); } -void nsRange::SetEndBefore(nsINode& aNode, ErrorResult& aRv) { +void nsRange::SetEndBefore( + nsINode& aNode, ErrorResult& aRv, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary) { if (!CanAccess(aNode)) { aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); return; @@ -1137,7 +1327,8 @@ void nsRange::SetEndBefore(nsINode& aNode, ErrorResult& aRv) { // If the node is being removed from its parent, GetRawRangeBoundaryBefore() // returns unset instance. Then, SetEnd() will throw // NS_ERROR_DOM_INVALID_NODE_TYPE_ERR. - SetEnd(RangeUtils::GetRawRangeBoundaryBefore(&aNode), aRv); + SetEnd(RangeUtils::GetRawRangeBoundaryBefore(&aNode), aRv, + aAllowCrossShadowBoundary); } void nsRange::SetEndAfterJS(nsINode& aNode, ErrorResult& aErr) { @@ -2201,6 +2392,11 @@ already_AddRefed<DocumentFragment> nsRange::CloneContents(ErrorResult& aRv) { already_AddRefed<nsRange> nsRange::CloneRange() const { RefPtr<nsRange> range = nsRange::Create(mOwner); range->DoSetRange(mStart, mEnd, mRoot); + if (mCrossShadowBoundaryRange) { + range->CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + mCrossShadowBoundaryRange->StartRef(), + mCrossShadowBoundaryRange->EndRef()); + } return range.forget(); } @@ -2640,13 +2836,14 @@ static nsresult GetPartialTextRect(RectCallback* aCallback, static void CollectClientRectsForSubtree( nsINode* aNode, RectCallback* aCollector, Sequence<nsString>* aTextList, nsINode* aStartContainer, uint32_t aStartOffset, nsINode* aEndContainer, - uint32_t aEndOffset, bool aClampToEdge, bool aFlushLayout) { + uint32_t aEndOffset, bool aClampToEdge, bool aFlushLayout, bool aTextOnly) { auto* content = nsIContent::FromNode(aNode); if (!content) { return; } - if (content->IsText()) { + const bool isText = content->IsText(); + if (isText) { if (aNode == aStartContainer) { int32_t offset = aStartContainer == aEndContainer ? static_cast<int32_t>(aEndOffset) @@ -2665,19 +2862,30 @@ static void CollectClientRectsForSubtree( } } - if (aNode->IsElement() && aNode->AsElement()->IsDisplayContents()) { - FlattenedChildIterator childIter(content); - - for (nsIContent* child = childIter.GetNextChild(); child; - child = childIter.GetNextChild()) { - CollectClientRectsForSubtree(child, aCollector, aTextList, - aStartContainer, aStartOffset, aEndContainer, - aEndOffset, aClampToEdge, aFlushLayout); + if (nsIFrame* frame = content->GetPrimaryFrame()) { + if (!aTextOnly || isText) { + nsLayoutUtils::GetAllInFlowRectsAndTexts( + frame, nsLayoutUtils::GetContainingBlockForClientRect(frame), + aCollector, aTextList, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS); + if (isText) { + return; + } + aTextOnly = true; + // We just get the text when calling GetAllInFlowRectsAndTexts, so we + // don't need to call it again when visiting the children. + aTextList = nullptr; } - } else if (nsIFrame* frame = content->GetPrimaryFrame()) { - nsLayoutUtils::GetAllInFlowRectsAndTexts( - frame, nsLayoutUtils::GetContainingBlockForClientRect(frame), - aCollector, aTextList, nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS); + } else if (!content->IsElement() || + !content->AsElement()->IsDisplayContents()) { + return; + } + + FlattenedChildIterator childIter(content); + for (nsIContent* child = childIter.GetNextChild(); child; + child = childIter.GetNextChild()) { + CollectClientRectsForSubtree(child, aCollector, aTextList, aStartContainer, + aStartOffset, aEndContainer, aEndOffset, + aClampToEdge, aFlushLayout, aTextOnly); } } @@ -2745,7 +2953,7 @@ void nsRange::CollectClientRectsAndText( CollectClientRectsForSubtree(node, aCollector, aTextList, aStartContainer, aStartOffset, aEndContainer, aEndOffset, - aClampToEdge, aFlushLayout); + aClampToEdge, aFlushLayout, false); } while (!iter.IsDone()); } @@ -2994,7 +3202,7 @@ void nsRange::ExcludeNonSelectableNodes(nsTArray<RefPtr<nsRange>>* aOutRanges) { // This is the initial range and all its nodes until now are // non-selectable so just trim them from the start. IgnoredErrorResult err; - range->SetStartBefore(*node, err); + range->SetStartBefore(*node, err, AllowRangeCrossShadowBoundary::Yes); if (err.Failed()) { return; } @@ -3008,7 +3216,8 @@ void nsRange::ExcludeNonSelectableNodes(nsTArray<RefPtr<nsRange>>* aOutRanges) { // Truncate the current range before the first non-selectable node. IgnoredErrorResult err; - range->SetEndBefore(*firstNonSelectableContent, err); + range->SetEndBefore(*firstNonSelectableContent, err, + AllowRangeCrossShadowBoundary::Yes); // Store it in the result (strong ref) - do this before creating // a new range in |newRange| below so we don't drop the last ref @@ -3252,3 +3461,52 @@ void nsRange::GetInnerTextNoFlush(DOMString& aValue, ErrorResult& aError, // Do not flush trailing line breaks! Required breaks at the end of the text // are suppressed. } + +template <typename SPT, typename SRT, typename EPT, typename ERT> +void nsRange::CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + const mozilla::RangeBoundaryBase<SPT, SRT>& aStartBoundary, + const mozilla::RangeBoundaryBase<EPT, ERT>& aEndBoundary) { + if (!StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()) { + return; + } + + MOZ_ASSERT(aStartBoundary.IsSetAndValid() && aEndBoundary.IsSetAndValid()); + + nsINode* startNode = aStartBoundary.Container(); + nsINode* endNode = aEndBoundary.Container(); + + if (!startNode && !endNode) { + ResetCrossShadowBoundaryRange(); + return; + } + + auto CanBecomeCrossShadowBoundaryPoint = [](nsINode* aContainer) -> bool { + if (!aContainer) { + return true; + } + + // Unlike normal ranges, shadow cross ranges don't work + // when the nodes aren't in document. + if (!aContainer->IsInComposedDoc()) { + return false; + } + + // AbstractRange::GetClosestCommonInclusiveAncestor only supports + // Document and Content nodes. + return aContainer->IsDocument() || aContainer->IsContent(); + }; + + if (!CanBecomeCrossShadowBoundaryPoint(startNode) || + !CanBecomeCrossShadowBoundaryPoint(endNode)) { + ResetCrossShadowBoundaryRange(); + return; + } + + if (!mCrossShadowBoundaryRange) { + mCrossShadowBoundaryRange = + StaticRange::Create(aStartBoundary, aEndBoundary, IgnoreErrors()); + return; + } + + mCrossShadowBoundaryRange->SetStartAndEnd(aStartBoundary, aEndBoundary); +} diff --git a/dom/base/nsRange.h b/dom/base/nsRange.h index 97756d3afc..94459087cb 100644 --- a/dom/base/nsRange.h +++ b/dom/base/nsRange.h @@ -13,6 +13,7 @@ #include "nsCOMPtr.h" #include "mozilla/dom/AbstractRange.h" +#include "mozilla/dom/StaticRange.h" #include "prmon.h" #include "nsStubMutationObserver.h" #include "nsWrapperCache.h" @@ -31,6 +32,13 @@ class DOMRect; class DOMRectList; class InspectorFontFace; class Selection; + +enum class CollapsePolicy : uint8_t { + No, // Don't need to collapse + DefaultRange, // Collapse the default range + DefaultRangeAndCrossShadowBoundaryRanges // Collapse both the default range + // and the cross boundary range +}; } // namespace dom } // namespace mozilla @@ -43,6 +51,8 @@ class nsRange final : public mozilla::dom::AbstractRange, using DOMRectList = mozilla::dom::DOMRectList; using RangeBoundary = mozilla::RangeBoundary; using RawRangeBoundary = mozilla::RawRangeBoundary; + using AllowRangeCrossShadowBoundary = + mozilla::dom::AllowRangeCrossShadowBoundary; virtual ~nsRange(); explicit nsRange(nsINode* aNode); @@ -111,14 +121,20 @@ class nsRange final : public mozilla::dom::AbstractRange, * When you set both start and end of a range, you should use * SetStartAndEnd() instead. */ - nsresult SetStart(nsINode* aContainer, uint32_t aOffset) { + nsresult SetStart(nsINode* aContainer, uint32_t aOffset, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No) { ErrorResult error; - SetStart(RawRangeBoundary(aContainer, aOffset), error); + SetStart(RawRangeBoundary(aContainer, aOffset), error, + aAllowCrossShadowBoundary); return error.StealNSResult(); } - nsresult SetEnd(nsINode* aContainer, uint32_t aOffset) { + nsresult SetEnd(nsINode* aContainer, uint32_t aOffset, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No) { ErrorResult error; - SetEnd(RawRangeBoundary(aContainer, aOffset), error); + SetEnd(RawRangeBoundary(aContainer, aOffset), error, + aAllowCrossShadowBoundary); return error.StealNSResult(); } @@ -224,6 +240,11 @@ class nsRange final : public mozilla::dom::AbstractRange, void SetStartAfterJS(nsINode& aNode, ErrorResult& aErr); void SetStartBeforeJS(nsINode& aNode, ErrorResult& aErr); + void SetStartAllowCrossShadowBoundary(nsINode& aNode, uint32_t aOffset, + ErrorResult& aErr); + void SetEndAllowCrossShadowBoundary(nsINode& aNode, uint32_t aOffset, + ErrorResult& aErr); + void SurroundContents(nsINode& aNode, ErrorResult& aErr); already_AddRefed<DOMRect> GetBoundingClientRect(bool aClampToEdge = true, bool aFlushLayout = true); @@ -235,14 +256,26 @@ class nsRange final : public mozilla::dom::AbstractRange, // Following methods should be used for internal use instead of *JS(). void SelectNode(nsINode& aNode, ErrorResult& aErr); void SelectNodeContents(nsINode& aNode, ErrorResult& aErr); - void SetEnd(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr); - void SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aErr); + void SetEnd(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No); + void SetEnd(const RawRangeBoundary& aPoint, ErrorResult& aErr, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No); void SetEndAfter(nsINode& aNode, ErrorResult& aErr); - void SetEndBefore(nsINode& aNode, ErrorResult& aErr); - void SetStart(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr); - void SetStart(const RawRangeBoundary& aPoint, ErrorResult& aErr); + void SetEndBefore(nsINode& aNode, ErrorResult& aErr, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No); + void SetStart(nsINode& aNode, uint32_t aOffset, ErrorResult& aErr, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No); + void SetStart(const RawRangeBoundary& aPoint, ErrorResult& aErr, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No); void SetStartAfter(nsINode& aNode, ErrorResult& aErr); - void SetStartBefore(nsINode& aNode, ErrorResult& aErr); + void SetStartBefore(nsINode& aNode, ErrorResult& aErr, + AllowRangeCrossShadowBoundary aAllowCrossShadowBoundary = + AllowRangeCrossShadowBoundary::No); void Collapse(bool aToStart); static void GetInnerTextNoFlush(mozilla::dom::DOMString& aValue, @@ -351,6 +384,82 @@ class nsRange final : public mozilla::dom::AbstractRange, */ nsINode* GetRegisteredClosestCommonInclusiveAncestor(); + template <typename SPT, typename SRT, typename EPT, typename ERT> + void CreateOrUpdateCrossShadowBoundaryRangeIfNeeded( + const mozilla::RangeBoundaryBase<SPT, SRT>& aStartBoundary, + const mozilla::RangeBoundaryBase<EPT, ERT>& aEndBoundary); + + void ResetCrossShadowBoundaryRange() { mCrossShadowBoundaryRange = nullptr; } + +#ifdef DEBUG + bool CrossShadowBoundaryRangeCollapsed() const { + MOZ_ASSERT(mCrossShadowBoundaryRange); + + return !mCrossShadowBoundaryRange->IsPositioned() || + (mCrossShadowBoundaryRange->GetStartContainer() == + mCrossShadowBoundaryRange->GetEndContainer() && + mCrossShadowBoundaryRange->StartOffset() == + mCrossShadowBoundaryRange->EndOffset()); + } +#endif + + /* + * The methods marked with MayCrossShadowBoundary[..] additionally check for + * the existence of mCrossShadowBoundaryRange, which indicates a range that + * crosses a shadow DOM boundary (i.e. mStart and mEnd are in different + * trees). If the caller can guarantee that this does not happen, there are + * additional variants of these methods named without MayCrossShadowBoundary, + * which provide a slightly faster implementation. + * */ + + nsIContent* GetMayCrossShadowBoundaryChildAtStartOffset() const { + return mCrossShadowBoundaryRange + ? mCrossShadowBoundaryRange->GetChildAtStartOffset() + : mStart.GetChildAtOffset(); + } + + nsIContent* GetMayCrossShadowBoundaryChildAtEndOffset() const { + return mCrossShadowBoundaryRange + ? mCrossShadowBoundaryRange->GetChildAtEndOffset() + : mEnd.GetChildAtOffset(); + } + + mozilla::dom::StaticRange* GetCrossShadowBoundaryRange() const { + return mCrossShadowBoundaryRange; + } + + nsINode* GetMayCrossShadowBoundaryStartContainer() const { + return mCrossShadowBoundaryRange + ? mCrossShadowBoundaryRange->GetStartContainer() + : mStart.Container(); + } + + nsINode* GetMayCrossShadowBoundaryEndContainer() const { + return mCrossShadowBoundaryRange + ? mCrossShadowBoundaryRange->GetEndContainer() + : mEnd.Container(); + } + + uint32_t MayCrossShadowBoundaryStartOffset() const { + return mCrossShadowBoundaryRange ? mCrossShadowBoundaryRange->StartOffset() + : StartOffset(); + } + + uint32_t MayCrossShadowBoundaryEndOffset() const { + return mCrossShadowBoundaryRange ? mCrossShadowBoundaryRange->EndOffset() + : EndOffset(); + } + + const RangeBoundary& MayCrossShadowBoundaryStartRef() const { + return mCrossShadowBoundaryRange ? mCrossShadowBoundaryRange->StartRef() + : StartRef(); + } + + const RangeBoundary& MayCrossShadowBoundaryEndRef() const { + return mCrossShadowBoundaryRange ? mCrossShadowBoundaryRange->EndRef() + : EndRef(); + } + protected: /** * DoSetRange() is called when `AbstractRange::SetStartAndEndInternal()` sets @@ -372,7 +481,9 @@ class nsRange final : public mozilla::dom::AbstractRange, MOZ_CAN_RUN_SCRIPT_BOUNDARY void DoSetRange( const mozilla::RangeBoundaryBase<SPT, SRT>& aStartBoundary, const mozilla::RangeBoundaryBase<EPT, ERT>& aEndBoundary, - nsINode* aRootNode, bool aNotInsertedYet = false); + nsINode* aRootNode, bool aNotInsertedYet = false, + mozilla::dom::CollapsePolicy aCollapsePolicy = mozilla::dom:: + CollapsePolicy::DefaultRangeAndCrossShadowBoundaryRanges); // Assume that this is guaranteed that this is held by the caller when // this is used. (Note that we cannot use AutoRestore for mCalledByJS @@ -424,6 +535,22 @@ class nsRange final : public mozilla::dom::AbstractRange, static nsTArray<RefPtr<nsRange>>* sCachedRanges; + // Used to keep track of the real start and end for a + // selection where the start and the end are in different trees. + // It's NULL when the nodes are in the same tree. + // + // mCrossShadowBoundaryRange doesn't deal with DOM mutations, because + // it's still an open question about how it should be handled. + // Spec: https://github.com/w3c/selection-api/issues/168. + // As a result, it'll be set to NULL if that happens. + // + // Theoretically, mCrossShadowBoundaryRange isn't really needed because + // we should be able to always store the real start and end, and + // just return one point when a collapse is needed. + // Bug https://bugzilla.mozilla.org/show_bug.cgi?id=1886028 is going + // to be used to improve mCrossShadowBoundaryRange. + RefPtr<mozilla::dom::StaticRange> mCrossShadowBoundaryRange; + friend class mozilla::dom::AbstractRange; }; namespace mozilla::dom { diff --git a/dom/base/nsWrapperCache.h b/dom/base/nsWrapperCache.h index f1c4e17fa3..4410b64ddf 100644 --- a/dom/base/nsWrapperCache.h +++ b/dom/base/nsWrapperCache.h @@ -12,6 +12,7 @@ #include "mozilla/ServoUtils.h" #include "mozilla/RustCell.h" #include "js/HeapAPI.h" +#include "js/RootingAPI.h" #include "js/TracingAPI.h" #include "js/TypeDecls.h" #include "nsISupports.h" @@ -196,6 +197,11 @@ class JS_HAZ_ROOTED nsWrapperCache { if (mWrapper) { MOZ_ASSERT(mWrapper == aOldObject); mWrapper = aNewObject; + if (PreservingWrapper() && !JS::ObjectIsTenured(mWrapper)) { + // Can pass prevp as null here since a previous store buffer entry has + // been cleared by the current nursery collection. + JS::HeapObjectPostWriteBarrier(&mWrapper, nullptr, mWrapper); + } } } diff --git a/dom/base/rust/lib.rs b/dom/base/rust/lib.rs index 1c155e062b..467eeeb832 100644 --- a/dom/base/rust/lib.rs +++ b/dom/base/rust/lib.rs @@ -156,8 +156,6 @@ bitflags! { const RTL_LOCALE = 1 << 1; /// LTR locale: specific to the XUL localedir attribute const LTR_LOCALE = 1 << 2; - /// LWTheme status - const LWTHEME = 1 << 3; const ALL_LOCALEDIR_BITS = Self::LTR_LOCALE.bits() | Self::RTL_LOCALE.bits(); } diff --git a/dom/base/test/browser.toml b/dom/base/test/browser.toml index a68bd2e873..04eca5d1aa 100644 --- a/dom/base/test/browser.toml +++ b/dom/base/test/browser.toml @@ -104,7 +104,7 @@ skip-if = ["fission"] # Fails with Fission, and we're unlikely to spend time to ["browser_multiple_popups.js"] skip-if = [ "os == 'win' && !debug", # Bug 1505235 - "os == 'mac' && !debug", # Bug 1661132 (osx) + "apple_catalina", # Bug 1661132 (osx), Bug 1866073 "socketprocess_networking", ] support-files = ["browser_multiple_popups.html"] diff --git a/dom/base/test/browser_aboutnewtab_process_selection.js b/dom/base/test/browser_aboutnewtab_process_selection.js index ad59077105..db0e80acdf 100644 --- a/dom/base/test/browser_aboutnewtab_process_selection.js +++ b/dom/base/test/browser_aboutnewtab_process_selection.js @@ -33,7 +33,7 @@ add_task(async function () { // Open 3 tabs using the preloaded browser. let tabs = []; for (let i = 0; i < 3; i++) { - BrowserOpenTab(); + BrowserCommands.openTab(); tabs.unshift(gBrowser.selectedTab); await BrowserTestUtils.maybeCreatePreloadedBrowser(gBrowser); @@ -114,7 +114,7 @@ add_task(async function preloaded_state_attribute() { "Sanity check that the first preloaded browser has the correct attribute" ); - BrowserOpenTab(); + BrowserCommands.openTab(); await BrowserTestUtils.maybeCreatePreloadedBrowser(gBrowser); // Now check that the tabs have the correct browser attributes set diff --git a/dom/base/test/file_window_close.html b/dom/base/test/file_window_close.html index 5adec04ec4..b3bb20d499 100644 --- a/dom/base/test/file_window_close.html +++ b/dom/base/test/file_window_close.html @@ -13,7 +13,7 @@ if (history.length == 4) { // We're coming back from history. function listener(m) { - if (m.message.includes("Scripts may not close windows that were not opened by script.")) { + if (m.message.includes("Scripts may only close windows that were opened by a script.")) { SpecialPowers.postConsoleSentinel(); SpecialPowers.pushPrefEnv({ set: [["dom.allow_scripts_to_close_windows", true]]}).then( function() { diff --git a/dom/base/test/fullscreen/browser_fullscreen-bug-1798219.js b/dom/base/test/fullscreen/browser_fullscreen-bug-1798219.js index 2aef23b042..6764ff2009 100644 --- a/dom/base/test/fullscreen/browser_fullscreen-bug-1798219.js +++ b/dom/base/test/fullscreen/browser_fullscreen-bug-1798219.js @@ -52,7 +52,7 @@ async function waitAndCheckFullscreenState(aWindow) { add_task(async () => { const URL = - "http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-bug-1798219.html"; + "https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-bug-1798219.html"; // We need this dummy tab which load the same URL as test tab to keep the // original content process alive after test page navigates away. let dummyTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL); @@ -90,7 +90,7 @@ add_task(async () => { await BrowserTestUtils.withNewTab( { gBrowser, - url: "http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-bug-1798219-2.html", + url: "https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-bug-1798219-2.html", }, async function (browser) { // Open a new window to run the tests, the original window will keep the diff --git a/dom/base/test/fullscreen/browser_fullscreen-navigation-history-race.js b/dom/base/test/fullscreen/browser_fullscreen-navigation-history-race.js index 49a48c3177..ab1651fd74 100644 --- a/dom/base/test/fullscreen/browser_fullscreen-navigation-history-race.js +++ b/dom/base/test/fullscreen/browser_fullscreen-navigation-history-race.js @@ -54,7 +54,7 @@ function preventBFCache(aBrowsingContext, aPrevent) { await BrowserTestUtils.withNewTab( { gBrowser, - url: "http://mochi.test:8888/browser/dom/base/test/fullscreen/dummy_page.html", + url: "https://example.com/browser/dom/base/test/fullscreen/dummy_page.html", }, async function (browser) { // Maybe prevent BFCache on initial page. @@ -66,7 +66,7 @@ function preventBFCache(aBrowsingContext, aPrevent) { // Navigate to fullscreen page. const url = crossOrigin ? "https://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html" - : "http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html"; + : "https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html"; const loaded = BrowserTestUtils.browserLoaded(browser, false, url); BrowserTestUtils.startLoadingURIString(browser, url); await loaded; diff --git a/dom/base/test/fullscreen/browser_fullscreen-navigation-history.js b/dom/base/test/fullscreen/browser_fullscreen-navigation-history.js index c4feb7f641..1b344b7a2f 100644 --- a/dom/base/test/fullscreen/browser_fullscreen-navigation-history.js +++ b/dom/base/test/fullscreen/browser_fullscreen-navigation-history.js @@ -54,7 +54,7 @@ function preventBFCache(aBrowsingContext, aPrevent) { await BrowserTestUtils.withNewTab( { gBrowser, - url: "http://mochi.test:8888/browser/dom/base/test/fullscreen/dummy_page.html", + url: "https://example.com/browser/dom/base/test/fullscreen/dummy_page.html", }, async function (browser) { // Maybe prevent BFCache on initial page. @@ -66,7 +66,7 @@ function preventBFCache(aBrowsingContext, aPrevent) { // Navigate to fullscreen page. const url = crossOrigin ? "https://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html" - : "http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html"; + : "https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html"; const loaded = BrowserTestUtils.browserLoaded(browser, false, url); BrowserTestUtils.startLoadingURIString(browser, url); await loaded; diff --git a/dom/base/test/fullscreen/browser_fullscreen-window-open-race.js b/dom/base/test/fullscreen/browser_fullscreen-window-open-race.js index 4cf8a3d8c7..95668db542 100644 --- a/dom/base/test/fullscreen/browser_fullscreen-window-open-race.js +++ b/dom/base/test/fullscreen/browser_fullscreen-window-open-race.js @@ -27,7 +27,7 @@ add_setup(async function () { add_task(async () => { const url = - "http://mochi.test:8888/browser/dom/base/test/fullscreen/dummy_page.html"; + "https://example.com/browser/dom/base/test/fullscreen/dummy_page.html"; const name = "foo"; await BrowserTestUtils.withNewTab( diff --git a/dom/base/test/fullscreen/file_MozDomFullscreen.html b/dom/base/test/fullscreen/file_MozDomFullscreen.html index f954892706..600d335501 100644 --- a/dom/base/test/fullscreen/file_MozDomFullscreen.html +++ b/dom/base/test/fullscreen/file_MozDomFullscreen.html @@ -3,6 +3,6 @@ </head> <body style="background-color: blue;"> <p>Outer doc</p> -<iframe id="innerFrame" src="http://mochi.test:8888/"></iframe> +<iframe id="innerFrame" src="https://example.com/"></iframe> </body> </html> diff --git a/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html b/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html index b60dea43bf..35b07cfa11 100644 --- a/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html +++ b/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html @@ -1,5 +1,5 @@ <div name="div" id="div" style="width: 100px; height: 100px; background: blue;"> <iframe id="iframe" allowfullscreen="yes" - src="http://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html"> + src="https://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-inner.html"> </iframe> </div><br> diff --git a/dom/base/test/fullscreen/file_fullscreen-iframe-top.html b/dom/base/test/fullscreen/file_fullscreen-iframe-top.html index dddf4930c2..b61cdc02c3 100644 --- a/dom/base/test/fullscreen/file_fullscreen-iframe-top.html +++ b/dom/base/test/fullscreen/file_fullscreen-iframe-top.html @@ -1,5 +1,5 @@ <div name="div" id="div" style="width: 100px; height: 100px; background: red;"> <iframe id="iframe" allowfullscreen="yes" - src="http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html"> + src="https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html"> </iframe> </div><br> diff --git a/dom/base/test/fullscreen/fullscreen_helpers.js b/dom/base/test/fullscreen/fullscreen_helpers.js index f097ae316e..41548cf8aa 100644 --- a/dom/base/test/fullscreen/fullscreen_helpers.js +++ b/dom/base/test/fullscreen/fullscreen_helpers.js @@ -8,15 +8,14 @@ const TEST_URLS = [ `data:text/html, <div name="div" id="div" style="width: 100px; height: 100px; background: red;"> <iframe id="iframe" allowfullscreen="yes" - src="http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html"></iframe> + src="https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-middle.html"></iframe> </div>`, // toplevel and inner most iframe are in same process, and middle iframe is // in a different process. - // eslint-disable-next-line @microsoft/sdl/no-insecure-url - `http://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-top.html`, + `https://example.org/browser/dom/base/test/fullscreen/file_fullscreen-iframe-top.html`, // toplevel and middle iframe are in same process, and inner most iframe is // in a different process. - `http://mochi.test:8888/browser/dom/base/test/fullscreen/file_fullscreen-iframe-top.html`, + `https://example.com/browser/dom/base/test/fullscreen/file_fullscreen-iframe-top.html`, ]; function waitRemoteFullscreenExitEvents(aBrowsingContexts) { diff --git a/dom/base/test/fullscreen/test_MozDomFullscreen_event.xhtml b/dom/base/test/fullscreen/test_MozDomFullscreen_event.xhtml index 3041d851ac..6b3d8fada2 100644 --- a/dom/base/test/fullscreen/test_MozDomFullscreen_event.xhtml +++ b/dom/base/test/fullscreen/test_MozDomFullscreen_event.xhtml @@ -17,7 +17,7 @@ var gPrevTrusted = SpecialPowers.getBoolPref("full-screen-api.allow-trusted-requ var newwindow; // Ensure "fullscreen" permissions are not present on the test URI. -var uri = Services.io.newURI("http://mochi.test:8888"); +var uri = Services.io.newURI("https://example.com"); var principal = Services.scriptSecurityManager.createContentPrincipal(uri, {}); Services.perms.removeFromPrincipal(principal, "fullscreen"); diff --git a/dom/base/test/jsmodules/chrome.toml b/dom/base/test/jsmodules/chrome.toml index 82d02ad4df..8b8a614bfb 100644 --- a/dom/base/test/jsmodules/chrome.toml +++ b/dom/base/test/jsmodules/chrome.toml @@ -3,11 +3,21 @@ support-files = [ "ambiguous_export.mjs", "import_ambiguous.mjs", "import_ambiguous_indirect_export.mjs", + "import_ambiguous_export.mjs", + "import_ambiguous_export_star.mjs", + "import_circular.mjs", + "import_circular_1.mjs", "import_no_export.mjs", "import_no_indirect_export.mjs", "exportA1.mjs", "exportA2.mjs", "export_ambiguous.mjs", + "export_star_ambiguous.mjs", + "module_a.mjs", + "module_b.mjs", + "module_c.mjs", + "module_d.mjs", + "module_e.mjs", "module_setRan.mjs", "module_testSyntax.mjs", "module_badSyntax.mjs", @@ -47,6 +57,8 @@ support-files = [ ["test_import_errorMessage.html"] +["test_import_errorMessage2.html"] + ["test_import_meta_resolve.html"] ["test_importedModuleMemoization.html"] diff --git a/dom/base/test/jsmodules/export_star_ambiguous.mjs b/dom/base/test/jsmodules/export_star_ambiguous.mjs new file mode 100644 index 0000000000..35cc979dee --- /dev/null +++ b/dom/base/test/jsmodules/export_star_ambiguous.mjs @@ -0,0 +1 @@ +export * from "./ambiguous_export.mjs"; diff --git a/dom/base/test/jsmodules/import_ambiguous_export.mjs b/dom/base/test/jsmodules/import_ambiguous_export.mjs new file mode 100644 index 0000000000..f5c12ff086 --- /dev/null +++ b/dom/base/test/jsmodules/import_ambiguous_export.mjs @@ -0,0 +1 @@ +import { a } from "./ambiguous_export.mjs"; diff --git a/dom/base/test/jsmodules/import_ambiguous_export_star.mjs b/dom/base/test/jsmodules/import_ambiguous_export_star.mjs new file mode 100644 index 0000000000..1ee2a56d37 --- /dev/null +++ b/dom/base/test/jsmodules/import_ambiguous_export_star.mjs @@ -0,0 +1 @@ +import { a } from "./export_star_ambiguous.mjs"; diff --git a/dom/base/test/jsmodules/import_circular.mjs b/dom/base/test/jsmodules/import_circular.mjs new file mode 100644 index 0000000000..bb3a46bd5e --- /dev/null +++ b/dom/base/test/jsmodules/import_circular.mjs @@ -0,0 +1 @@ +export { a } from "./import_circular_1.mjs"; diff --git a/dom/base/test/jsmodules/import_circular_1.mjs b/dom/base/test/jsmodules/import_circular_1.mjs new file mode 100644 index 0000000000..eb7c038c77 --- /dev/null +++ b/dom/base/test/jsmodules/import_circular_1.mjs @@ -0,0 +1 @@ +export { a } from "./import_circular.mjs"; diff --git a/dom/base/test/jsmodules/import_no_export.mjs b/dom/base/test/jsmodules/import_no_export.mjs index 47cabac557..d989498c88 100644 --- a/dom/base/test/jsmodules/import_no_export.mjs +++ b/dom/base/test/jsmodules/import_no_export.mjs @@ -1 +1 @@ -import x from "./no_export.mjs"; +import { x } from "./no_export.mjs"; diff --git a/dom/base/test/jsmodules/import_no_indirect_export.mjs b/dom/base/test/jsmodules/import_no_indirect_export.mjs index dd1ca847fc..bfcdcedbc5 100644 --- a/dom/base/test/jsmodules/import_no_indirect_export.mjs +++ b/dom/base/test/jsmodules/import_no_indirect_export.mjs @@ -1,2 +1,2 @@ /* eslint-disable import/default */ -import x from "./no_indirect_export.mjs"; +import { a } from "./no_indirect_export.mjs"; diff --git a/dom/base/test/jsmodules/importmaps/classic_script.js b/dom/base/test/jsmodules/importmaps/classic_script.js new file mode 100644 index 0000000000..d7ae0be054 --- /dev/null +++ b/dom/base/test/jsmodules/importmaps/classic_script.js @@ -0,0 +1 @@ +// Empty script. diff --git a/dom/base/test/jsmodules/importmaps/mochitest.toml b/dom/base/test/jsmodules/importmaps/mochitest.toml index 4229455722..1f95b155ac 100644 --- a/dom/base/test/jsmodules/importmaps/mochitest.toml +++ b/dom/base/test/jsmodules/importmaps/mochitest.toml @@ -3,6 +3,9 @@ support-files = [ "bug_1865410_module_a.mjs", "bug_1865410_module_b.mjs", "bug_1873417.mjs", + "classic_script.js", + "module_chain_1.mjs", + "module_chain_2.mjs", "module_importMap_with_external_script_0.mjs", "module_importMap_with_external_script_1.mjs", "module_importMap_with_external_script_2.mjs", @@ -13,6 +16,7 @@ support-files = [ "module_importMap_with_external_script_6.mjs", "module_importMap_with_external_script_6.mjs^headers^", "module_importMap_with_external_script_7.mjs", + "module_importMap_with_nonexisting_module.mjs", "bad/module_2.mjs", "bad/module_3.mjs", "bad/module_4.mjs", @@ -31,3 +35,6 @@ support-files = [ ["test_bug_1873417.html"] ["test_importMap_with_external_script.html"] +["test_importMap_with_nonexisting_module.html"] +["test_dynamic_importMap_with_external_script.html"] +["test_dynamic_importMap_load_completes.html"] diff --git a/dom/base/test/jsmodules/importmaps/module_chain_1.mjs b/dom/base/test/jsmodules/importmaps/module_chain_1.mjs new file mode 100644 index 0000000000..d9515fab7f --- /dev/null +++ b/dom/base/test/jsmodules/importmaps/module_chain_1.mjs @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/no-unassigned-import +import {} from "./module_chain_2.mjs"; diff --git a/dom/base/test/jsmodules/importmaps/module_chain_2.mjs b/dom/base/test/jsmodules/importmaps/module_chain_2.mjs new file mode 100644 index 0000000000..ce12406a76 --- /dev/null +++ b/dom/base/test/jsmodules/importmaps/module_chain_2.mjs @@ -0,0 +1 @@ +loaded = true; diff --git a/dom/base/test/jsmodules/importmaps/module_importMap_with_nonexisting_module.mjs b/dom/base/test/jsmodules/importmaps/module_importMap_with_nonexisting_module.mjs new file mode 100644 index 0000000000..4f9981bbe3 --- /dev/null +++ b/dom/base/test/jsmodules/importmaps/module_importMap_with_nonexisting_module.mjs @@ -0,0 +1,4 @@ +/* eslint-disable import/no-unassigned-import, import/no-unresolved */ +// Bareword specifier should be mapped to ./good/module_0.mjs. +import {} from "bare"; +import * as test from "nonexistingmodule"; diff --git a/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_load_completes.html b/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_load_completes.html new file mode 100644 index 0000000000..da354c1ca0 --- /dev/null +++ b/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_load_completes.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> +<title>Test script loading complets when there's a dynamicly inserted import map</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<!-- + This test case used to fail intermittently with some modules never + being loaded. +--> + +<script> + let loaded = false; // Set by module_chain_2.mjs. +</script> + +<script src="classic_script.js"></script> + +<script> + const script = document.createElement('script'); + script.type = 'importmap'; + script.textContent = `{}`; + document.head.appendChild(script); +</script> + +<script src="module_chain_1.mjs" type="module"></script> + +<script type="module"> + ok(loaded, "Expected all modules loaded"); +</script> + +<body></body> diff --git a/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_with_external_script.html b/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_with_external_script.html new file mode 100644 index 0000000000..b78992fb87 --- /dev/null +++ b/dom/base/test/jsmodules/importmaps/test_dynamic_importMap_with_external_script.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> +<title>Test speculative preload of external script doesn't conflict with import map</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<!-- + These tests check that speculative preloading, which could happen before + the import map is installed, doesn't load the wrong modules. This version + dynamically inserts the import map script element after speculative + preloading has started. +--> + +<script> + const script = document.createElement('script'); + script.type = 'importmap'; + script.textContent = + `{ + "imports": { + "bare": "./good/module_0.mjs", + "./bad/module_1.mjs": "./good/module_1.mjs", + "./bad/module_2.mjs": "./good/module_2.mjs", + "./bad/module_3.mjs": "./good/module_3.mjs", + "./bad/module_4.mjs": "./good/module_4.mjs", + "./bad/module_7.mjs": "./good/module_7.mjs" + } + }`; + document.head.appendChild(script); +</script> + +<!-- +Test bareword import (not supported before import map installed). +--> +<script type="module" src="module_importMap_with_external_script_0.mjs"></script> + +<!-- +Test mapping from missing resource to existing resource (not found before +import map installed). +--> +<script type="module" src="module_importMap_with_external_script_1.mjs"></script> + +<!-- +Test mapping from one existing resource to another (would load wrong resource before +import map installed). +--> +<script type="module" src="module_importMap_with_external_script_2.mjs"></script> + +<!-- +Test mapping from one existing resource to another with circular dependency. +--> +<script type="module" src="module_importMap_with_external_script_3.mjs"></script> + +<!-- +Test with redirect, script_6.mjs -> script_5.mjs -> script_4.mjs. +We redirect twice here, as sometimes one redirect can't reproduce the crash +from bug 1835468. +--> +<script type="module" src="module_importMap_with_external_script_6.mjs"></script> + +<!-- +Test with async attribute +--> +<script type="module" async src="module_importMap_with_external_script_7.mjs"></script> + +<script> + SimpleTest.waitForExplicitFinish(); + + let passCount = 0; + const expectedCount = 6; + + function success(name) { + ok(true, "Test passed, loaded " + name); + passCount++; + if (passCount == expectedCount) { + SimpleTest.finish(); + } + } +</script> +<body></body> diff --git a/dom/base/test/jsmodules/importmaps/test_importMap_with_nonexisting_module.html b/dom/base/test/jsmodules/importmaps/test_importMap_with_nonexisting_module.html new file mode 100644 index 0000000000..57cfd5e5a1 --- /dev/null +++ b/dom/base/test/jsmodules/importmaps/test_importMap_with_nonexisting_module.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test an import map with an nonexisting module specifier</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +<script type="importmap"> +{ + "imports": { + "bare": "./good/module_0.mjs" + } +} +</script> + + +<script> + SimpleTest.waitForExplicitFinish(); + + window.onerror = (event, src, lineno, colno, error) => { + ok(error instanceof TypeError, "Should be a TypeError"); + SimpleTest.finish(); + }; + +</script> +<script type="module" src="module_importMap_with_nonexisting_module.mjs"></script> diff --git a/dom/base/test/jsmodules/module_a.mjs b/dom/base/test/jsmodules/module_a.mjs new file mode 100644 index 0000000000..0b0c3304ff --- /dev/null +++ b/dom/base/test/jsmodules/module_a.mjs @@ -0,0 +1,2 @@ +export var a = 1; +export var b = 2; diff --git a/dom/base/test/jsmodules/module_b.mjs b/dom/base/test/jsmodules/module_b.mjs new file mode 100644 index 0000000000..79da84736d --- /dev/null +++ b/dom/base/test/jsmodules/module_b.mjs @@ -0,0 +1,2 @@ +export var b = 3; +export var c = 4; diff --git a/dom/base/test/jsmodules/module_c.mjs b/dom/base/test/jsmodules/module_c.mjs new file mode 100644 index 0000000000..5a2a7e3e09 --- /dev/null +++ b/dom/base/test/jsmodules/module_c.mjs @@ -0,0 +1,3 @@ +/* eslint-disable import/export */ +export * from "./module_a.mjs"; +export * from "./module_b.mjs"; diff --git a/dom/base/test/jsmodules/module_d.mjs b/dom/base/test/jsmodules/module_d.mjs new file mode 100644 index 0000000000..04dc02d27a --- /dev/null +++ b/dom/base/test/jsmodules/module_d.mjs @@ -0,0 +1 @@ +import { a } from "./module_c.mjs"; diff --git a/dom/base/test/jsmodules/module_e.mjs b/dom/base/test/jsmodules/module_e.mjs new file mode 100644 index 0000000000..544c424fcb --- /dev/null +++ b/dom/base/test/jsmodules/module_e.mjs @@ -0,0 +1 @@ +import { b } from "./module_c.mjs"; diff --git a/dom/base/test/jsmodules/test_import_errorMessage.html b/dom/base/test/jsmodules/test_import_errorMessage.html index 6ab0b1dd74..2330b46dd9 100644 --- a/dom/base/test/jsmodules/test_import_errorMessage.html +++ b/dom/base/test/jsmodules/test_import_errorMessage.html @@ -8,40 +8,38 @@ let count = 0; window.onerror = function (event, src, lineno, colno, error) { - info("window.onerror :" + error.message); + info("window.onerror: message: " + error.message); + info("window.onerror: src: " + src); ok(error instanceof SyntaxError, "Should be a SyntaxError."); - // import_no_indirect_export.mjs and import_ambiguous_indirect_export.mjs - // are related to indirect import/export. - if (count < 2) { - ok(error.message.match("indirect"), "Should contain 'indirect'"); - } - - // import_ambiguous_indirect_export.mjs and import_ambiguous.mjs both - // have ambiguous import/export. - if (count % 2 === 1) { - ok(error.message.match("ambiguous"), "Should contain 'ambiguous'"); - } - - if (count === 2) { - ok(!error.message.match("ambiguous") && !error.message.match("indirect"), - "Should NOT contain 'indirect' nor 'ambiguous'"); + if (src.match("no_indirect_export.mjs") || + src.match("import_no_export.mjs")) { + ok(error.message.match("doesn't provide an export named")); + } else if(src.match("export_ambiguous.mjs") || + src.match("import_ambiguous_export_star.mjs") || + src.match("import_ambiguous_export.mjs") || + src.match("import_ambiguous.mjs")) { + ok(error.message.match("contains ambiguous star export")); + } else if (src.match("import_circular_1.mjs")) { + ok(error.message.match("contains circular import")); + } else { + ok(false, "unknown src " + src); } count++; }; function testLoaded() { - ok(count === 4, "Should have 4 SynaxErrors thrown."); + ok(count === 7, "Should have 7 SynaxErrors thrown."); SimpleTest.finish(); } + </script> -<!-- -In window.onerror will test the error messages, so if the order is changed, -the code in window.onerror should be updated as well. ---> <script type="module" src="import_no_indirect_export.mjs"></script> <script type="module" src="import_ambiguous_indirect_export.mjs"></script> +<script type="module" src="import_ambiguous_export_star.mjs"></script> +<script type="module" src="import_ambiguous_export.mjs"></script> <script type="module" src="import_no_export.mjs"></script> <script type="module" src="import_ambiguous.mjs"></script> +<script type="module" src="import_circular.mjs"></script> <body onload='testLoaded()'></body> diff --git a/dom/base/test/jsmodules/test_import_errorMessage2.html b/dom/base/test/jsmodules/test_import_errorMessage2.html new file mode 100644 index 0000000000..ed4227362e --- /dev/null +++ b/dom/base/test/jsmodules/test_import_errorMessage2.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test to get the filename of the requested module after it has been evaluated</title> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script> + SimpleTest.waitForExplicitFinish(); + + let count = 0; + + window.onerror = function (event, src, lineno, colno, error) { + info("window.onerror: message: " + error.message); + info("window.onerror: src: " + src); + ok(error instanceof SyntaxError, "Should be a SyntaxError."); + + if (src.match("module_e.mjs")) { + ok(error.message.match("contains ambiguous star export")); + } else { + ok(false, "unknown src " + src); + } + count++; + }; + + function testLoaded() { + ok(count === 1, "Should have 1 SynaxError thrown."); + SimpleTest.finish(); + } + +</script> + +<!--module_c.mjs will be evaluated here--> +<script type="module" src="module_d.mjs"></script> + +<!--module_c.mjs will be linked again here--> +<script type="module" src="module_e.mjs"></script> +<body onload='testLoaded()'></body> diff --git a/dom/base/test/mochitest.toml b/dom/base/test/mochitest.toml index fb6724e497..a1e7b31a19 100644 --- a/dom/base/test/mochitest.toml +++ b/dom/base/test/mochitest.toml @@ -1170,6 +1170,8 @@ skip-if = [ ["test_content_iterator_subtree.html"] +["test_content_iterator_subtree_shadow_tree.html"] + ["test_copyimage.html"] skip-if = [ "os == 'android'", diff --git a/dom/base/test/test_bug564863-2.xhtml b/dom/base/test/test_bug564863-2.xhtml index 6f338f612d..c860a7f6d7 100644 --- a/dom/base/test/test_bug564863-2.xhtml +++ b/dom/base/test/test_bug564863-2.xhtml @@ -55,11 +55,11 @@ SimpleTest.waitForExplicitFinish(); // not when run as a Mochitest plain. //is(xul_cs.color, "rgb(0, 0, 0)", "xul color " + test); - attrValue = removed ? null : ""; + let attrValue = removed ? null : ""; is(xul.id, "", "xul id " + test); - is(xul.getAttribute("id"), "", "xul getAttribute " + test); + is(xul.getAttribute("id"), attrValue, "xul getAttribute " + test); is($("xul_id"), null, "xul getElementById " + test); } @@ -149,7 +149,7 @@ SimpleTest.waitForExplicitFinish(); await new Promise(SimpleTest.executeSoon); if (mutation) { is(mutation.target, xul, "target is xul"); - is(xul.getAttribute("id"), "", "xul no longer has id attr"); + is(xul.getAttribute("id"), null, "xul no longer has id attr"); is(xul.id, "", "xul no longer has id"); xul.id = "other_xul_id"; } else { diff --git a/dom/base/test/test_content_iterator_subtree_shadow_tree.html b/dom/base/test/test_content_iterator_subtree_shadow_tree.html new file mode 100644 index 0000000000..033aaf80a7 --- /dev/null +++ b/dom/base/test/test_content_iterator_subtree_shadow_tree.html @@ -0,0 +1,290 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test for content subtree iterator with ShadowDOM involved</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<script> +var Cc = SpecialPowers.Cc; +var Ci = SpecialPowers.Ci; +function finish() { + // The SimpleTest may require usual elements in the template, but they shouldn't be during test. + // So, let's create them at end of the test. + document.body.innerHTML = '<div id="display"></div><div id="content"></div><pre id="test"></pre>'; + SimpleTest.finish(); +} + +function createContentIterator() { + return Cc["@mozilla.org/scriptable-content-iterator;1"] + .createInstance(Ci.nsIScriptableContentIterator); +} + +function getNodeDescription(aNode) { + if (aNode === undefined) { + return "undefine"; + } + if (aNode === null) { + return "null"; + } + function getElementDescription(aElement) { + if (aElement.host) { + aElement = aElement.host; + } + if (aElement.tagName === "BR") { + if (aElement.previousSibling) { + return `<br> element after ${getNodeDescription(aElement.previousSibling)}`; + } + return `<br> element in ${getElementDescription(aElement.parentElement)}`; + } + let hasHint = aElement == document.body; + let tag = `<${aElement.tagName.toLowerCase()}`; + if (aElement.getAttribute("id")) { + tag += ` id="${aElement.getAttribute("id")}"`; + hasHint = true; + } + if (aElement.getAttribute("class")) { + tag += ` class="${aElement.getAttribute("class")}"`; + hasHint = true; + } + if (aElement.getAttribute("type")) { + tag += ` type="${aElement.getAttribute("type")}"`; + } + if (aElement.getAttribute("name")) { + tag += ` name="${aElement.getAttribute("name")}"`; + } + if (aElement.getAttribute("value")) { + tag += ` value="${aElement.getAttribute("value")}"`; + hasHint = true; + } + if (aElement.getAttribute("style")) { + tag += ` style="${aElement.getAttribute("style")}"`; + hasHint = true; + } + if (hasHint) { + return tag + ">"; + } + + return `${tag}> in ${getElementDescription(aElement.parentElement || aElement.parentNode)}`; + } + switch (aNode.nodeType) { + case aNode.TEXT_NODE: + return `text node, "${aNode.wholeText.replace(/\n/g, '\\n')}"`; + case aNode.COMMENT_NODE: + return `comment node, "${aNode.data.replace(/\n/g, '\\n')}"`; + case aNode.ELEMENT_NODE: + return getElementDescription(SpecialPowers.unwrap(aNode)); + default: + return "unknown node"; + } +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function () { + let iter = createContentIterator(); + + function runTest() { + /** + * Basic tests with complicated tree. + */ + function check(aIter, aExpectedResult, aDescription) { + if (aExpectedResult.length) { + is(SpecialPowers.unwrap(aIter.currentNode), aExpectedResult[0], + `${aDescription}: currentNode should be the text node immediately after initialization (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(aExpectedResult[0])})`); + ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true immediately after initialization`); + + aIter.first(); + is(SpecialPowers.unwrap(aIter.currentNode), aExpectedResult[0], + `${aDescription}: currentNode should be the text node after calling first() (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(aExpectedResult[0])})`); + ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true after calling first()`); + + for (let expected of aExpectedResult) { + is(SpecialPowers.unwrap(aIter.currentNode), expected, + `${aDescription}: currentNode should be the node (got: ${getNodeDescription(aIter.currentNode)}, expected: ${getNodeDescription(expected)})`); + ok(!aIter.isDone, `${aDescription}: isDone shouldn't be true when ${getNodeDescription(expected)} is expected`); + aIter.next(); + } + + is(SpecialPowers.unwrap(aIter.currentNode), null, + `${aDescription}: currentNode should be null after calling next() finally (got: ${getNodeDescription(aIter.currentNode)}`); + ok(aIter.isDone, `${aDescription}: isDone should be true after calling next() finally`); + } else { + is(SpecialPowers.unwrap(aIter.currentNode), null, + `${aDescription}: currentNode should be null immediately after initialization (got: ${getNodeDescription(aIter.currentNode)})`); + ok(aIter.isDone, `${aDescription}: isDone should be true immediately after initialization`); + + aIter.first(); + is(SpecialPowers.unwrap(aIter.currentNode), null, + `${aDescription}: currentNode should be null after calling first() (got: ${getNodeDescription(aIter.currentNode)})`); + ok(aIter.isDone, `${aDescription}: isDone should be true after calling first()`); + } + } + + // Structure + // <div>OuterText1</div> + // <div #host1> + // #ShadowRoot + // InnerText1 + // <div>OuterText2</div> + // <div #host2> + // #ShadowRoot + // <div>InnerText2</div> + // <div>InnerText3</div> + // <div #host3> + // #ShadowRoot + // <div #host4> + // #ShadowRoot + // InnerText4 + // OuterText3 + + document.body.innerHTML = `<div id="outerText1">OuterText1</div>` + + `<div id="host1"></div>` + + `<div id="outerText2">OuterText2</div>` + + `<div id="host2"></div>` + + `<div id="host3"></div>` + + `OuterText3`; + const outerText1 = document.getElementById("outerText1"); + const outerText2 = document.getElementById("outerText2"); + + const host1 = document.getElementById("host1"); + const root1 = host1.attachShadow({mode: "open"}); + root1.innerHTML = "InnerText1"; + + const host2 = document.getElementById("host2"); + const root2 = host2.attachShadow({mode: "open"}); + root2.innerHTML = "<div>InnerText2</div><div>InnerText3</div>"; + + const host3 = document.getElementById("host3"); + const root3 = host3.attachShadow({mode: "open"}); + root3.innerHTML = `<div id="host4"></div>`; + + const host4 = root3.getElementById("host4"); + const root4 = host4.attachShadow({mode: "open"}); + root4.innerHTML = "InnerText4"; + + /** + * Selects the <body> with a range. + */ + range = document.createRange(); + range.selectNode(document.body); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + check(iter, [document.body], "Initialized with range selecting the <body>"); + + /** + * Selects all children in the <body> with a range. + */ + range = document.createRange(); + range.selectNodeContents(document.body); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + check(iter, [outerText1, host1, + outerText2, host2, + host3, // host4 is a child of host3 + document.body.lastChild], + "Initialized with range selecting all children in the <body>"); + + /** + * range around elements. + */ + range = document.createRange(); + SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1.firstChild, 0); + SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root1.firstChild, root1.firstChild.length); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + // outerText1.firstChild is a node without children, so the + // next candidate is root1.firstChild, given root1.firstChild + // is also the end container which isn't fully contained + // by this range, so the iterator returns nothing. + check(iter, [], "Initialized with range selecting 'OuterText1 and InnerText1'"); + + // From light DOM to Shadow DOM #1 + range = document.createRange(); + SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1, 0); + SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root1.firstChild, root1.firstChild.length); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + // [start] outerText1 is a container and it has children, so the first node + // is the topmost descendant, which is outerText.firstChild. + // [end] The end point of this iteration is also outerText1.firstChild because + // it is also the topmost element in the previous node of root1.firstChild. + // Iteration #1: outerText1.firstChild as it is the start node + check(iter, [outerText1.firstChild], "Initialized with range selecting 'OuterText1 and InnerText1'"); + + // From light DOM to Shadow DOM #2 + SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1, 0); + SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root2, root2.childNodes.length); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + // [start] outerText1 is a container and it has children, so the first node + // is the topmost descendant, which is outerText.firstChild. + // [end] root2 is the container and it has children, so the end node is + // the last node of root2, which is root2.lastChild + // Iteration #1: outerText1.firstChild, as it's the start node + // Iteration #2: host1, as it's next available node after outerText1.firstChild + // Iteration #3: outerText2, as it's the next sibiling of host1 + // Iteration #4: host2, as it's the next sibling of outerText2. Since it's + // the ancestor of the end node, so we get into this tree and returns + // root2.firstChild here. + // Iteration #5: root2.lastChild, as it's the next sibling of root2.firstChild + check(iter, [outerText1.firstChild, host1, outerText2, root2.firstChild, root2.lastChild], + "Initialized with range selecting 'OuterText1, InnerText1, OuterText2 and InnerText2'"); + + // From Shadow DOM to Shadow DOM #1 + SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(root1.firstChild, 0); + SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root2.lastChild, root2.lastChild.length); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + // [start] outerText2 is the start because root1.firstChild doesn't have children, + // so we look for next available node which is outerText2. + // [end] root2.lastChild is the end container, so we look for previous + // nodes and get root2.firstChild + // Iteration #1: outerText2, as it's the start node + // Iteration #2: host2, as it's the next sibling of outerText2. Since it's + // the ancestor of the end node, so we get into this tree and returns + // root2.firstChild here. + check(iter, [outerText2, root2.firstChild], "Initialized with range selecting 'InnerText1, OuterText2 and InnerText2'"); + + // From Shadow DOM to Shadow DOM #2 + SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(root1, 0); + SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root2.lastChild, root2.lastChild.length); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + // [start] root1 is the start container and it has children, so the first node + // is the topmost descendant, which is root1.firstChild. + // [end] root2.lastChild is the end container, so we look for previous + // nodes and get root2.firstChild + // Iteration #1: root1.firstChild, as it's the start node + // Iteration #2: outerText2, as it's the next available node + // Iteration #3: host2, as it's the next sibling of outerText2. Since it's + // the ancestor of the end node, so we get into this tree and returns + // root2.firstChild here. + check(iter, [root1.firstChild, outerText2, root2.firstChild], "Initialized with range selecting 'InnerText1, OuterText2 and InnerText2'"); + + SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(root1.firstChild, 1); + SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(root4.firstChild, root4.firstChild.length); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + // [start] outerText2 is the start because root1.firstChild doesn't have children, + // so we look for next available node which is outerText2. + // [end] host2 is the end container, so we look for previous + // nodes root4.firstChild and eventually get host2. + // Iteration #1: outerText2, as it's the start node + // Iteration #2: host2, as it's the next sibling of outerText2 + check(iter, [outerText2, host2], "Initialized with range selecting 'InnerText1, OuterText2, InnerText2 and InnerText3'"); + + // From light to light + SpecialPowers.wrap(range).setStartAllowCrossShadowBoundary(outerText1.firstChild, 0); + SpecialPowers.wrap(range).setEndAllowCrossShadowBoundary(document.body.lastChild, document.body.lastChild.length); + iter.initWithRangeAllowCrossShadowBoundary(Ci.nsIScriptableContentIterator.SUBTREE_ITERATOR, range); + // [start] host1 is the start because it's the next available node of + // outerText1.firstChild. + // [end] host3 is the end because the previous node of document.body.lastChild is host3. + // Iteration #1: host1, as it's the start node + // Iteration #2: outerText2, as it's the next sibling of host1 + // Iteration #3: host2, as it's the next sibling of outerText2 + // Iteration #4: host3, as it's the next sibling of host2 + check(iter, [host1, outerText2, host2, host3], + "Initialized with range selecting 'OuterText1, InnerText1, OuterText2, InnerText2, InnerText3 and OuterText3'"); + + finish(); + } + + SpecialPowers.pushPrefEnv({"set": [["dom.shadowdom.selection_across_boundary.enabled", true]]}, runTest); +}); +</script> +</head> +<body></body> +</html> diff --git a/dom/base/test/test_embed_xorigin_document.html b/dom/base/test/test_embed_xorigin_document.html index 7d4c29aacf..4566d48534 100644 --- a/dom/base/test/test_embed_xorigin_document.html +++ b/dom/base/test/test_embed_xorigin_document.html @@ -12,11 +12,6 @@ const testPath = window.location.href.replace("http://mochi.test:8888", ""); const testDir = testPath.substring(0, testPath.lastIndexOf('/') + 1); add_task(async function() { - // FIXME: Remove when bug 1658342 is fixed - await SpecialPowers.pushPrefEnv({ - set: [["fission.remoteObjectEmbed", true]], - }); - info("Loading image in embed"); let embed = document.createElement("embed"); document.body.appendChild(embed); diff --git a/dom/base/test/test_range_bounds.html b/dom/base/test/test_range_bounds.html index 657d315198..dba687eead 100644 --- a/dom/base/test/test_range_bounds.html +++ b/dom/base/test/test_range_bounds.html @@ -38,6 +38,22 @@ function isEmptyRect(rect, name) { is(rect.height, 0, name+'empty rect should have height = 0'); } +function getTextBoundingClientRect(node) { + const quads = node.getBoxQuads()[0]; + return DOMRect.fromRect({ + x: quads.p1.x, + y: quads.p1.y, + width: quads.p2.x - quads.p1.x, + height: quads.p3.y - quads.p2.y + }); +} + +function sortRectList(rectlist) { + return Array.prototype.slice.call(rectlist, 0).sort(function(a, b) { + return a.top - b.top || a.left - b.left; + }); +} + function isEmptyRectList(rectlist, name) { name = annotateName(name); is(rectlist.length, 0, name + 'empty rectlist should have zero rects'); @@ -90,6 +106,9 @@ function runATest(obj) { //convert RectList to a real array obj.rectList=Array.prototype.slice.call(obj.rectList, 0); } + if (obj.mustSortBeforeComparing) { + rectlist = sortRectList(rectlist); + } obj.rectList.forEach(function(r,i) { is(_getRect(rectlist[i]),_getRect(r), annotateName(testname+": item at "+i)); @@ -109,11 +128,16 @@ function doTest(){ thirdDiv = root.childNodes[5]; var firstPRect = firstP.getBoundingClientRect(), spanInFirstPRect = spanInFirstP.getBoundingClientRect(), + textInFirstPRect = getTextBoundingClientRect(firstP.firstChild), + textInSpanInFirstPRect = getTextBoundingClientRect(spanInFirstP.firstChild), firstDivRect = firstDiv.getBoundingClientRect(), + textInFirstDivRect = getTextBoundingClientRect(firstDiv.firstChild), spanInFirstDivRect = spanInFirstDiv.getBoundingClientRect(), + textInSpanInFirstDivRect = getTextBoundingClientRect(spanInFirstDiv.firstChild), secondPRect = secondP.getBoundingClientRect(), secondDivRect = secondDiv.getBoundingClientRect(), spanInSecondPRect = spanInSecondP.getBoundingClientRect(), + textInSpanInSecondPRect = getTextBoundingClientRect(spanInSecondP.firstChild), spanInSecondDivRect = spanInSecondDiv.getBoundingClientRect(), spanInSecondDivRectList = spanInSecondDiv.getClientRects(); var widthPerchar = spanInSecondPRect.width / spanInSecondP.firstChild.length; @@ -132,12 +156,14 @@ function doTest(){ {name:'collapsedAtEndOfTextNode', range:[firstP.firstChild, 6], rect:[spanInFirstPRect.left, spanInFirstPRect.left, spanInFirstPRect.top, spanInFirstPRect.bottom, 0, spanInFirstPRect.height]}, - {name:'singleBlockNode', range:[root, 1, root, 2], rect:firstPRect}, + {name:'singleBlockNode', range:[root, 1, root, 2], rect:firstPRect, + rectList:[firstPRect, textInFirstPRect, spanInFirstPRect]}, {name:'twoBlockNodes', range:[root, 1, root, 3], rect:[firstPRect.left, firstPRect.right, firstPRect.top, firstDivRect.bottom, firstPRect.width, firstDivRect.bottom - firstPRect.top], - rectList:[firstPRect, firstDivRect]}, + rectList:[firstPRect, textInFirstPRect, textInSpanInFirstPRect, + firstDivRect, textInFirstDivRect, textInSpanInFirstDivRect]}, {name:'endOfTextNodeToEndOfAnotherTextNodeInAnotherBlock', range:[spanInFirstP.firstChild, 1, firstDiv.firstChild, 5], rect:[spanInFirstDivRect.left - 5*widthPerchar, spanInFirstDivRect.left, @@ -163,7 +189,7 @@ function doTest(){ rectList:[[spanInSecondPRect.left - 3*widthPerchar, spanInSecondPRect.left, spanInSecondPRect.top, spanInSecondPRect.bottom, 3 * widthPerchar, spanInSecondPRect.height], - spanInSecondPRect, + spanInSecondPRect, textInSpanInSecondPRect, [spanInSecondPRect.right, spanInSecondPRect.right + widthPerchar, spanInSecondPRect.top, spanInSecondPRect.bottom, widthPerchar, spanInSecondPRect.height]]} @@ -228,10 +254,17 @@ function doTest(){ } function testMixedDir(){ var root = document.getElementById('mixeddir'); + var bdo = document.getElementById('bdo'); var firstSpan = root.firstElementChild, firstSpanRect=firstSpan.getBoundingClientRect(), - firstSpanRectList = firstSpan.getClientRects(); + firstSpanWithInnerTextRectList = Array.from(firstSpan.getClientRects()); + firstSpanWithInnerTextRectList.push(...bdo.getClientRects()); + + // Depending on the font rendering, the order of the rects composing the bdo + // element may vary. We need to sort the list of rects before comparing it to + // the expected list. + firstSpanWithInnerTextRectList = sortRectList(firstSpanWithInnerTextRectList); runATest({name:'mixeddir',range:[firstSpan.firstChild,0,firstSpan.lastChild,firstSpan.lastChild.length], - rect: firstSpanRect, rectList:firstSpanRectList}); + rect: firstSpanRect, rectList:firstSpanWithInnerTextRectList, mustSortBeforeComparing: true}); root = document.getElementById('mixeddir2'); firstSpan = root.firstElementChild; @@ -271,7 +304,10 @@ function testShadowDOM() { isnot(rect.height, 0, "Div element inside shadow shouldn't have zero size."); } -function test(){ +async function test(){ + // We use getBoxQuads to get some text nodes bounding rects. + await SpecialPowers.pushPrefEnv({"set": [["layout.css.getBoxQuads.enabled", true]]}); + //test ltr doTest(); diff --git a/dom/base/test/unit/test_xhr_standalone.js b/dom/base/test/unit/test_xhr_standalone.js index 94f2d7d642..100027f38a 100644 --- a/dom/base/test/unit/test_xhr_standalone.js +++ b/dom/base/test/unit/test_xhr_standalone.js @@ -6,6 +6,10 @@ // in non-window non-Worker context function run_test() { + Services.prefs.setBoolPref( + "network.fetch.systemDefaultsToOmittingCredentials", + false + ); var xhr = new XMLHttpRequest(); xhr.open("GET", "data:,", false); var exceptionThrown = false; @@ -13,6 +17,7 @@ function run_test() { xhr.responseType = ""; xhr.withCredentials = false; } catch (e) { + console.error(e); exceptionThrown = true; } Assert.equal(false, exceptionThrown); diff --git a/dom/base/use_counter_metrics.yaml b/dom/base/use_counter_metrics.yaml index 211d3e1344..11b569e6d4 100644 --- a/dom/base/use_counter_metrics.yaml +++ b/dom/base/use_counter_metrics.yaml @@ -107,8 +107,8 @@ use.counter: send_in_pings: - use-counters -# Total of 2307 use counter metrics (excludes denominators). -# Total of 358 'page' use counters. +# Total of 2301 use counter metrics (excludes denominators). +# Total of 354 'page' use counters. use.counter.page: svgsvgelement_getelementbyid: type: counter @@ -450,57 +450,6 @@ use.counter.page: send_in_pings: - use-counters - onstart: - type: counter - description: > - Whether a page sets a <marquee> onstart event listener. - Compare against `use.counter.top_level_content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - - onbounce: - type: counter - description: > - Whether a page sets a <marquee> onbounce event listener. - Compare against `use.counter.top_level_content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - - onfinish: - type: counter - description: > - Whether a page sets a <marquee> onfinish event listener. - Compare against `use.counter.top_level_content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - onoverflow: type: counter description: > @@ -569,23 +518,6 @@ use.counter.page: send_in_pings: - use-counters - js_late_weekday: - type: counter - description: > - Whether a page parses a Date with day of week in an unexpected position. - Compare against `use.counter.top_level_content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - js_wasm_legacy_exceptions: type: counter description: > @@ -6196,7 +6128,7 @@ use.counter.page: send_in_pings: - use-counters -# Total of 358 'document' use counters. +# Total of 354 'document' use counters. use.counter.doc: svgsvgelement_getelementbyid: type: counter @@ -6538,57 +6470,6 @@ use.counter.doc: send_in_pings: - use-counters - onstart: - type: counter - description: > - Whether a document sets a <marquee> onstart event listener. - Compare against `use.counter.content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - - onbounce: - type: counter - description: > - Whether a document sets a <marquee> onbounce event listener. - Compare against `use.counter.content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - - onfinish: - type: counter - description: > - Whether a document sets a <marquee> onfinish event listener. - Compare against `use.counter.content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - onoverflow: type: counter description: > @@ -6657,23 +6538,6 @@ use.counter.doc: send_in_pings: - use-counters - js_late_weekday: - type: counter - description: > - Whether a document parses a Date with day of week in an unexpected position. - Compare against `use.counter.content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - js_wasm_legacy_exceptions: type: counter description: > @@ -15677,7 +15541,7 @@ use.counter.deprecated_ops.doc: send_in_pings: - use-counters -# Total of 696 'CSS (page)' use counters. +# Total of 697 'CSS (page)' use counters. use.counter.css.page: css_align_content: type: counter @@ -27528,7 +27392,7 @@ use.counter.css.page: send_in_pings: - use-counters -# Total of 696 'CSS (document)' use counters. +# Total of 697 'CSS (document)' use counters. use.counter.css.doc: css_align_content: type: counter diff --git a/dom/bindings/BindingUtils.cpp b/dom/bindings/BindingUtils.cpp index 4cccda7a4b..11d12dd364 100644 --- a/dom/bindings/BindingUtils.cpp +++ b/dom/bindings/BindingUtils.cpp @@ -758,42 +758,30 @@ bool DefineLegacyUnforgeableAttributes( return DefinePrefable(cx, obj, props); } -// We should use JSFunction objects for interface objects, but we need a custom -// hasInstance hook because we have new interface objects on prototype chains of -// old (XPConnect-based) bindings. We also need Xrays and arbitrary numbers of -// reserved slots (e.g. for legacy factory functions). So we define a custom -// funToString ObjectOps member for interface objects. -JSString* InterfaceObjectToString(JSContext* aCx, JS::Handle<JSObject*> aObject, - bool /* isToSource */) { - const JSClass* clasp = JS::GetClass(aObject); - MOZ_ASSERT(IsDOMIfaceAndProtoClass(clasp)); - - const DOMIfaceJSClass* ifaceJSClass = DOMIfaceJSClass::FromJSClass(clasp); - return JS_NewStringCopyZ(aCx, ifaceJSClass->mFunToString); +bool InterfaceObjectJSNative(JSContext* cx, unsigned argc, JS::Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + return NativeHolderFromInterfaceObject(&args.callee())->mNative(cx, argc, vp); } -bool Constructor(JSContext* cx, unsigned argc, JS::Value* vp) { +bool LegacyFactoryFunctionJSNative(JSContext* cx, unsigned argc, + JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - const JS::Value& v = js::GetFunctionNativeReserved( - &args.callee(), CONSTRUCTOR_NATIVE_HOLDER_RESERVED_SLOT); - const JSNativeHolder* nativeHolder = - static_cast<const JSNativeHolder*>(v.toPrivate()); - return (nativeHolder->mNative)(cx, argc, vp); -} - -static JSObject* CreateConstructor(JSContext* cx, JS::Handle<JSObject*> global, - const char* name, - const JSNativeHolder* nativeHolder, - unsigned ctorNargs) { - JSFunction* fun = js::NewFunctionWithReserved(cx, Constructor, ctorNargs, - JSFUN_CONSTRUCTOR, name); + return NativeHolderFromLegacyFactoryFunction(&args.callee()) + ->mNative(cx, argc, vp); +} + +static JSObject* CreateLegacyFactoryFunction(JSContext* cx, jsid name, + const JSNativeHolder* nativeHolder, + unsigned ctorNargs) { + JSFunction* fun = js::NewFunctionByIdWithReserved( + cx, LegacyFactoryFunctionJSNative, ctorNargs, JSFUN_CONSTRUCTOR, name); if (!fun) { return nullptr; } JSObject* constructor = JS_GetFunctionObject(fun); js::SetFunctionNativeReserved( - constructor, CONSTRUCTOR_NATIVE_HOLDER_RESERVED_SLOT, + constructor, LEGACY_FACTORY_FUNCTION_NATIVE_HOLDER_RESERVED_SLOT, JS::PrivateValue(const_cast<JSNativeHolder*>(nativeHolder))); return constructor; } @@ -846,11 +834,11 @@ static bool InterfaceIsInstance(JSContext* cx, unsigned argc, JS::Value* vp) { return true; } - // If "this" doesn't have a DOMIfaceAndProtoJSClass, it's not a DOM - // constructor, so just fall back. But note that we should - // CheckedUnwrapStatic here, because otherwise we won't get the right answers. - // The static version is OK, because we're looking for DOM constructors, which - // are not cross-origin objects. + // If "this" is not an interface object, likewise return false (again, like + // OrdinaryHasInstance). But note that we should CheckedUnwrapStatic here, + // because otherwise we won't get the right answers. + // The static version is OK, because we're looking for interface objects, + // which are not cross-origin objects. JS::Rooted<JSObject*> thisObj( cx, js::CheckedUnwrapStatic(&args.thisv().toObject())); if (!thisObj) { @@ -859,20 +847,16 @@ static bool InterfaceIsInstance(JSContext* cx, unsigned argc, JS::Value* vp) { return true; } - const JSClass* thisClass = JS::GetClass(thisObj); - - if (!IsDOMIfaceAndProtoClass(thisClass)) { + if (!IsInterfaceObject(thisObj)) { args.rval().setBoolean(false); return true; } - const DOMIfaceAndProtoJSClass* clasp = - DOMIfaceAndProtoJSClass::FromJSClass(thisClass); + const DOMInterfaceInfo* interfaceInfo = InterfaceInfoFromObject(thisObj); - // If "this" isn't a DOM constructor or is a constructor for an interface - // without a prototype, just fall back. - if (clasp->mType != eInterface || - clasp->mPrototypeID == prototypes::id::_ID_Count) { + // If "this" is a constructor for an interface without a prototype, just fall + // back. + if (interfaceInfo->mPrototypeID == prototypes::id::_ID_Count) { args.rval().setBoolean(false); return true; } @@ -881,13 +865,13 @@ static bool InterfaceIsInstance(JSContext* cx, unsigned argc, JS::Value* vp) { const DOMJSClass* domClass = GetDOMClass( js::UncheckedUnwrap(instance, /* stopAtWindowProxy = */ false)); - if (domClass && - domClass->mInterfaceChain[clasp->mDepth] == clasp->mPrototypeID) { + if (domClass && domClass->mInterfaceChain[interfaceInfo->mDepth] == + interfaceInfo->mPrototypeID) { args.rval().setBoolean(true); return true; } - if (IsRemoteObjectProxy(instance, clasp->mPrototypeID)) { + if (IsRemoteObjectProxy(instance, interfaceInfo->mPrototypeID)) { args.rval().setBoolean(true); return true; } @@ -896,80 +880,85 @@ static bool InterfaceIsInstance(JSContext* cx, unsigned argc, JS::Value* vp) { return true; } -// name must be an atom (or JS::PropertyKey::NonIntAtom will assert). -static JSObject* CreateInterfaceObject( - JSContext* cx, JS::Handle<JSObject*> global, - JS::Handle<JSObject*> constructorProto, - const DOMIfaceJSClass* constructorClass, unsigned ctorNargs, - const LegacyFactoryFunction* legacyFactoryFunctions, - JS::Handle<JSObject*> proto, const NativeProperties* properties, - const NativeProperties* chromeOnlyProperties, JS::Handle<JSString*> name, - bool isChrome, bool defineOnGlobal, const char* const* legacyWindowAliases, - bool isNamespace) { - JS::Rooted<JSObject*> constructor(cx); - MOZ_ASSERT(constructorProto); - MOZ_ASSERT(constructorClass); - constructor = JS_NewObjectWithGivenProto(cx, constructorClass->ToJSClass(), - constructorProto); - if (!constructor) { - return nullptr; - } - - if (!isNamespace) { - if (!JS_DefineProperty(cx, constructor, "length", ctorNargs, - JSPROP_READONLY)) { - return nullptr; - } - - if (!JS_DefineProperty(cx, constructor, "name", name, JSPROP_READONLY)) { - return nullptr; - } - } - - if (constructorClass->wantsInterfaceIsInstance && isChrome && - !JS_DefineFunction(cx, constructor, "isInstance", InterfaceIsInstance, 1, - // Don't bother making it enumerable - 0)) { - return nullptr; - } - +bool InitInterfaceOrNamespaceObject( + JSContext* cx, JS::Handle<JSObject*> obj, + const NativeProperties* properties, + const NativeProperties* chromeOnlyProperties, bool isChrome) { if (properties) { if (properties->HasStaticMethods() && - !DefinePrefable(cx, constructor, properties->StaticMethods())) { - return nullptr; + !DefinePrefable(cx, obj, properties->StaticMethods())) { + return false; } if (properties->HasStaticAttributes() && - !DefinePrefable(cx, constructor, properties->StaticAttributes())) { - return nullptr; + !DefinePrefable(cx, obj, properties->StaticAttributes())) { + return false; } if (properties->HasConstants() && - !DefinePrefable(cx, constructor, properties->Constants())) { - return nullptr; + !DefinePrefable(cx, obj, properties->Constants())) { + return false; } } if (chromeOnlyProperties && isChrome) { if (chromeOnlyProperties->HasStaticMethods() && - !DefinePrefable(cx, constructor, - chromeOnlyProperties->StaticMethods())) { - return nullptr; + !DefinePrefable(cx, obj, chromeOnlyProperties->StaticMethods())) { + return false; } if (chromeOnlyProperties->HasStaticAttributes() && - !DefinePrefable(cx, constructor, - chromeOnlyProperties->StaticAttributes())) { - return nullptr; + !DefinePrefable(cx, obj, chromeOnlyProperties->StaticAttributes())) { + return false; } if (chromeOnlyProperties->HasConstants() && - !DefinePrefable(cx, constructor, chromeOnlyProperties->Constants())) { + !DefinePrefable(cx, obj, chromeOnlyProperties->Constants())) { + return false; + } + } + + return true; +} + +// name must be an atom (or JS::PropertyKey::NonIntAtom will assert). +static JSObject* CreateInterfaceObject( + JSContext* cx, JS::Handle<JSObject*> global, + JS::Handle<JSObject*> interfaceProto, const DOMInterfaceInfo* interfaceInfo, + unsigned ctorNargs, + const Span<const LegacyFactoryFunction>& legacyFactoryFunctions, + JS::Handle<JSObject*> proto, const NativeProperties* properties, + const NativeProperties* chromeOnlyProperties, JS::Handle<JSString*> name, + bool isChrome, bool defineOnGlobal, + const char* const* legacyWindowAliases) { + MOZ_ASSERT(interfaceProto); + MOZ_ASSERT(interfaceInfo); + + JS::Rooted<jsid> nameId(cx, JS::PropertyKey::NonIntAtom(name)); + + JS::Rooted<JSObject*> constructor(cx); + { + JSFunction* fun = js::NewFunctionByIdWithReservedAndProto( + cx, InterfaceObjectJSNative, interfaceProto, ctorNargs, + JSFUN_CONSTRUCTOR, nameId); + if (!fun) { return nullptr; } + + constructor = JS_GetFunctionObject(fun); } - if (isNamespace && !DefineToStringTag(cx, constructor, name)) { + js::SetFunctionNativeReserved( + constructor, INTERFACE_OBJECT_INFO_RESERVED_SLOT, + JS::PrivateValue(const_cast<DOMInterfaceInfo*>(interfaceInfo))); + + // Eagerly force creation of the .length and .name properties, because they + // need to be defined before the .prototype property (CreateBuiltinFunction + // called from the WebIDL spec sets them, and then the .prototype property is + // defined in the WebIDL spec itself). + bool unused; + if (!JS_HasProperty(cx, constructor, "length", &unused) || + !JS_HasProperty(cx, constructor, "name", &unused)) { return nullptr; } @@ -977,8 +966,19 @@ static JSObject* CreateInterfaceObject( return nullptr; } - JS::Rooted<jsid> nameStr(cx, JS::PropertyKey::NonIntAtom(name)); - if (defineOnGlobal && !DefineConstructor(cx, global, nameStr, constructor)) { + if (!InitInterfaceOrNamespaceObject(cx, constructor, properties, + chromeOnlyProperties, isChrome)) { + return nullptr; + } + + if (defineOnGlobal && !DefineConstructor(cx, global, nameId, constructor)) { + return nullptr; + } + + if (interfaceInfo->wantsInterfaceIsInstance && isChrome && + !JS_DefineFunction(cx, constructor, "isInstance", InterfaceIsInstance, 1, + // Don't bother making it enumerable + 0)) { return nullptr; } @@ -990,25 +990,28 @@ static JSObject* CreateInterfaceObject( } } - if (legacyFactoryFunctions) { - int legacyFactoryFunctionSlot = DOM_INTERFACE_SLOTS_BASE; - while (legacyFactoryFunctions->mName) { - JS::Rooted<JSObject*> legacyFactoryFunction( - cx, CreateConstructor(cx, global, legacyFactoryFunctions->mName, - &legacyFactoryFunctions->mHolder, - legacyFactoryFunctions->mNargs)); - if (!legacyFactoryFunction || - !JS_DefineProperty(cx, legacyFactoryFunction, "prototype", proto, - JSPROP_PERMANENT | JSPROP_READONLY) || - (defineOnGlobal && - !DefineConstructor(cx, global, legacyFactoryFunctions->mName, - legacyFactoryFunction))) { - return nullptr; - } - JS::SetReservedSlot(constructor, legacyFactoryFunctionSlot++, - JS::ObjectValue(*legacyFactoryFunction)); - ++legacyFactoryFunctions; + int legacyFactoryFunctionSlot = + INTERFACE_OBJECT_FIRST_LEGACY_FACTORY_FUNCTION; + for (const LegacyFactoryFunction& lff : legacyFactoryFunctions) { + JSString* fname = JS_AtomizeString(cx, lff.mName); + if (!fname) { + return nullptr; + } + + nameId = JS::PropertyKey::NonIntAtom(fname); + + JS::Rooted<JSObject*> legacyFactoryFunction( + cx, CreateLegacyFactoryFunction(cx, nameId, &lff.mHolder, lff.mNargs)); + if (!legacyFactoryFunction || + !JS_DefineProperty(cx, legacyFactoryFunction, "prototype", proto, + JSPROP_PERMANENT | JSPROP_READONLY) || + (defineOnGlobal && + !DefineConstructor(cx, global, nameId, legacyFactoryFunction))) { + return nullptr; } + js::SetFunctionNativeReserved(constructor, legacyFactoryFunctionSlot, + JS::ObjectValue(*legacyFactoryFunction)); + ++legacyFactoryFunctionSlot; } return constructor; @@ -1101,18 +1104,20 @@ bool DefineProperties(JSContext* cx, JS::Handle<JSObject*> obj, return true; } +namespace binding_detail { + void CreateInterfaceObjects( JSContext* cx, JS::Handle<JSObject*> global, JS::Handle<JSObject*> protoProto, const DOMIfaceAndProtoJSClass* protoClass, - JS::Heap<JSObject*>* protoCache, JS::Handle<JSObject*> constructorProto, - const DOMIfaceJSClass* constructorClass, unsigned ctorNargs, + JS::Heap<JSObject*>* protoCache, JS::Handle<JSObject*> interfaceProto, + const DOMInterfaceInfo* interfaceInfo, unsigned ctorNargs, bool isConstructorChromeOnly, - const LegacyFactoryFunction* legacyFactoryFunctions, + const Span<const LegacyFactoryFunction>& legacyFactoryFunctions, JS::Heap<JSObject*>* constructorCache, const NativeProperties* properties, const NativeProperties* chromeOnlyProperties, const char* name, bool defineOnGlobal, const char* const* unscopableNames, bool isGlobal, - const char* const* legacyWindowAliases, bool isNamespace) { - MOZ_ASSERT(protoClass || constructorClass, "Need at least one class!"); + const char* const* legacyWindowAliases) { + MOZ_ASSERT(protoClass || interfaceInfo, "Need at least a class or info!"); MOZ_ASSERT( !((properties && (properties->HasMethods() || properties->HasAttributes())) || @@ -1125,16 +1130,16 @@ void CreateInterfaceObjects( (chromeOnlyProperties && (chromeOnlyProperties->HasStaticMethods() || chromeOnlyProperties->HasStaticAttributes()))) || - constructorClass, - "Static methods but no constructorClass!"); + interfaceInfo, + "Static methods but no info!"); MOZ_ASSERT(!protoClass == !protoCache, "If, and only if, there is an interface prototype object we need " "to cache it"); - MOZ_ASSERT(bool(constructorClass) == bool(constructorCache), + MOZ_ASSERT(bool(interfaceInfo) == bool(constructorCache), "If, and only if, there is an interface object we need to cache " "it"); - MOZ_ASSERT(constructorProto || !constructorClass, - "Must have a constructor proto if we plan to create a constructor " + MOZ_ASSERT(interfaceProto || !interfaceInfo, + "Must have a interface proto if we plan to create an interface " "object"); bool isChrome = nsContentUtils::ThreadsafeIsSystemCaller(cx); @@ -1160,12 +1165,12 @@ void CreateInterfaceObjects( } JSObject* interface; - if (constructorClass) { + if (interfaceInfo) { interface = CreateInterfaceObject( - cx, global, constructorProto, constructorClass, + cx, global, interfaceProto, interfaceInfo, (isChrome || !isConstructorChromeOnly) ? ctorNargs : 0, legacyFactoryFunctions, proto, properties, chromeOnlyProperties, - nameStr, isChrome, defineOnGlobal, legacyWindowAliases, isNamespace); + nameStr, isChrome, defineOnGlobal, legacyWindowAliases); if (!interface) { if (protoCache) { // If we fail we need to make sure to clear the value of protoCache we @@ -1178,6 +1183,45 @@ void CreateInterfaceObjects( } } +} // namespace binding_detail + +void CreateNamespaceObject(JSContext* cx, JS::Handle<JSObject*> global, + JS::Handle<JSObject*> namespaceProto, + const DOMIfaceAndProtoJSClass& namespaceClass, + JS::Heap<JSObject*>* namespaceCache, + const NativeProperties* properties, + const NativeProperties* chromeOnlyProperties, + const char* name, bool defineOnGlobal) { + JS::Rooted<JSString*> nameStr(cx, JS_AtomizeString(cx, name)); + if (!nameStr) { + return; + } + JS::Rooted<jsid> nameId(cx, JS::PropertyKey::NonIntAtom(nameStr)); + + JS::Rooted<JSObject*> namespaceObj( + cx, JS_NewObjectWithGivenProto(cx, namespaceClass.ToJSClass(), + namespaceProto)); + if (!namespaceObj) { + return; + } + + if (!InitInterfaceOrNamespaceObject( + cx, namespaceObj, properties, chromeOnlyProperties, + nsContentUtils::ThreadsafeIsSystemCaller(cx))) { + return; + } + + if (defineOnGlobal && !DefineConstructor(cx, global, nameId, namespaceObj)) { + return; + } + + if (!DefineToStringTag(cx, namespaceObj, nameStr)) { + return; + } + + *namespaceCache = namespaceObj; +} + // Only set aAllowNativeWrapper to false if you really know you need it; if in // doubt use true. Setting it to false disables security wrappers. static bool NativeInterface2JSObjectAndThrowIfFailed( @@ -1461,14 +1505,9 @@ bool ThrowConstructorWithoutNew(JSContext* cx, const char* name) { return ThrowErrorMessage<MSG_CONSTRUCTOR_WITHOUT_NEW>(cx, name); } -inline const NativePropertyHooks* GetNativePropertyHooksFromConstructorFunction( +inline const NativePropertyHooks* GetNativePropertyHooksFromJSNative( JS::Handle<JSObject*> obj) { - MOZ_ASSERT(JS_IsNativeFunction(obj, Constructor)); - const JS::Value& v = js::GetFunctionNativeReserved( - obj, CONSTRUCTOR_NATIVE_HOLDER_RESERVED_SLOT); - const JSNativeHolder* nativeHolder = - static_cast<const JSNativeHolder*>(v.toPrivate()); - return nativeHolder->mPropertyHooks; + return NativeHolderFromObject(obj)->mPropertyHooks; } inline const NativePropertyHooks* GetNativePropertyHooks( @@ -1484,7 +1523,7 @@ inline const NativePropertyHooks* GetNativePropertyHooks( if (JS_ObjectIsFunction(obj)) { type = eInterface; - return GetNativePropertyHooksFromConstructorFunction(obj); + return GetNativePropertyHooksFromJSNative(obj); } MOZ_ASSERT(IsDOMIfaceAndProtoClass(JS::GetClass(obj))); @@ -1855,23 +1894,20 @@ static bool ResolvePrototypeOrConstructor( } if (id.get() == GetJSIDByIndex(cx, XPCJSContext::IDX_ISINSTANCE)) { - const JSClass* objClass = JS::GetClass(obj); - if (IsDOMIfaceAndProtoClass(objClass)) { - const DOMIfaceJSClass* clazz = DOMIfaceJSClass::FromJSClass(objClass); - if (clazz->wantsInterfaceIsInstance) { - cacheOnHolder = true; - - JSObject* funObj = XrayCreateFunction( - cx, wrapper, {InterfaceIsInstance, nullptr}, 1, id); - if (!funObj) { - return false; - } - - desc.set(Some(JS::PropertyDescriptor::Data( - JS::ObjectValue(*funObj), {JS::PropertyAttribute::Configurable, - JS::PropertyAttribute::Writable}))); - return true; + if (IsInterfaceObject(obj) && + InterfaceInfoFromObject(obj)->wantsInterfaceIsInstance) { + cacheOnHolder = true; + + JSObject* funObj = XrayCreateFunction( + cx, wrapper, {InterfaceIsInstance, nullptr}, 1, id); + if (!funObj) { + return false; } + + desc.set(Some(JS::PropertyDescriptor::Data( + JS::ObjectValue(*funObj), {JS::PropertyAttribute::Configurable, + JS::PropertyAttribute::Writable}))); + return true; } } } else if (type == eNamespace) { @@ -2187,31 +2223,6 @@ NativePropertyHooks sEmptyNativePropertyHooks = { constructors::id::_ID_Count, nullptr}; -const JSClassOps sBoringInterfaceObjectClassClassOps = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* enumerate */ - nullptr, /* newEnumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - nullptr, /* finalize */ - ThrowingConstructor, /* call */ - ThrowingConstructor, /* construct */ - nullptr, /* trace */ -}; - -const js::ObjectOps sInterfaceObjectClassObjectOps = { - nullptr, /* lookupProperty */ - nullptr, /* defineProperty */ - nullptr, /* hasProperty */ - nullptr, /* getProperty */ - nullptr, /* setProperty */ - nullptr, /* getOwnPropertyDescriptor */ - nullptr, /* deleteProperty */ - nullptr, /* getElements */ - InterfaceObjectToString, /* funToString */ -}; - bool GetPropertyOnPrototype(JSContext* cx, JS::Handle<JSObject*> proxy, JS::Handle<JS::Value> receiver, JS::Handle<jsid> id, bool* found, JS::MutableHandle<JS::Value> vp) { @@ -3572,16 +3583,8 @@ bool ForEachHandler(JSContext* aCx, unsigned aArgc, JS::Value* aVp) { static inline prototypes::ID GetProtoIdForNewtarget( JS::Handle<JSObject*> aNewTarget) { - const JSClass* newTargetClass = JS::GetClass(aNewTarget); - if (IsDOMIfaceAndProtoClass(newTargetClass)) { - const DOMIfaceAndProtoJSClass* newTargetIfaceClass = - DOMIfaceAndProtoJSClass::FromJSClass(newTargetClass); - if (newTargetIfaceClass->mType == eInterface) { - return newTargetIfaceClass->mPrototypeID; - } - } else if (JS_IsNativeFunction(aNewTarget, Constructor)) { - return GetNativePropertyHooksFromConstructorFunction(aNewTarget) - ->mPrototypeID; + if (IsDOMConstructor(aNewTarget)) { + return GetNativePropertyHooksFromJSNative(aNewTarget)->mPrototypeID; } return prototypes::id::_ID_Count; diff --git a/dom/bindings/BindingUtils.h b/dom/bindings/BindingUtils.h index dd7a42b670..1e3049d456 100644 --- a/dom/bindings/BindingUtils.h +++ b/dom/bindings/BindingUtils.h @@ -698,12 +698,45 @@ struct JSNativeHolder { const NativePropertyHooks* mPropertyHooks; }; +// Struct for holding information for WebIDL interface objects (which are +// function objects). A pointer to this struct is held in the first reserved +// slot of the function object. +struct DOMInterfaceInfo { + JSNativeHolder nativeHolder; + + ProtoGetter mGetParentProto; + + const prototypes::ID mPrototypeID; // uint16_t + const uint32_t mDepth; + + // Boolean indicating whether this object wants a isInstance property + // pointing to InterfaceIsInstance defined on it. Only ever true for + // interfaces. + bool wantsInterfaceIsInstance; +}; + struct LegacyFactoryFunction { const char* mName; const JSNativeHolder mHolder; unsigned mNargs; }; +namespace binding_detail { + +void CreateInterfaceObjects( + JSContext* cx, JS::Handle<JSObject*> global, + JS::Handle<JSObject*> protoProto, const DOMIfaceAndProtoJSClass* protoClass, + JS::Heap<JSObject*>* protoCache, JS::Handle<JSObject*> interfaceProto, + const DOMInterfaceInfo* interfaceInfo, unsigned ctorNargs, + bool isConstructorChromeOnly, + const Span<const LegacyFactoryFunction>& legacyFactoryFunctions, + JS::Heap<JSObject*>* constructorCache, const NativeProperties* properties, + const NativeProperties* chromeOnlyProperties, const char* name, + bool defineOnGlobal, const char* const* unscopableNames, bool isGlobal, + const char* const* legacyWindowAliases); + +} // namespace binding_detail + // clang-format off /* * Create a DOM interface object (if constructorClass is non-null) and/or a @@ -712,24 +745,20 @@ struct LegacyFactoryFunction { * global is used as the parent of the interface object and the interface * prototype object * protoProto is the prototype to use for the interface prototype object. - * interfaceProto is the prototype to use for the interface object. This can be - * null if both constructorClass and constructor are null (as in, - * if we're not creating an interface object at all). * protoClass is the JSClass to use for the interface prototype object. * This is null if we should not create an interface prototype * object. * protoCache a pointer to a JSObject pointer where we should cache the * interface prototype object. This must be null if protoClass is and * vice versa. - * constructorClass is the JSClass to use for the interface object. - * This is null if we should not create an interface object or - * if it should be a function object. - * constructor holds the JSNative to back the interface object which should be a - * Function, unless constructorClass is non-null in which case it is - * ignored. If this is null and constructorClass is also null then - * we should not create an interface object at all. + * interfaceProto is the prototype to use for the interface object. This can be + * null if interfaceInfo is null (as in, if we're not creating an + * interface object at all). + * interfaceInfo is the info to use for the interface object. This can be null + * if we're not creating an interface object. * ctorNargs is the length of the constructor function; 0 if no constructor * isConstructorChromeOnly if true, the constructor is ChromeOnly. + * legacyFactoryFunctions the legacy factory functions (can be empty) * constructorCache a pointer to a JSObject pointer where we should cache the * interface object. This must be null if both constructorClass * and constructor are null, and non-null otherwise. @@ -761,23 +790,66 @@ struct LegacyFactoryFunction { * char* names of the legacy window aliases for this * interface. * - * At least one of protoClass, constructorClass or constructor should be - * non-null. If constructorClass or constructor are non-null, the resulting - * interface object will be defined on the given global with property name - * |name|, which must also be non-null. + * At least one of protoClass or interfaceInfo should be non-null. If + * interfaceInfo is non-null, the resulting interface object will be defined on + * the given global with property name |name|, which must also be non-null. */ // clang-format on -void CreateInterfaceObjects( +template <size_t N> +inline void CreateInterfaceObjects( JSContext* cx, JS::Handle<JSObject*> global, JS::Handle<JSObject*> protoProto, const DOMIfaceAndProtoJSClass* protoClass, - JS::Heap<JSObject*>* protoCache, JS::Handle<JSObject*> constructorProto, - const DOMIfaceJSClass* constructorClass, unsigned ctorNargs, + JS::Heap<JSObject*>* protoCache, JS::Handle<JSObject*> interfaceProto, + const DOMInterfaceInfo* interfaceInfo, unsigned ctorNargs, bool isConstructorChromeOnly, - const LegacyFactoryFunction* legacyFactoryFunctions, + const Span<const LegacyFactoryFunction, N>& legacyFactoryFunctions, JS::Heap<JSObject*>* constructorCache, const NativeProperties* properties, const NativeProperties* chromeOnlyProperties, const char* name, bool defineOnGlobal, const char* const* unscopableNames, bool isGlobal, - const char* const* legacyWindowAliases, bool isNamespace); + const char* const* legacyWindowAliases) { + // We're using 1 slot for the interface info already, so we only have + // INTERFACE_OBJECT_MAX_SLOTS - 1 slots for legacy factory functions. + static_assert(N <= INTERFACE_OBJECT_MAX_SLOTS - + INTERFACE_OBJECT_FIRST_LEGACY_FACTORY_FUNCTION); + + return binding_detail::CreateInterfaceObjects( + cx, global, protoProto, protoClass, protoCache, interfaceProto, + interfaceInfo, ctorNargs, isConstructorChromeOnly, legacyFactoryFunctions, + constructorCache, properties, chromeOnlyProperties, name, defineOnGlobal, + unscopableNames, isGlobal, legacyWindowAliases); +} + +/* + * Create a namespace object. + * + * global the global on which to install a property named with name pointing to + * the namespace object if defineOnGlobal is true. + * namespaceProto is the prototype to use for the namespace object. + * namespaceClass is the JSClass to use for the namespace object. + * namespaceCache a pointer to a JSObject pointer where we should cache the + * namespace object. + * properties contains the methods, attributes and constants to be defined on + * objects in any compartment. + * chromeProperties contains the methods, attributes and constants to be defined + * on objects in chrome compartments. This must be null if the + * namespace doesn't have any ChromeOnly properties or if the + * object is being created in non-chrome compartment. + * name the name to use for the WebIDL class string, which is the value + * that's used for @@toStringTag, and the name of the property on the + * global object that would be set to the namespace object. + * defineOnGlobal controls whether properties should be defined on the given + * global for the namespace object. This can be false in + * situations where we want the properties to only appear on + * privileged Xrays but not on the unprivileged underlying + * global. + */ +void CreateNamespaceObject(JSContext* cx, JS::Handle<JSObject*> global, + JS::Handle<JSObject*> namespaceProto, + const DOMIfaceAndProtoJSClass& namespaceClass, + JS::Heap<JSObject*>* namespaceCache, + const NativeProperties* properties, + const NativeProperties* chromeOnlyProperties, + const char* name, bool defineOnGlobal); /** * Define the properties (regular and chrome-only) on obj. @@ -2267,18 +2339,53 @@ inline bool AddStringToIDVector(JSContext* cx, name); } -// We use one constructor JSNative to represent all DOM interface objects (so -// we can easily detect when we need to wrap them in an Xray wrapper). We store -// the real JSNative in the mNative member of a JSNativeHolder in the -// CONSTRUCTOR_NATIVE_HOLDER_RESERVED_SLOT slot of the JSFunction object for a -// specific interface object. We also store the NativeProperties in the -// JSNativeHolder. -// Note that some interface objects are not yet a JSFunction but a normal -// JSObject with a DOMJSClass, those do not use these slots. +// We use one JSNative to represent all DOM interface objects (so we can easily +// detect when we need to wrap them in an Xray wrapper). A pointer to the +// relevant DOMInterfaceInfo is stored in the +// INTERFACE_OBJECT_INFO_RESERVED_SLOT slot of the JSFunction object for a +// specific interface object. We store the real JSNative and the +// NativeProperties in a JSNativeHolder, held by the DOMInterfaceInfo. +bool InterfaceObjectJSNative(JSContext* cx, unsigned argc, JS::Value* vp); + +inline bool IsInterfaceObject(JSObject* obj) { + return JS_IsNativeFunction(obj, InterfaceObjectJSNative); +} + +inline const DOMInterfaceInfo* InterfaceInfoFromObject(JSObject* obj) { + MOZ_ASSERT(IsInterfaceObject(obj)); + const JS::Value& v = + js::GetFunctionNativeReserved(obj, INTERFACE_OBJECT_INFO_RESERVED_SLOT); + return static_cast<const DOMInterfaceInfo*>(v.toPrivate()); +} -enum { CONSTRUCTOR_NATIVE_HOLDER_RESERVED_SLOT = 0 }; +inline const JSNativeHolder* NativeHolderFromInterfaceObject(JSObject* obj) { + MOZ_ASSERT(IsInterfaceObject(obj)); + return &InterfaceInfoFromObject(obj)->nativeHolder; +} -bool Constructor(JSContext* cx, unsigned argc, JS::Value* vp); +// We use one JSNative to represent all legacy factory functions (so we can +// easily detect when we need to wrap them in an Xray wrapper). We store the +// real JSNative and the NativeProperties in a JSNativeHolder in the +// LEGACY_FACTORY_FUNCTION_NATIVE_HOLDER_RESERVED_SLOT slot of the JSFunction +// object. +bool LegacyFactoryFunctionJSNative(JSContext* cx, unsigned argc, JS::Value* vp); + +inline bool IsLegacyFactoryFunction(JSObject* obj) { + return JS_IsNativeFunction(obj, LegacyFactoryFunctionJSNative); +} + +inline const JSNativeHolder* NativeHolderFromLegacyFactoryFunction( + JSObject* obj) { + MOZ_ASSERT(IsLegacyFactoryFunction(obj)); + const JS::Value& v = js::GetFunctionNativeReserved( + obj, LEGACY_FACTORY_FUNCTION_NATIVE_HOLDER_RESERVED_SLOT); + return static_cast<const JSNativeHolder*>(v.toPrivate()); +} + +inline const JSNativeHolder* NativeHolderFromObject(JSObject* obj) { + return IsInterfaceObject(obj) ? NativeHolderFromInterfaceObject(obj) + : NativeHolderFromLegacyFactoryFunction(obj); +} // Implementation of the bits that XrayWrapper needs @@ -2350,8 +2457,11 @@ inline bool XrayGetNativeProto(JSContext* cx, JS::Handle<JSObject*> obj, protop.set(JS::GetRealmObjectPrototype(cx)); } } else if (JS_ObjectIsFunction(obj)) { - MOZ_ASSERT(JS_IsNativeFunction(obj, Constructor)); - protop.set(JS::GetRealmFunctionPrototype(cx)); + if (IsLegacyFactoryFunction(obj)) { + protop.set(JS::GetRealmFunctionPrototype(cx)); + } else { + protop.set(InterfaceInfoFromObject(obj)->mGetParentProto(cx)); + } } else { const JSClass* clasp = JS::GetClass(obj); MOZ_ASSERT(IsDOMIfaceAndProtoClass(clasp)); @@ -2426,36 +2536,16 @@ inline JSObject* GetCachedSlotStorageObject(JSContext* cx, extern NativePropertyHooks sEmptyNativePropertyHooks; -extern const JSClassOps sBoringInterfaceObjectClassClassOps; - -extern const js::ObjectOps sInterfaceObjectClassObjectOps; +inline bool IsDOMConstructor(JSObject* obj) { + return IsInterfaceObject(obj) || IsLegacyFactoryFunction(obj); +} inline bool UseDOMXray(JSObject* obj) { const JSClass* clasp = JS::GetClass(obj); - return IsDOMClass(clasp) || JS_IsNativeFunction(obj, Constructor) || + return IsDOMClass(clasp) || IsDOMConstructor(obj) || IsDOMIfaceAndProtoClass(clasp); } -inline bool IsDOMConstructor(JSObject* obj) { - if (JS_IsNativeFunction(obj, dom::Constructor)) { - // LegacyFactoryFunction, like Image - return true; - } - - const JSClass* clasp = JS::GetClass(obj); - // Check for a DOM interface object. - return dom::IsDOMIfaceAndProtoClass(clasp) && - dom::DOMIfaceAndProtoJSClass::FromJSClass(clasp)->mType == - dom::eInterface; -} - -#ifdef DEBUG -inline bool HasConstructor(JSObject* obj) { - return JS_IsNativeFunction(obj, Constructor) || - JS::GetClass(obj)->getConstruct(); -} -#endif - // Helpers for creating a const version of a type. template <typename T> const T& Constify(T& arg) { @@ -3207,10 +3297,6 @@ void DeprecationWarning(JSContext* aCx, JSObject* aObject, void DeprecationWarning(const GlobalObject& aGlobal, DeprecatedOperations aOperation); -// A callback to perform funToString on an interface object -JSString* InterfaceObjectToString(JSContext* aCx, JS::Handle<JSObject*> aObject, - unsigned /* indent */); - namespace binding_detail { // Get a JS global object that can be used for some temporary allocations. The // idea is that this should be used for situations when you need to operate in diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index 7eed2593aa..d735bcc2ee 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -382,8 +382,8 @@ DOMInterfaces = { }, 'IDBFactory': { - 'implicitJSContext': [ 'open', 'deleteDatabase', 'openForPrincipal', - 'deleteForPrincipal' ], + 'implicitJSContext': [ 'open', 'deleteDatabase', 'databases', + 'openForPrincipal', 'deleteForPrincipal' ], }, 'IDBKeyRange': { @@ -1890,6 +1890,16 @@ if buildconfig.substs.get("ENABLE_TESTS", False): 'register': False, }, + 'TestLegacyFactoryFunctionInterface' : { + 'headerFile': 'TestBindingHeader.h', + 'register': False, + }, + + 'TestLegacyFactoryFunctionInterface2' : { + 'headerFile': 'TestBindingHeader.h', + 'register': False, + }, + 'TestNamedDeleterInterface' : { 'headerFile': 'TestBindingHeader.h', 'register': False, diff --git a/dom/bindings/CallbackObject.cpp b/dom/bindings/CallbackObject.cpp index 484bba06c0..19370666fd 100644 --- a/dom/bindings/CallbackObject.cpp +++ b/dom/bindings/CallbackObject.cpp @@ -29,7 +29,7 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CallbackObject) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(CallbackObject) -NS_IMPL_CYCLE_COLLECTING_RELEASE(CallbackObject) +NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(CallbackObject, Reset()) NS_IMPL_CYCLE_COLLECTION_CLASS(CallbackObject) diff --git a/dom/bindings/Codegen.py b/dom/bindings/Codegen.py index 7a6a28bffc..a6a49f9b2f 100644 --- a/dom/bindings/Codegen.py +++ b/dom/bindings/Codegen.py @@ -926,11 +926,51 @@ def InterfaceObjectProtoGetter(descriptor, forXrays=False): return (protoGetter, protoHandleGetter) -class CGInterfaceObjectJSClass(CGThing): - def __init__(self, descriptor, properties): +class CGNamespaceObjectJSClass(CGThing): + def __init__(self, descriptor): + CGThing.__init__(self) + self.descriptor = descriptor + + def declare(self): + # We're purely for internal consumption + return "" + + def define(self): + (protoGetter, _) = InterfaceObjectProtoGetter(self.descriptor, forXrays=True) + + classString = self.descriptor.interface.getExtendedAttribute("ClassString") + if classString is None: + classString = self.descriptor.interface.identifier.name + else: + classString = classString[0] + return fill( + """ + static const DOMIfaceAndProtoJSClass sNamespaceObjectClass = { + { + "${classString}", + JSCLASS_IS_DOMIFACEANDPROTOJSCLASS, + JS_NULL_CLASS_OPS, + JS_NULL_CLASS_SPEC, + JS_NULL_CLASS_EXT, + JS_NULL_OBJECT_OPS + }, + eNamespace, + prototypes::id::_ID_Count, + 0, + ${hooks}, + ${protoGetter} + }; + """, + classString=classString, + hooks=NativePropertyHooks(self.descriptor), + protoGetter=protoGetter, + ) + + +class CGInterfaceObjectInfo(CGThing): + def __init__(self, descriptor): CGThing.__init__(self) self.descriptor = descriptor - self.properties = properties def declare(self): # We're purely for internal consumption @@ -938,104 +978,31 @@ class CGInterfaceObjectJSClass(CGThing): def define(self): if self.descriptor.interface.ctor(): - assert not self.descriptor.interface.isNamespace() ctorname = CONSTRUCT_HOOK_NAME - elif self.descriptor.interface.isNamespace(): - ctorname = "nullptr" else: ctorname = "ThrowingConstructor" wantsIsInstance = self.descriptor.interface.hasInterfacePrototypeObject() prototypeID, depth = PrototypeIDAndDepth(self.descriptor) - slotCount = "DOM_INTERFACE_SLOTS_BASE" - if len(self.descriptor.interface.legacyFactoryFunctions) > 0: - slotCount += " + %i /* slots for the legacy factory functions */" % len( - self.descriptor.interface.legacyFactoryFunctions - ) (protoGetter, _) = InterfaceObjectProtoGetter(self.descriptor, forXrays=True) - if ctorname == "ThrowingConstructor": - ret = "" - classOpsPtr = "&sBoringInterfaceObjectClassClassOps" - elif ctorname == "nullptr": - ret = "" - classOpsPtr = "JS_NULL_CLASS_OPS" - else: - ret = fill( - """ - static const JSClassOps sInterfaceObjectClassOps = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* enumerate */ - nullptr, /* newEnumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - nullptr, /* finalize */ - ${ctorname}, /* call */ - ${ctorname}, /* construct */ - nullptr, /* trace */ - }; - - """, - ctorname=ctorname, - ) - classOpsPtr = "&sInterfaceObjectClassOps" - - if self.descriptor.interface.isNamespace(): - classString = self.descriptor.interface.getExtendedAttribute("ClassString") - if classString is None: - classString = self.descriptor.interface.identifier.name - else: - classString = classString[0] - funToString = "nullptr" - objectOps = "JS_NULL_OBJECT_OPS" - else: - classString = "Function" - funToString = ( - '"function %s() {\\n [native code]\\n}"' - % self.descriptor.interface.identifier.name - ) - # We need non-default ObjectOps so we can actually make - # use of our funToString. - objectOps = "&sInterfaceObjectClassObjectOps" - - ret = ret + fill( + return fill( """ - static const DOMIfaceJSClass sInterfaceObjectClass = { - { - { - "${classString}", - JSCLASS_IS_DOMIFACEANDPROTOJSCLASS | JSCLASS_HAS_RESERVED_SLOTS(${slotCount}), - ${classOpsPtr}, - JS_NULL_CLASS_SPEC, - JS_NULL_CLASS_EXT, - ${objectOps} - }, - ${type}, - ${prototypeID}, - ${depth}, - ${hooks}, - ${protoGetter} - }, + static const DOMInterfaceInfo sInterfaceObjectInfo = { + { ${ctorname}, ${hooks} }, + ${protoGetter}, + ${prototypeID}, + ${depth}, ${wantsIsInstance}, - ${funToString} }; """, - classString=classString, - slotCount=slotCount, - classOpsPtr=classOpsPtr, + ctorname=ctorname, hooks=NativePropertyHooks(self.descriptor), - objectOps=objectOps, - type="eNamespace" - if self.descriptor.interface.isNamespace() - else "eInterface", + protoGetter=protoGetter, prototypeID=prototypeID, depth=depth, - protoGetter=protoGetter, wantsIsInstance=toStringBool(wantsIsInstance), - funToString=funToString, ) - return ret class CGList(CGThing): @@ -2323,7 +2290,6 @@ class CGLegacyFactoryFunctions(CGThing): static const LegacyFactoryFunction legacyFactoryFunctions[] = { $*{legacyFactoryFunctions} - { nullptr, { nullptr, nullptr }, 0 } }; """, name=self.descriptor.name, @@ -3538,83 +3504,30 @@ class CGCreateInterfaceObjectsMethod(CGAbstractMethod): self.haveLegacyWindowAliases = haveLegacyWindowAliases def definition_body(self): - (protoGetter, protoHandleGetter) = InterfacePrototypeObjectProtoGetter( - self.descriptor - ) - if protoHandleGetter is None: - parentProtoType = "Rooted" - getParentProto = "aCx, " + protoGetter - else: - parentProtoType = "Handle" - getParentProto = protoHandleGetter - getParentProto = getParentProto + "(aCx)" - - (protoGetter, protoHandleGetter) = InterfaceObjectProtoGetter(self.descriptor) - if protoHandleGetter is None: - getConstructorProto = "aCx, " + protoGetter - constructorProtoType = "Rooted" - else: - getConstructorProto = protoHandleGetter - constructorProtoType = "Handle" - getConstructorProto += "(aCx)" - needInterfaceObject = self.descriptor.interface.hasInterfaceObject() - needInterfacePrototypeObject = ( - self.descriptor.interface.hasInterfacePrototypeObject() - ) - - # if we don't need to create anything, why are we generating this? - assert needInterfaceObject or needInterfacePrototypeObject - - getParentProto = fill( - """ - JS::${type}<JSObject*> parentProto(${getParentProto}); - if (!parentProto) { - return; - } - """, - type=parentProtoType, - getParentProto=getParentProto, - ) - - getConstructorProto = fill( - """ - JS::${type}<JSObject*> constructorProto(${getConstructorProto}); - if (!constructorProto) { - return; - } - """, - type=constructorProtoType, - getConstructorProto=getConstructorProto, - ) - - if self.descriptor.interface.ctor(): - constructArgs = methodLength(self.descriptor.interface.ctor()) - isConstructorChromeOnly = isChromeOnly(self.descriptor.interface.ctor()) - else: - constructArgs = 0 - isConstructorChromeOnly = False - if len(self.descriptor.interface.legacyFactoryFunctions) > 0: - legacyFactoryFunctions = "legacyFactoryFunctions" - else: - legacyFactoryFunctions = "nullptr" + if needInterfaceObject: + (protoGetter, protoHandleGetter) = InterfaceObjectProtoGetter( + self.descriptor + ) + if protoHandleGetter is None: + getConstructorProto = "aCx, " + protoGetter + constructorProtoType = "Rooted" + else: + getConstructorProto = protoHandleGetter + constructorProtoType = "Handle" - if needInterfacePrototypeObject: - protoClass = "&sPrototypeClass" - protoCache = ( - "&aProtoAndIfaceCache.EntrySlotOrCreate(prototypes::id::%s)" - % self.descriptor.name + getConstructorProto = fill( + """ + JS::${type}<JSObject*> constructorProto(${getConstructorProto}(aCx)); + if (!constructorProto) { + return; + } + """, + type=constructorProtoType, + getConstructorProto=getConstructorProto, ) - parentProto = "parentProto" - getParentProto = CGGeneric(getParentProto) - else: - protoClass = "nullptr" - protoCache = "nullptr" - parentProto = "nullptr" - getParentProto = None - if needInterfaceObject: - interfaceClass = "&sInterfaceObjectClass" + interfaceInfo = "&sInterfaceObjectInfo" interfaceCache = ( "&aProtoAndIfaceCache.EntrySlotOrCreate(constructors::id::%s)" % self.descriptor.name @@ -3624,12 +3537,11 @@ class CGCreateInterfaceObjectsMethod(CGAbstractMethod): else: # We don't have slots to store the legacy factory functions. assert len(self.descriptor.interface.legacyFactoryFunctions) == 0 - interfaceClass = "nullptr" + interfaceInfo = "nullptr" interfaceCache = "nullptr" getConstructorProto = None constructorProto = "nullptr" - isGlobal = self.descriptor.isGlobal() is not None if self.properties.hasNonChromeOnly(): properties = "sNativeProperties.Upcast()" else: @@ -3648,31 +3560,117 @@ class CGCreateInterfaceObjectsMethod(CGAbstractMethod): name = self.descriptor.interface.getClassName() assert not (needInterfaceObject and " " in name) - call = fill( + if self.descriptor.interface.isNamespace(): + # If we don't need to create anything, why are we generating this? + assert needInterfaceObject + + call = fill( + """ + JS::Heap<JSObject*>* interfaceCache = ${interfaceCache}; + dom::CreateNamespaceObject(aCx, aGlobal, ${constructorProto}, + sNamespaceObjectClass, + interfaceCache, + ${properties}, + ${chromeProperties}, + "${name}", aDefineOnGlobal); + """, + interfaceCache=interfaceCache, + constructorProto=constructorProto, + properties=properties, + chromeProperties=chromeProperties, + name=name, + ) + return CGList( + [ + getConstructorProto, + CGGeneric(call), + ], + "\n", + ).define() + + needInterfacePrototypeObject = ( + self.descriptor.interface.hasInterfacePrototypeObject() + ) + + # If we don't need to create anything, why are we generating this? + assert needInterfaceObject or needInterfacePrototypeObject + + if needInterfacePrototypeObject: + (protoGetter, protoHandleGetter) = InterfacePrototypeObjectProtoGetter( + self.descriptor + ) + if protoHandleGetter is None: + parentProtoType = "Rooted" + getParentProto = "aCx, " + protoGetter + else: + parentProtoType = "Handle" + getParentProto = protoHandleGetter + + getParentProto = fill( + """ + JS::${type}<JSObject*> parentProto(${getParentProto}(aCx)); + if (!parentProto) { + return; + } + """, + type=parentProtoType, + getParentProto=getParentProto, + ) + + protoClass = "&sPrototypeClass" + protoCache = ( + "&aProtoAndIfaceCache.EntrySlotOrCreate(prototypes::id::%s)" + % self.descriptor.name + ) + parentProto = "parentProto" + getParentProto = CGGeneric(getParentProto) + else: + protoClass = "nullptr" + protoCache = "nullptr" + parentProto = "nullptr" + getParentProto = None + + if self.descriptor.interface.ctor(): + constructArgs = methodLength(self.descriptor.interface.ctor()) + isConstructorChromeOnly = isChromeOnly(self.descriptor.interface.ctor()) + else: + constructArgs = 0 + isConstructorChromeOnly = False + if len(self.descriptor.interface.legacyFactoryFunctions) > 0: + legacyFactoryFunctions = "Span(legacyFactoryFunctions)" + else: + legacyFactoryFunctions = "Span<const LegacyFactoryFunction, 0>{}" + + isGlobal = self.descriptor.isGlobal() is not None + + ensureCaches = fill( """ JS::Heap<JSObject*>* protoCache = ${protoCache}; JS::Heap<JSObject*>* interfaceCache = ${interfaceCache}; + """, + protoCache=protoCache, + interfaceCache=interfaceCache, + ) + call = fill( + """ dom::CreateInterfaceObjects(aCx, aGlobal, ${parentProto}, ${protoClass}, protoCache, - ${constructorProto}, ${interfaceClass}, ${constructArgs}, ${isConstructorChromeOnly}, ${legacyFactoryFunctions}, + ${constructorProto}, ${interfaceInfo}, ${constructArgs}, ${isConstructorChromeOnly}, ${legacyFactoryFunctions}, interfaceCache, ${properties}, ${chromeProperties}, "${name}", aDefineOnGlobal, ${unscopableNames}, ${isGlobal}, - ${legacyWindowAliases}, - ${isNamespace}); + ${legacyWindowAliases}); """, protoClass=protoClass, parentProto=parentProto, - protoCache=protoCache, constructorProto=constructorProto, - interfaceClass=interfaceClass, + interfaceInfo=interfaceInfo, constructArgs=constructArgs, isConstructorChromeOnly=toStringBool(isConstructorChromeOnly), legacyFactoryFunctions=legacyFactoryFunctions, - interfaceCache=interfaceCache, properties=properties, chromeProperties=chromeProperties, name=name, @@ -3681,7 +3679,6 @@ class CGCreateInterfaceObjectsMethod(CGAbstractMethod): legacyWindowAliases="legacyWindowAliases" if self.haveLegacyWindowAliases else "nullptr", - isNamespace=toStringBool(self.descriptor.interface.isNamespace()), ) # If we fail after here, we must clear interface and prototype caches @@ -3896,8 +3893,14 @@ class CGCreateInterfaceObjectsMethod(CGAbstractMethod): ) else: defineProtoVar = None + + # ensureCaches needs to come first as it crashes on failure (like OOM). + # We want to make sure that the caches do exist before we try to return + # to the caller, so it can rely on that (and detect other failures by + # checking for null in the caches). return CGList( [ + CGGeneric(ensureCaches), getParentProto, getConstructorProto, CGGeneric(call), @@ -16955,9 +16958,11 @@ class CGDescriptor(CGThing): # done, set up our NativePropertyHooks. cgThings.append(CGNativePropertyHooks(descriptor, properties)) - if descriptor.interface.hasInterfaceObject(): + if descriptor.interface.isNamespace(): + cgThings.append(CGNamespaceObjectJSClass(descriptor)) + elif descriptor.interface.hasInterfaceObject(): cgThings.append(CGClassConstructor(descriptor, descriptor.interface.ctor())) - cgThings.append(CGInterfaceObjectJSClass(descriptor, properties)) + cgThings.append(CGInterfaceObjectInfo(descriptor)) cgThings.append(CGLegacyFactoryFunctions(descriptor)) cgThings.append(CGLegacyCallHook(descriptor)) diff --git a/dom/bindings/Configuration.py b/dom/bindings/Configuration.py index 354a450de6..cfcb2dcf39 100644 --- a/dom/bindings/Configuration.py +++ b/dom/bindings/Configuration.py @@ -43,6 +43,52 @@ class Configuration(DescriptorProvider): the configuration file. """ + class IDLAttrGetterOrSetterTemplate: + class TemplateAdditionalArg: + def __init__(self, type, name, value=None): + self.type = type + self.name = name + self.value = value + + def __init__(self, template, getter, setter, argument, attrName): + self.descriptor = None + self.usedInOtherInterfaces = False + self.getter = getter + self.setter = setter + self.argument = ( + Configuration.IDLAttrGetterOrSetterTemplate.TemplateAdditionalArg( + *argument + ) + ) + self.attrNameString = attrName + self.attr = None + + class TemplateIDLAttribute: + def __init__(self, attr): + assert attr.isAttr() + assert not attr.isMaplikeOrSetlikeAttr() + assert not attr.slotIndices + + self.identifier = attr.identifier + self.type = attr.type + self.extendedAttributes = attr.getExtendedAttributes() + self.slotIndices = None + + def getExtendedAttribute(self, name): + return self.extendedAttributes.get(name) + + def isAttr(self): + return True + + def isMaplikeOrSetlikeAttr(self): + return False + + def isMethod(self): + return False + + def isStatic(self): + return False + def __init__(self, filename, webRoots, parseData, generatedEvents=[]): DescriptorProvider.__init__(self) @@ -51,28 +97,12 @@ class Configuration(DescriptorProvider): exec(io.open(filename, encoding="utf-8").read(), glbl) config = glbl["DOMInterfaces"] - class IDLAttrGetterOrSetterTemplate: - def __init__(self, template, getter, setter, argument, attrName): - class TemplateAdditionalArg: - def __init__(self, type, name, value=None): - self.type = type - self.name = name - self.value = value - - self.descriptor = None - self.usedInOtherInterfaces = False - self.getter = getter - self.setter = setter - self.argument = TemplateAdditionalArg(*argument) - self.attrNameString = attrName - self.attr = None - self.attributeTemplates = dict() attributeTemplatesByInterface = dict() for interface, templates in glbl["TemplatedAttributes"].items(): for template in templates: name = template.get("template") - t = IDLAttrGetterOrSetterTemplate(**template) + t = Configuration.IDLAttrGetterOrSetterTemplate(**template) self.attributeTemplates[name] = t attributeTemplatesByInterface.setdefault(interface, list()).append(t) @@ -351,33 +381,8 @@ class Configuration(DescriptorProvider): ) # This mimics a real IDL attribute for templated bindings. - class TemplateIDLAttribute: - def __init__(self, attr): - assert attr.isAttr() - assert not attr.isMaplikeOrSetlikeAttr() - assert not attr.slotIndices - - self.identifier = attr.identifier - self.type = attr.type - self.extendedAttributes = attr.getExtendedAttributes() - self.slotIndices = None - - def getExtendedAttribute(self, name): - return self.extendedAttributes.get(name) - - def isAttr(self): - return True - - def isMaplikeOrSetlikeAttr(self): - return False - - def isMethod(self): - return False - - def isStatic(self): - return False - template.attr = TemplateIDLAttribute(firstAttribute) + template.attr = Configuration.TemplateIDLAttribute(firstAttribute) def filterExtendedAttributes(extendedAttributes): # These are the extended attributes that we allow to have diff --git a/dom/bindings/DOMJSClass.h b/dom/bindings/DOMJSClass.h index 12055fe6ba..3d8a734140 100644 --- a/dom/bindings/DOMJSClass.h +++ b/dom/bindings/DOMJSClass.h @@ -570,7 +570,7 @@ struct DOMIfaceAndProtoJSClass { // initialization for aggregate/POD types. const JSClass mBase; - // Either eInterface, eNamespace, eInterfacePrototype, + // Either eNamespace, eInterfacePrototype, // eGlobalInterfacePrototype or eNamedPropertiesObject. DOMObjectType mType; // uint8_t @@ -589,25 +589,6 @@ struct DOMIfaceAndProtoJSClass { const JSClass* ToJSClass() const { return &mBase; } }; -// Special JSClass for DOM interface objects. -struct DOMIfaceJSClass : public DOMIfaceAndProtoJSClass { - // Boolean indicating whether this object wants an isInstance property - // pointing to InterfaceIsInstance defined on it. Only ever true for the - // eInterface case. - bool wantsInterfaceIsInstance; - - // The value to return for Function.prototype.toString on this interface - // object. - const char* mFunToString; - - static const DOMIfaceJSClass* FromJSClass(const JSClass* base) { - const DOMIfaceAndProtoJSClass* clazz = - DOMIfaceAndProtoJSClass::FromJSClass(base); - MOZ_ASSERT(clazz->mType == eInterface || clazz->mType == eNamespace); - return static_cast<const DOMIfaceJSClass*>(clazz); - } -}; - class ProtoAndIfaceCache; inline bool DOMGlobalHasProtoAndIFaceCache(JSObject* global) { diff --git a/dom/bindings/JSSlots.h b/dom/bindings/JSSlots.h index c9e9ffed07..54aed1289d 100644 --- a/dom/bindings/JSSlots.h +++ b/dom/bindings/JSSlots.h @@ -23,8 +23,21 @@ #define DOM_INSTANCE_RESERVED_SLOTS 1 // Interface objects store a number of reserved slots equal to -// DOM_INTERFACE_SLOTS_BASE + number of legacy factory functions. -#define DOM_INTERFACE_SLOTS_BASE 0 +// INTERFACE_OBJECT_INFO_RESERVED_SLOT + number of legacy factory functions, +// with a maximum of js::FunctionExtended::NUM_EXTENDED_SLOTS. +// INTERFACE_OBJECT_INFO_RESERVED_SLOT contains the DOMInterfaceInfo. +// INTERFACE_OBJECT_FIRST_LEGACY_FACTORY_FUNCTION and higher contain the +// JSObjects for the legacy factory functions. +enum { + INTERFACE_OBJECT_INFO_RESERVED_SLOT = 0, + INTERFACE_OBJECT_FIRST_LEGACY_FACTORY_FUNCTION, +}; +// See js::FunctionExtended::NUM_EXTENDED_SLOTS. +#define INTERFACE_OBJECT_MAX_SLOTS 3 + +// Legacy factory functions store a JSNativeHolder in the +// LEGACY_FACTORY_FUNCTION_NATIVE_HOLDER_RESERVED_SLOT slot. +enum { LEGACY_FACTORY_FUNCTION_NATIVE_HOLDER_RESERVED_SLOT = 0 }; // Interface prototype objects store a number of reserved slots equal to // DOM_INTERFACE_PROTO_SLOTS_BASE or DOM_INTERFACE_PROTO_SLOTS_BASE + 1 if a diff --git a/dom/bindings/WebIDLGlobalNameHash.cpp b/dom/bindings/WebIDLGlobalNameHash.cpp index 3201f71ffd..24e48a343f 100644 --- a/dom/bindings/WebIDLGlobalNameHash.cpp +++ b/dom/bindings/WebIDLGlobalNameHash.cpp @@ -38,16 +38,21 @@ static JSObject* FindNamedConstructorForXray( return nullptr; } - // This is a call over Xrays, so we will actually use the return value - // (instead of just having it defined on the global now). Check for named - // constructors with this id, in case that's what the caller is asking for. - for (unsigned slot = DOM_INTERFACE_SLOTS_BASE; - slot < JSCLASS_RESERVED_SLOTS(JS::GetClass(interfaceObject)); ++slot) { - JSObject* constructor = - &JS::GetReservedSlot(interfaceObject, slot).toObject(); - if (JS_GetMaybePartialFunctionId(JS_GetObjectFunction(constructor)) == - aId.toString()) { - return constructor; + if (IsInterfaceObject(interfaceObject)) { + // This is a call over Xrays, so we will actually use the return value + // (instead of just having it defined on the global now). Check for named + // constructors with this id, in case that's what the caller is asking for. + for (unsigned slot = INTERFACE_OBJECT_FIRST_LEGACY_FACTORY_FUNCTION; + slot < INTERFACE_OBJECT_MAX_SLOTS; ++slot) { + const JS::Value& v = js::GetFunctionNativeReserved(interfaceObject, slot); + if (!v.isObject()) { + break; + } + JSObject* constructor = &v.toObject(); + if (JS_GetMaybePartialFunctionId(JS_GetObjectFunction(constructor)) == + aId.toString()) { + return constructor; + } } } diff --git a/dom/bindings/mozwebidlcodegen/__init__.py b/dom/bindings/mozwebidlcodegen/__init__.py index 3e8c6aa420..bba95edb78 100644 --- a/dom/bindings/mozwebidlcodegen/__init__.py +++ b/dom/bindings/mozwebidlcodegen/__init__.py @@ -11,7 +11,8 @@ import io import json import logging import os -from copy import deepcopy +import sys +from multiprocessing import Pool import mozpack.path as mozpath from mach.mixin.logging import LoggingMixin @@ -22,6 +23,50 @@ from mozbuild.util import FileAvoidWrite # There are various imports in this file in functions to avoid adding # dependencies to config.status. See bug 949875. +# Limit the count on Windows, because of bug 1889842 and also the +# inefficiency of fork on Windows. +DEFAULT_PROCESS_COUNT = 4 if sys.platform == "win32" else os.cpu_count() + + +class WebIDLPool: + """ + Distribute generation load across several processes, avoiding redundant state + copies. + """ + + GeneratorState = None + + def __init__(self, GeneratorState, *, processes=None): + if processes is None: + processes = DEFAULT_PROCESS_COUNT + + # As a special case, don't spawn an extra process if processes=1 + if processes == 1: + WebIDLPool._init(GeneratorState) + + class SeqPool: + def map(self, *args): + return list(map(*args)) + + self.pool = SeqPool() + else: + self.pool = Pool( + initializer=WebIDLPool._init, + initargs=(GeneratorState,), + processes=processes, + ) + + def run(self, filenames): + return self.pool.map(WebIDLPool._run, filenames) + + @staticmethod + def _init(GeneratorState): + WebIDLPool.GeneratorState = GeneratorState + + @staticmethod + def _run(filename): + return WebIDLPool.GeneratorState._generate_build_files_for_webidl(filename) + class BuildResult(object): """Represents the result of processing WebIDL files. @@ -50,6 +95,9 @@ class WebIDLCodegenManagerState(dict): state should be considered a black box to everyone except WebIDLCodegenManager. But we'll still document it. + Any set stored in this dict should be copied and sorted in the `dump()` + method. + Fields: version @@ -117,12 +165,15 @@ class WebIDLCodegenManagerState(dict): def dump(self, fh): """Dump serialized state to a file handle.""" - normalized = deepcopy(self) + normalized = self.copy() + webidls = normalized["webidls"] = self["webidls"].copy() for k, v in self["webidls"].items(): + webidls_k = webidls[k] = v.copy() + # Convert sets to lists because JSON doesn't support sets. - normalized["webidls"][k]["outputs"] = sorted(v["outputs"]) - normalized["webidls"][k]["inputs"] = sorted(v["inputs"]) + webidls_k["outputs"] = sorted(v["outputs"]) + webidls_k["inputs"] = sorted(v["inputs"]) normalized["dictionaries_convertible_to_js"] = sorted( self["dictionaries_convertible_to_js"] @@ -251,7 +302,7 @@ class WebIDLCodegenManager(LoggingMixin): return self._config - def generate_build_files(self): + def generate_build_files(self, *, processes=None): """Generate files required for the build. This function is in charge of generating all the .h/.cpp files derived @@ -279,6 +330,9 @@ class WebIDLCodegenManager(LoggingMixin): file. 3. If an output file is missing, ensure it is present by performing necessary regeneration. + + if `processes` is set to None, run in parallel using the + multiprocess.Pool default. If set to 1, don't use extra processes. """ # Despite #1 above, we assume the build system is smart enough to not # invoke us if nothing has changed. Therefore, any invocation means @@ -311,11 +365,20 @@ class WebIDLCodegenManager(LoggingMixin): d.identifier.name for d in self._config.getDictionariesConvertibleFromJS() ) + # Distribute the generation load across several processes. This requires + # a) that `self' is serializable and b) that `self' is unchanged by + # _generate_build_files_for_webidl(...) + ordered_changed_inputs = sorted(changed_inputs) + pool = WebIDLPool(self, processes=processes) + generation_results = pool.run(ordered_changed_inputs) + # Generate bindings from .webidl files. - for filename in sorted(changed_inputs): + for filename, generation_result in zip( + ordered_changed_inputs, generation_results + ): basename = mozpath.basename(filename) result.inputs.add(filename) - written, deps = self._generate_build_files_for_webidl(filename) + written, deps = generation_result result.created |= written[0] result.updated |= written[1] result.unchanged |= written[2] @@ -560,6 +623,8 @@ class WebIDLCodegenManager(LoggingMixin): return paths + # Parallelization of the generation step relies on this method not changing + # the internal state of the object def _generate_build_files_for_webidl(self, filename): from Codegen import CGBindingRoot, CGEventRoot diff --git a/dom/bindings/mozwebidlcodegen/test/test_mozwebidlcodegen.py b/dom/bindings/mozwebidlcodegen/test/test_mozwebidlcodegen.py index cd822af538..46a3ab6239 100644 --- a/dom/bindings/mozwebidlcodegen/test/test_mozwebidlcodegen.py +++ b/dom/bindings/mozwebidlcodegen/test/test_mozwebidlcodegen.py @@ -247,18 +247,21 @@ class TestWebIDLCodegenManager(unittest.TestCase): args = self._get_manager_args() m1 = WebIDLCodegenManager(**args) with MockedOpen({fake_path: "# Original content"}): + # MockedOpen is not compatible with distributed filesystem + # access, so force the number of processes used to generate + # files to 1. try: - result = m1.generate_build_files() + result = m1.generate_build_files(processes=1) l = len(result.inputs) with io.open(fake_path, "wt", newline="\n") as fh: fh.write("# Modified content") m2 = WebIDLCodegenManager(**args) - result = m2.generate_build_files() + result = m2.generate_build_files(processes=1) self.assertEqual(len(result.inputs), l) - result = m2.generate_build_files() + result = m2.generate_build_files(processes=1) self.assertEqual(len(result.inputs), 0) finally: del sys.modules["mozwebidlcodegen.fakemodule"] diff --git a/dom/bindings/nsIScriptError.idl b/dom/bindings/nsIScriptError.idl index 3d7925a08d..b650de181c 100644 --- a/dom/bindings/nsIScriptError.idl +++ b/dom/bindings/nsIScriptError.idl @@ -91,7 +91,7 @@ interface nsIScriptError : nsIConsoleMessage // Error created from a Promise rejection. readonly attribute boolean isPromiseRejection; - [noscript] void initIsPromiseRejection(in bool isPromiseRejection); + [noscript] void initIsPromiseRejection(in boolean isPromiseRejection); // The "exception" value generated when an uncaught exception is thrown // by JavaScript. This can be any value, e.g. @@ -135,8 +135,8 @@ interface nsIScriptError : nsIConsoleMessage in uint32_t columnNumber, in uint32_t flags, in ACString category, - [optional] in bool fromPrivateWindow, - [optional] in bool fromChromeContext); + [optional] in boolean fromPrivateWindow, + [optional] in boolean fromChromeContext); /* This should be called instead of nsIScriptError.init to * initialize with a window id. The window id should be for the @@ -158,7 +158,7 @@ interface nsIScriptError : nsIConsoleMessage in uint32_t flags, in ACString category, in unsigned long long innerWindowID, - [optional] in bool fromChromeContext); + [optional] in boolean fromChromeContext); /* This is the same function as initWithWindowID, but it expects an already * sanitized sourceName. @@ -172,7 +172,7 @@ interface nsIScriptError : nsIConsoleMessage in uint32_t flags, in ACString category, in unsigned long long innerWindowID, - [optional] in bool fromChromeContext); + [optional] in boolean fromChromeContext); /* This is the same function as initWithWindowID with an uri as a source parameter. */ @@ -184,7 +184,7 @@ interface nsIScriptError : nsIConsoleMessage in uint32_t flags, in ACString category, in unsigned long long innerWindowID, - [optional] in bool fromChromeContext); + [optional] in boolean fromChromeContext); /* Initialize the script source ID in a new error. */ void initSourceId(in uint32_t sourceId); diff --git a/dom/bindings/parser/WebIDL.py b/dom/bindings/parser/WebIDL.py index 43f9ec12f1..9a3a77f075 100644 --- a/dom/bindings/parser/WebIDL.py +++ b/dom/bindings/parser/WebIDL.py @@ -121,6 +121,8 @@ class Location(object): class BuiltinLocation(object): + __slots__ = "msg", "filename" + def __init__(self, text): self.msg = text + "\n" self.filename = "<builtin>" @@ -142,9 +144,11 @@ class BuiltinLocation(object): class IDLObject(object): + __slots__ = "location", "userData", "filename" + def __init__(self, location): self.location = location - self.userData = dict() + self.userData = {} self.filename = location and location.filename def isInterface(self): @@ -220,6 +224,8 @@ class IDLObject(object): class IDLScope(IDLObject): + __slots__ = "parentScope", "_name", "_dict", "globalNames", "globalNameMapping" + def __init__(self, location, parentScope, identifier): IDLObject.__init__(self, location) @@ -349,6 +355,8 @@ class IDLScope(IDLObject): class IDLIdentifier(IDLObject): + __slots__ = "name", "scope" + def __init__(self, location, scope, name): IDLObject.__init__(self, location) @@ -373,12 +381,14 @@ class IDLIdentifier(IDLObject): class IDLUnresolvedIdentifier(IDLObject): + __slots__ = ("name",) + def __init__( self, location, name, allowDoubleUnderscore=False, allowForbidden=False ): IDLObject.__init__(self, location) - assert len(name) > 0 + assert name if name == "__noSuchMethod__": raise WebIDLError("__noSuchMethod__ is deprecated", [location]) @@ -417,6 +427,7 @@ class IDLUnresolvedIdentifier(IDLObject): class IDLObjectWithIdentifier(IDLObject): + # no slots, incompatible with multiple inheritance def __init__(self, location, parentScope, identifier): IDLObject.__init__(self, location) @@ -434,6 +445,8 @@ class IDLObjectWithIdentifier(IDLObject): class IDLObjectWithScope(IDLObjectWithIdentifier, IDLScope): + __slots__ = () + def __init__(self, location, parentScope, identifier): assert isinstance(identifier, IDLUnresolvedIdentifier) @@ -442,6 +455,8 @@ class IDLObjectWithScope(IDLObjectWithIdentifier, IDLScope): class IDLIdentifierPlaceholder(IDLObjectWithIdentifier): + __slots__ = () + def __init__(self, location, identifier): assert isinstance(identifier, IDLUnresolvedIdentifier) IDLObjectWithIdentifier.__init__(self, location, None, identifier) @@ -459,6 +474,7 @@ class IDLIdentifierPlaceholder(IDLObjectWithIdentifier): class IDLExposureMixins: + # no slots, incompatible with multiple inheritance def __init__(self, location): # _exposureGlobalNames are the global names listed in our [Exposed] # extended attribute. exposureSet is the exposure set as defined in the @@ -541,6 +557,8 @@ class IDLExposureMixins: class IDLExternalInterface(IDLObjectWithIdentifier): + __slots__ = ("parent",) + def __init__(self, location, parentScope, identifier): assert isinstance(identifier, IDLUnresolvedIdentifier) assert isinstance(parentScope, IDLScope) @@ -591,6 +609,8 @@ class IDLExternalInterface(IDLObjectWithIdentifier): class IDLPartialDictionary(IDLObject): + __slots__ = "identifier", "members", "_nonPartialDictionary", "_finished" + def __init__(self, location, name, members, nonPartialDictionary): assert isinstance(name, IDLUnresolvedIdentifier) @@ -619,6 +639,15 @@ class IDLPartialDictionary(IDLObject): class IDLPartialInterfaceOrNamespace(IDLObject): + __slots__ = ( + "identifier", + "members", + "propagatedExtendedAttrs", + "_haveSecureContextExtendedAttribute", + "_nonPartialInterfaceOrNamespace", + "_finished", + ) + def __init__(self, location, name, members, nonPartialInterfaceOrNamespace): assert isinstance(name, IDLUnresolvedIdentifier) @@ -726,12 +755,22 @@ def globalNameSetToExposureSet(globalScope, nameSet, exposureSet): # we use a special class to be able to store them both in the scope for the # same identifier. class IDLOperations: + __slots__ = "static", "regular" + def __init__(self, static=None, regular=None): self.static = static self.regular = regular class IDLInterfaceOrInterfaceMixinOrNamespace(IDLObjectWithScope, IDLExposureMixins): + __slots__ = ( + "_finished", + "members", + "_partials", + "_extendedAttrDict", + "_isKnownNonPartial", + ) + def __init__(self, location, parentScope, name): assert isinstance(parentScope, IDLScope) assert isinstance(name, IDLUnresolvedIdentifier) @@ -897,10 +936,12 @@ class IDLInterfaceOrInterfaceMixinOrNamespace(IDLObjectWithScope, IDLExposureMix class IDLInterfaceMixin(IDLInterfaceOrInterfaceMixinOrNamespace): + __slots__ = ("actualExposureGlobalNames",) + def __init__(self, location, parentScope, name, members, isKnownNonPartial): self.actualExposureGlobalNames = set() - assert isKnownNonPartial or len(members) == 0 + assert isKnownNonPartial or not members IDLInterfaceOrInterfaceMixinOrNamespace.__init__( self, location, parentScope, name ) @@ -1001,9 +1042,27 @@ class IDLInterfaceMixin(IDLInterfaceOrInterfaceMixinOrNamespace): class IDLInterfaceOrNamespace(IDLInterfaceOrInterfaceMixinOrNamespace): + __slots__ = ( + "parent", + "_callback", + "maplikeOrSetlikeOrIterable", + "legacyFactoryFunctions", + "legacyWindowAliases", + "includedMixins", + "interfacesBasedOnSelf", + "_hasChildInterfaces", + "_isOnGlobalProtoChain", + "totalMembersInSlots", + "_ownMembersInSlots", + "iterableInterface", + "asyncIterableInterface", + "hasCrossOriginMembers", + "hasDescendantWithCrossOriginMembers", + ) + def __init__(self, location, parentScope, name, parent, members, isKnownNonPartial): assert isKnownNonPartial or not parent - assert isKnownNonPartial or len(members) == 0 + assert isKnownNonPartial or not members self.parent = None self._callback = False @@ -1011,13 +1070,13 @@ class IDLInterfaceOrNamespace(IDLInterfaceOrInterfaceMixinOrNamespace): # legacyFactoryFunctions needs deterministic ordering because bindings code # outputs the constructs in the order that legacyFactoryFunctions enumerates # them. - self.legacyFactoryFunctions = list() + self.legacyFactoryFunctions = [] self.legacyWindowAliases = [] self.includedMixins = set() # self.interfacesBasedOnSelf is the set of interfaces that inherit from # self, including self itself. # Used for distinguishability checking. - self.interfacesBasedOnSelf = set([self]) + self.interfacesBasedOnSelf = {self} self._hasChildInterfaces = False self._isOnGlobalProtoChain = False @@ -1885,6 +1944,8 @@ class IDLInterfaceOrNamespace(IDLInterfaceOrInterfaceMixinOrNamespace): class IDLInterface(IDLInterfaceOrNamespace): + __slots__ = ("classNameOverride",) + def __init__( self, location, @@ -2103,6 +2164,8 @@ class IDLInterface(IDLInterfaceOrNamespace): class IDLNamespace(IDLInterfaceOrNamespace): + __slots__ = () + def __init__(self, location, parentScope, name, members, isKnownNonPartial): IDLInterfaceOrNamespace.__init__( self, location, parentScope, name, None, members, isKnownNonPartial @@ -2161,6 +2224,16 @@ class IDLNamespace(IDLInterfaceOrNamespace): class IDLDictionary(IDLObjectWithScope): + __slots__ = ( + "parent", + "_finished", + "members", + "_partialDictionaries", + "_extendedAttrDict", + "needsConversionToJS", + "needsConversionFromJS", + ) + def __init__(self, location, parentScope, name, parent, members): assert isinstance(parentScope, IDLScope) assert isinstance(name, IDLUnresolvedIdentifier) @@ -2388,6 +2461,8 @@ class IDLDictionary(IDLObjectWithScope): class IDLEnum(IDLObjectWithIdentifier): + __slots__ = ("_values",) + def __init__(self, location, parentScope, name, values): assert isinstance(parentScope, IDLScope) assert isinstance(name, IDLUnresolvedIdentifier) @@ -2462,6 +2537,16 @@ class IDLType(IDLObject): "observablearray", ) + __slots__ = ( + "name", + "builtin", + "legacyNullToEmptyString", + "_clamp", + "_enforceRange", + "_allowShared", + "_extendedAttrDict", + ) + def __init__(self, location, name): IDLObject.__init__(self, location) self.name = name @@ -2662,6 +2747,8 @@ class IDLUnresolvedType(IDLType): Unresolved types are interface types """ + __slots__ = ("extraTypeAttributes",) + def __init__(self, location, name, attrs=[]): IDLType.__init__(self, location, name) self.extraTypeAttributes = attrs @@ -2702,6 +2789,8 @@ class IDLUnresolvedType(IDLType): class IDLParametrizedType(IDLType): + __slots__ = "builtin", "inner" + def __init__(self, location, name, innerType): IDLType.__init__(self, location, name) self.builtin = False @@ -2725,6 +2814,8 @@ class IDLParametrizedType(IDLType): class IDLNullableType(IDLParametrizedType): + __slots__ = () + def __init__(self, location, innerType): assert not innerType == BuiltinTypes[IDLBuiltinType.Types.any] @@ -2900,6 +2991,8 @@ class IDLNullableType(IDLParametrizedType): class IDLSequenceType(IDLParametrizedType): + __slots__ = ("name",) + def __init__(self, location, parameterType): assert not parameterType.isUndefined() @@ -2960,6 +3053,8 @@ class IDLSequenceType(IDLParametrizedType): class IDLRecordType(IDLParametrizedType): + __slots__ = "keyType", "name" + def __init__(self, location, keyType, valueType): assert keyType.isString() assert keyType.isComplete() @@ -3038,6 +3133,8 @@ class IDLRecordType(IDLParametrizedType): class IDLObservableArrayType(IDLParametrizedType): + __slots__ = () + def __init__(self, location, innerType): assert not innerType.isUndefined() IDLParametrizedType.__init__(self, location, None, innerType) @@ -3104,6 +3201,14 @@ class IDLObservableArrayType(IDLParametrizedType): class IDLUnionType(IDLType): + __slots__ = ( + "memberTypes", + "hasNullableType", + "_dictionaryType", + "flatMemberTypes", + "builtin", + ) + def __init__(self, location, memberTypes): IDLType.__init__(self, location, "") self.memberTypes = memberTypes @@ -3247,6 +3352,8 @@ class IDLUnionType(IDLType): class IDLTypedefType(IDLType): + __slots__ = "inner", "builtin" + def __init__(self, location, innerType, name): IDLType.__init__(self, location, name) self.inner = innerType @@ -3354,6 +3461,8 @@ class IDLTypedefType(IDLType): class IDLTypedef(IDLObjectWithIdentifier): + __slots__ = ("innerType",) + def __init__(self, location, parentScope, innerType, name): # Set self.innerType first, because IDLObjectWithIdentifier.__init__ # will call our __str__, which wants to use it. @@ -3386,6 +3495,8 @@ class IDLTypedef(IDLObjectWithIdentifier): class IDLWrapperType(IDLType): + __slots__ = "inner", "_identifier", "builtin" + def __init__(self, location, inner): IDLType.__init__(self, location, inner.identifier.name) self.inner = inner @@ -3582,6 +3693,8 @@ class IDLWrapperType(IDLType): class IDLPromiseType(IDLParametrizedType): + __slots__ = () + def __init__(self, location, innerType): IDLParametrizedType.__init__(self, location, "Promise", innerType) @@ -3745,6 +3858,14 @@ class IDLBuiltinType(IDLType): Types.Float64Array: "Float64Array", } + __slots__ = ( + "_typeTag", + "_clamped", + "_rangeEnforced", + "_withLegacyNullToEmptyString", + "_withAllowShared", + ) + def __init__( self, location, @@ -4242,6 +4363,11 @@ class NoCoercionFoundError(WebIDLError): class IDLValue(IDLObject): + __slots__ = ( + "type", + "value", + ) + def __init__(self, location, type, value): IDLObject.__init__(self, location) self.type = type @@ -4374,6 +4500,8 @@ class IDLValue(IDLObject): class IDLNullValue(IDLObject): + __slots__ = "type", "value" + def __init__(self, location): IDLObject.__init__(self, location) self.type = None @@ -4403,6 +4531,8 @@ class IDLNullValue(IDLObject): class IDLEmptySequenceValue(IDLObject): + __slots__ = "type", "value" + def __init__(self, location): IDLObject.__init__(self, location) self.type = None @@ -4433,6 +4563,8 @@ class IDLEmptySequenceValue(IDLObject): class IDLDefaultDictionaryValue(IDLObject): + __slots__ = "type", "value" + def __init__(self, location): IDLObject.__init__(self, location) self.type = None @@ -4463,6 +4595,8 @@ class IDLDefaultDictionaryValue(IDLObject): class IDLUndefinedValue(IDLObject): + __slots__ = "type", "value" + def __init__(self, location): IDLObject.__init__(self, location) self.type = None @@ -4492,6 +4626,7 @@ class IDLInterfaceMember(IDLObjectWithIdentifier, IDLExposureMixins): AffectsValues = ("Nothing", "Everything") DependsOnValues = ("Nothing", "DOMState", "DeviceState", "Everything") + # no slots : multiple inheritance def __init__(self, location, identifier, tag, extendedAttrDict=None): IDLObjectWithIdentifier.__init__(self, location, None, identifier) IDLExposureMixins.__init__(self, location) @@ -4611,6 +4746,14 @@ class IDLInterfaceMember(IDLObjectWithIdentifier, IDLExposureMixins): class IDLMaplikeOrSetlikeOrIterableBase(IDLInterfaceMember): + __slots__ = ( + "keyType", + "valueType", + "maplikeOrSetlikeOrIterableType", + "disallowedMemberNames", + "disallowedNonMethodNames", + ) + def __init__(self, location, identifier, ifaceType, keyType, valueType, ifaceKind): IDLInterfaceMember.__init__(self, location, identifier, ifaceKind) if keyType is not None: @@ -4827,6 +4970,8 @@ class IDLMaplikeOrSetlikeOrIterableBase(IDLInterfaceMember): # Iterable adds ES6 iterator style functions and traits # (keys/values/entries/@@iterator) to an interface. class IDLIterable(IDLMaplikeOrSetlikeOrIterableBase): + __slots__ = ("iteratorType",) + def __init__(self, location, identifier, keyType, valueType, scope): IDLMaplikeOrSetlikeOrIterableBase.__init__( self, @@ -4902,6 +5047,8 @@ class IDLIterable(IDLMaplikeOrSetlikeOrIterableBase): class IDLAsyncIterable(IDLMaplikeOrSetlikeOrIterableBase): + __slots__ = "iteratorType", "argList" + def __init__(self, location, identifier, keyType, valueType, argList, scope): for arg in argList: if not arg.optional: @@ -4986,6 +5133,8 @@ class IDLAsyncIterable(IDLMaplikeOrSetlikeOrIterableBase): # MaplikeOrSetlike adds ES6 map-or-set-like traits to an interface. class IDLMaplikeOrSetlike(IDLMaplikeOrSetlikeOrIterableBase): + __slots__ = "readonly", "slotIndices", "prefix" + def __init__( self, location, identifier, maplikeOrSetlikeType, readonly, keyType, valueType ): @@ -5153,6 +5302,8 @@ class IDLMaplikeOrSetlike(IDLMaplikeOrSetlikeOrIterableBase): class IDLConst(IDLInterfaceMember): + __slots__ = "type", "value" + def __init__(self, location, identifier, type, value): IDLInterfaceMember.__init__( self, location, identifier, IDLInterfaceMember.Tags.Const @@ -5225,6 +5376,21 @@ class IDLConst(IDLInterfaceMember): class IDLAttribute(IDLInterfaceMember): + __slots__ = ( + "type", + "readonly", + "inherit", + "_static", + "legacyLenientThis", + "_legacyUnforgeable", + "stringifier", + "slotIndices", + "maplikeOrSetlike", + "dependsOn", + "affects", + "bindingAliases", + ) + def __init__( self, location, @@ -5827,6 +5993,18 @@ class IDLAttribute(IDLInterfaceMember): class IDLArgument(IDLObjectWithIdentifier): + __slots__ = ( + "type", + "optional", + "defaultValue", + "variadic", + "dictionaryMember", + "_isComplete", + "_allowTreatNonCallableAsNull", + "_extendedAttrDict", + "allowTypeAttributes", + ) + def __init__( self, location, @@ -5970,6 +6148,15 @@ class IDLArgument(IDLObjectWithIdentifier): class IDLCallback(IDLObjectWithScope): + __slots__ = ( + "_returnType", + "_arguments", + "_treatNonCallableAsNull", + "_treatNonObjectAsNull", + "_isRunScriptBoundary", + "_isConstructor", + ) + def __init__( self, location, parentScope, identifier, returnType, arguments, isConstructor ): @@ -6067,6 +6254,8 @@ class IDLCallback(IDLObjectWithScope): class IDLCallbackType(IDLType): + __slots__ = ("callback",) + def __init__(self, location, callback): IDLType.__init__(self, location, callback.identifier.name) self.callback = callback @@ -6109,6 +6298,8 @@ class IDLMethodOverload: the full set of overloads. """ + __slots__ = "returnType", "arguments", "location" + def __init__(self, returnType, arguments, location): self.returnType = returnType # Clone the list of arguments, just in case @@ -6131,6 +6322,25 @@ class IDLMethod(IDLInterfaceMember, IDLScope): NamedOrIndexed = enum("Neither", "Named", "Indexed") + __slots__ = ( + "_hasOverloads", + "_overloads", + "_static", + "_getter", + "_setter", + "_deleter", + "_legacycaller", + "_stringifier", + "maplikeOrSetlikeOrIterable", + "_htmlConstructor", + "underlyingAttr", + "_specialType", + "_legacyUnforgeable", + "dependsOn", + "affects", + "aliases", + ) + def __init__( self, location, @@ -6797,6 +7007,14 @@ class IDLMethod(IDLInterfaceMember, IDLScope): class IDLConstructor(IDLMethod): + __slots__ = ( + "_initLocation", + "_initArgs", + "_initName", + "_inited", + "_initExtendedAttrs", + ) + def __init__(self, location, args, name): # We can't actually init our IDLMethod yet, because we do not know the # return type yet. Just save the info we have for now and we will init @@ -6866,6 +7084,8 @@ class IDLConstructor(IDLMethod): class IDLIncludesStatement(IDLObject): + __slots__ = ("interface", "mixin", "_finished") + def __init__(self, location, interface, mixin): IDLObject.__init__(self, location) self.interface = interface @@ -6922,6 +7142,8 @@ class IDLExtendedAttribute(IDLObject): A class to represent IDL extended attributes so we can give them locations """ + __slots__ = ("_tuple",) + def __init__(self, location, tuple): IDLObject.__init__(self, location) self._tuple = tuple diff --git a/dom/bindings/test/TestBindingHeader.h b/dom/bindings/test/TestBindingHeader.h index 77053d9ba6..2e8c496758 100644 --- a/dom/bindings/test/TestBindingHeader.h +++ b/dom/bindings/test/TestBindingHeader.h @@ -131,29 +131,6 @@ class TestInterface : public nsISupports, public nsWrapperCache { JS::Handle<JS::Value>, const Optional<JS::Handle<JSObject*>>&, const Optional<JS::Handle<JSObject*>>&, ErrorResult&); - static already_AddRefed<TestInterface> Test3(const GlobalObject&, - const LongOrStringAnyRecord&, - ErrorResult&); - - static already_AddRefed<TestInterface> Test4( - const GlobalObject&, const Record<nsString, Record<nsString, JS::Value>>&, - ErrorResult&); - - static already_AddRefed<TestInterface> Test5( - const GlobalObject&, - const Record< - nsString, - Sequence<Record<nsString, - Record<nsString, Sequence<Sequence<JS::Value>>>>>>&, - ErrorResult&); - - static already_AddRefed<TestInterface> Test6( - const GlobalObject&, - const Sequence<Record< - nsCString, - Sequence<Sequence<Record<nsCString, Record<nsString, JS::Value>>>>>>&, - ErrorResult&); - // Integer types int8_t ReadonlyByte(); int8_t WritableByte(); @@ -1394,6 +1371,48 @@ class TestInterface : public nsISupports, public nsWrapperCache { void PassString(OwningNonNull<nsAString>&) = delete; }; +class TestLegacyFactoryFunctionInterface : public nsISupports, + public nsWrapperCache { + public: + NS_DECL_ISUPPORTS + + // We need a GetParentObject to make binding codegen happy + virtual nsISupports* GetParentObject(); + + // And now our actual WebIDL API + static already_AddRefed<TestLegacyFactoryFunctionInterface> Test3( + const GlobalObject&, const LongOrStringAnyRecord&, ErrorResult&); + + static already_AddRefed<TestLegacyFactoryFunctionInterface> Test4( + const GlobalObject&, const Record<nsString, Record<nsString, JS::Value>>&, + ErrorResult&); +}; + +class TestLegacyFactoryFunctionInterface2 : public nsISupports, + public nsWrapperCache { + public: + NS_DECL_ISUPPORTS + + // We need a GetParentObject to make binding codegen happy + virtual nsISupports* GetParentObject(); + + // And now our actual WebIDL API + static already_AddRefed<TestLegacyFactoryFunctionInterface2> Test5( + const GlobalObject&, + const Record< + nsString, + Sequence<Record<nsString, + Record<nsString, Sequence<Sequence<JS::Value>>>>>>&, + ErrorResult&); + + static already_AddRefed<TestLegacyFactoryFunctionInterface2> Test6( + const GlobalObject&, + const Sequence<Record< + nsCString, + Sequence<Sequence<Record<nsCString, Record<nsString, JS::Value>>>>>>&, + ErrorResult&); +}; + class TestIndexedGetterInterface : public nsISupports, public nsWrapperCache { public: NS_DECL_ISUPPORTS diff --git a/dom/bindings/test/TestCodeGen.webidl b/dom/bindings/test/TestCodeGen.webidl index 827d9c35a7..0d186cd682 100644 --- a/dom/bindings/test/TestCodeGen.webidl +++ b/dom/bindings/test/TestCodeGen.webidl @@ -162,10 +162,6 @@ enum OnlyForUseInInnerUnion { LegacyFactoryFunction=Test2(DictForConstructor dict, any any1, object obj1, object? obj2, sequence<Dict> seq, optional any any2, optional object obj3, optional object? obj4), - LegacyFactoryFunction=Test3((long or record<DOMString, any>) arg1), - LegacyFactoryFunction=Test4(record<DOMString, record<DOMString, any>> arg1), - LegacyFactoryFunction=Test5(record<DOMString, sequence<record<DOMString, record<DOMString, sequence<sequence<any>>>>>> arg1), - LegacyFactoryFunction=Test6(sequence<record<ByteString, sequence<sequence<record<ByteString, record<USVString, any>>>>>> arg1), Exposed=Window] interface TestInterface { constructor(); @@ -1087,7 +1083,19 @@ interface TestInterface { undefined passUnionArrayBuffer((DOMString or ArrayBuffer) foo); undefined passUnionAllowSharedArrayBuffer((DOMString or [AllowShared] ArrayBuffer) foo); - // If you add things here, add them to TestExampleGen and TestJSImplGen as well + // If you add things here, add them to TestExampleGen as well +}; + +[LegacyFactoryFunction=Test3((long or record<DOMString, any>) arg1), + LegacyFactoryFunction=Test4(record<DOMString, record<DOMString, any>> arg1), + Exposed=Window] +interface TestLegacyFactoryFunctionInterface { +}; + +[LegacyFactoryFunction=Test5(record<DOMString, sequence<record<DOMString, record<DOMString, sequence<sequence<any>>>>>> arg1), + LegacyFactoryFunction=Test6(sequence<record<ByteString, sequence<sequence<record<ByteString, record<USVString, any>>>>>> arg1), + Exposed=Window] +interface TestLegacyFactoryFunctionInterface2 { }; [Exposed=Window] diff --git a/dom/bindings/test/TestExampleGen.webidl b/dom/bindings/test/TestExampleGen.webidl index 3d90e2bf9b..65e9840cec 100644 --- a/dom/bindings/test/TestExampleGen.webidl +++ b/dom/bindings/test/TestExampleGen.webidl @@ -870,7 +870,10 @@ interface TestExampleInterface { undefined passUnionArrayBuffer((DOMString or ArrayBuffer) foo); undefined passUnionAllowSharedArrayBuffer((DOMString or [AllowShared] ArrayBuffer) foo); - // If you add things here, add them to TestCodeGen and TestJSImplGen as well + // If you add things here, add them to TestExampleGen. If they need to be + // supported in JS-implemented WebIDL then you need to add them to + // TestJSImplGen as well, if they are not supported in JS-implemented WebIDL + // then the codegen should throw for that specific case. }; [Exposed=Window] diff --git a/dom/bindings/test/test_dom_xrays.html b/dom/bindings/test/test_dom_xrays.html index 6d65ab7315..eafa0c632c 100644 --- a/dom/bindings/test/test_dom_xrays.html +++ b/dom/bindings/test/test_dom_xrays.html @@ -176,6 +176,27 @@ function test() { // ECMAScript-defined properties live on the prototype, overriding any named properties. checkXrayProperty(coll, "toString", [ undefined, undefined, win.Object.prototype.toString ]); + // Constructors + img = new win.Image(); + ok(win.HTMLImageElement.isInstance(img), "Constructor created the right type of object"); + + let threw; + try { + threw = false; + win.Image(); + } catch (e) { + threw = true; + } + ok(threw, "Constructors should throw when called without new"); + + try { + threw = false; + new win.Node(); + } catch (e) { + threw = true; + } + ok(threw, "Constructing an interface without a constructor should throw"); + // Frozen arrays should come from our compartment, not the target one. var languages1 = win.navigator.languages; isnot(languages1, undefined, "Must have .languages"); @@ -358,7 +379,7 @@ function test() { // legacyCaller should work. ok(win.HTMLAllCollection.isInstance(doc.all), "HTMLDocument.all should be an instance of HTMLAllCollection"); - let element, threw; + let element; try { threw = false; element = doc.all(0); diff --git a/dom/cache/Cache.cpp b/dom/cache/Cache.cpp index 3f84e08e66..78480022c1 100644 --- a/dom/cache/Cache.cpp +++ b/dom/cache/Cache.cpp @@ -34,12 +34,11 @@ namespace { enum class PutStatusPolicy { Default, RequireOK }; -bool IsValidPutRequestURL(const nsAString& aUrl, ErrorResult& aRv) { +bool IsValidPutRequestURL(const nsACString& aUrl, ErrorResult& aRv) { bool validScheme = false; // make a copy because ProcessURL strips the fragmet - NS_ConvertUTF16toUTF8 url(aUrl); - + nsAutoCString url(aUrl); TypeUtils::ProcessURL(url, &validScheme, nullptr, nullptr, aRv); if (aRv.Failed()) { return false; @@ -47,8 +46,7 @@ bool IsValidPutRequestURL(const nsAString& aUrl, ErrorResult& aRv) { if (!validScheme) { // `url` has been modified, so don't use it here. - aRv.ThrowTypeError<MSG_INVALID_URL_SCHEME>("Request", - NS_ConvertUTF16toUTF8(aUrl)); + aRv.ThrowTypeError<MSG_INVALID_URL_SCHEME>("Request", aUrl); return false; } @@ -66,7 +64,7 @@ static bool IsValidPutRequestMethod(const Request& aRequest, ErrorResult& aRv) { return true; } -static bool IsValidPutRequestMethod(const RequestOrUSVString& aRequest, +static bool IsValidPutRequestMethod(const RequestOrUTF8String& aRequest, ErrorResult& aRv) { // If the provided request is a string URL, then it will default to // a valid http method automatically. @@ -81,12 +79,10 @@ static bool IsValidPutResponseStatus(Response& aResponse, ErrorResult& aRv) { if ((aPolicy == PutStatusPolicy::RequireOK && !aResponse.Ok()) || aResponse.Status() == 206) { - nsAutoString url; + nsAutoCString url; aResponse.GetUrl(url); - aRv.ThrowTypeError<MSG_CACHE_ADD_FAILED_RESPONSE>( - GetEnumString(aResponse.Type()), IntToCString(aResponse.Status()), - NS_ConvertUTF16toUTF8(url)); + GetEnumString(aResponse.Type()), IntToCString(aResponse.Status()), url); return false; } @@ -228,7 +224,7 @@ Cache::Cache(nsIGlobalObject* aGlobal, CacheChild* aActor, Namespace aNamespace) } already_AddRefed<Promise> Cache::Match(JSContext* aCx, - const RequestOrUSVString& aRequest, + const RequestOrUTF8String& aRequest, const CacheQueryOptions& aOptions, ErrorResult& aRv) { if (NS_WARN_IF(!mActor)) { @@ -259,7 +255,7 @@ already_AddRefed<Promise> Cache::Match(JSContext* aCx, } already_AddRefed<Promise> Cache::MatchAll( - JSContext* aCx, const Optional<RequestOrUSVString>& aRequest, + JSContext* aCx, const Optional<RequestOrUTF8String>& aRequest, const CacheQueryOptions& aOptions, ErrorResult& aRv) { if (NS_WARN_IF(!mActor)) { aRv.Throw(NS_ERROR_UNEXPECTED); @@ -291,7 +287,7 @@ already_AddRefed<Promise> Cache::MatchAll( } already_AddRefed<Promise> Cache::Add(JSContext* aContext, - const RequestOrUSVString& aRequest, + const RequestOrUTF8String& aRequest, CallerType aCallerType, ErrorResult& aRv) { if (NS_WARN_IF(!mActor)) { aRv.Throw(NS_ERROR_UNEXPECTED); @@ -315,7 +311,7 @@ already_AddRefed<Promise> Cache::Add(JSContext* aContext, return nullptr; } - nsAutoString url; + nsAutoCString url; request->GetUrl(url); if (NS_WARN_IF(!IsValidPutRequestURL(url, aRv))) { return nullptr; @@ -326,7 +322,8 @@ already_AddRefed<Promise> Cache::Add(JSContext* aContext, } already_AddRefed<Promise> Cache::AddAll( - JSContext* aContext, const Sequence<OwningRequestOrUSVString>& aRequestList, + JSContext* aContext, + const Sequence<OwningRequestOrUTF8String>& aRequestList, CallerType aCallerType, ErrorResult& aRv) { if (NS_WARN_IF(!mActor)) { aRv.Throw(NS_ERROR_UNEXPECTED); @@ -340,7 +337,7 @@ already_AddRefed<Promise> Cache::AddAll( nsTArray<SafeRefPtr<Request>> requestList(aRequestList.Length()); for (uint32_t i = 0; i < aRequestList.Length(); ++i) { - RequestOrUSVString requestOrString; + RequestOrUTF8String requestOrString; if (aRequestList[i].IsRequest()) { requestOrString.SetAsRequest() = aRequestList[i].GetAsRequest(); @@ -349,8 +346,8 @@ already_AddRefed<Promise> Cache::AddAll( return nullptr; } } else { - requestOrString.SetAsUSVString().ShareOrDependUpon( - aRequestList[i].GetAsUSVString()); + requestOrString.SetAsUTF8String().ShareOrDependUpon( + aRequestList[i].GetAsUTF8String()); } RootedDictionary<RequestInit> requestInit(aContext); @@ -360,7 +357,7 @@ already_AddRefed<Promise> Cache::AddAll( return nullptr; } - nsAutoString url; + nsAutoCString url; request->GetUrl(url); if (NS_WARN_IF(!IsValidPutRequestURL(url, aRv))) { return nullptr; @@ -373,7 +370,7 @@ already_AddRefed<Promise> Cache::AddAll( } already_AddRefed<Promise> Cache::Put(JSContext* aCx, - const RequestOrUSVString& aRequest, + const RequestOrUTF8String& aRequest, Response& aResponse, ErrorResult& aRv) { if (NS_WARN_IF(!mActor)) { aRv.Throw(NS_ERROR_UNEXPECTED); @@ -418,7 +415,7 @@ already_AddRefed<Promise> Cache::Put(JSContext* aCx, } already_AddRefed<Promise> Cache::Delete(JSContext* aCx, - const RequestOrUSVString& aRequest, + const RequestOrUTF8String& aRequest, const CacheQueryOptions& aOptions, ErrorResult& aRv) { if (NS_WARN_IF(!mActor)) { @@ -448,7 +445,7 @@ already_AddRefed<Promise> Cache::Delete(JSContext* aCx, } already_AddRefed<Promise> Cache::Keys( - JSContext* aCx, const Optional<RequestOrUSVString>& aRequest, + JSContext* aCx, const Optional<RequestOrUTF8String>& aRequest, const CacheQueryOptions& aOptions, ErrorResult& aRv) { if (NS_WARN_IF(!mActor)) { aRv.Throw(NS_ERROR_UNEXPECTED); @@ -552,7 +549,7 @@ already_AddRefed<Promise> Cache::AddAll( // future once fetch supports it. for (uint32_t i = 0; i < aRequestList.Length(); ++i) { - RequestOrUSVString requestOrString; + RequestOrUTF8String requestOrString; requestOrString.SetAsRequest() = aRequestList[i].unsafeGetRawPtr(); RootedDictionary<RequestInit> requestInit(aGlobal.Context()); RefPtr<Promise> fetch = diff --git a/dom/cache/Cache.h b/dom/cache/Cache.h index 02ccb557f7..35b133357b 100644 --- a/dom/cache/Cache.h +++ b/dom/cache/Cache.h @@ -22,10 +22,10 @@ class ErrorResult; namespace dom { -class OwningRequestOrUSVString; +class OwningRequestOrUTF8String; class Promise; struct CacheQueryOptions; -class RequestOrUSVString; +class RequestOrUTF8String; class Response; template <typename T> class Optional; @@ -46,27 +46,27 @@ class Cache final : public nsISupports, // webidl interface methods already_AddRefed<Promise> Match(JSContext* aCx, - const RequestOrUSVString& aRequest, + const RequestOrUTF8String& aRequest, const CacheQueryOptions& aOptions, ErrorResult& aRv); already_AddRefed<Promise> MatchAll( - JSContext* aCx, const Optional<RequestOrUSVString>& aRequest, + JSContext* aCx, const Optional<RequestOrUTF8String>& aRequest, const CacheQueryOptions& aOptions, ErrorResult& aRv); already_AddRefed<Promise> Add(JSContext* aContext, - const RequestOrUSVString& aRequest, + const RequestOrUTF8String& aRequest, CallerType aCallerType, ErrorResult& aRv); already_AddRefed<Promise> AddAll( - JSContext* aContext, const Sequence<OwningRequestOrUSVString>& aRequests, + JSContext* aContext, const Sequence<OwningRequestOrUTF8String>& aRequests, CallerType aCallerType, ErrorResult& aRv); already_AddRefed<Promise> Put(JSContext* aCx, - const RequestOrUSVString& aRequest, + const RequestOrUTF8String& aRequest, Response& aResponse, ErrorResult& aRv); already_AddRefed<Promise> Delete(JSContext* aCx, - const RequestOrUSVString& aRequest, + const RequestOrUTF8String& aRequest, const CacheQueryOptions& aOptions, ErrorResult& aRv); already_AddRefed<Promise> Keys(JSContext* aCx, - const Optional<RequestOrUSVString>& aRequest, + const Optional<RequestOrUTF8String>& aRequest, const CacheQueryOptions& aParams, ErrorResult& aRv); diff --git a/dom/cache/CacheStorage.cpp b/dom/cache/CacheStorage.cpp index b5647ba791..f41966a5e3 100644 --- a/dom/cache/CacheStorage.cpp +++ b/dom/cache/CacheStorage.cpp @@ -298,7 +298,7 @@ CacheStorage::CacheStorage(nsresult aFailureResult) } already_AddRefed<Promise> CacheStorage::Match( - JSContext* aCx, const RequestOrUSVString& aRequest, + JSContext* aCx, const RequestOrUTF8String& aRequest, const MultiCacheQueryOptions& aOptions, ErrorResult& aRv) { NS_ASSERT_OWNINGTHREAD(CacheStorage); diff --git a/dom/cache/CacheStorage.h b/dom/cache/CacheStorage.h index 306c7cccd4..3bfbece08b 100644 --- a/dom/cache/CacheStorage.h +++ b/dom/cache/CacheStorage.h @@ -56,7 +56,7 @@ class CacheStorage final : public nsISupports, // webidl interface methods already_AddRefed<Promise> Match(JSContext* aCx, - const RequestOrUSVString& aRequest, + const RequestOrUTF8String& aRequest, const MultiCacheQueryOptions& aOptions, ErrorResult& aRv); already_AddRefed<Promise> Has(const nsAString& aKey, ErrorResult& aRv); diff --git a/dom/cache/CacheTypes.ipdlh b/dom/cache/CacheTypes.ipdlh index 67131ed86a..5a0bafc737 100644 --- a/dom/cache/CacheTypes.ipdlh +++ b/dom/cache/CacheTypes.ipdlh @@ -59,7 +59,7 @@ struct CacheRequest nsCString urlFragment; HeadersEntry[] headers; HeadersGuardEnum headersGuard; - nsString referrer; + nsCString referrer; ReferrerPolicy referrerPolicy; RequestMode mode; RequestCredentials credentials; diff --git a/dom/cache/DBSchema.cpp b/dom/cache/DBSchema.cpp index 0c2c9cd4fb..390c1a9fd6 100644 --- a/dom/cache/DBSchema.cpp +++ b/dom/cache/DBSchema.cpp @@ -1646,7 +1646,7 @@ nsresult DeleteSecurityInfo(mozIStorageConnection& aConn, int32_t aId, QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetInt32, 0)); }())); - MOZ_DIAGNOSTIC_ASSERT(refcount >= aCount); + MOZ_ASSERT_DEBUG_OR_FUZZING(refcount >= aCount); // Next, calculate the new refcount int32_t newCount = refcount - aCount; @@ -1794,8 +1794,8 @@ nsresult InsertEntry(mozIStorageConnection& aConn, CacheId aCacheId, QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringByName("request_url_fragment"_ns, aRequest.urlFragment()))); - QM_TRY(MOZ_TO_RESULT( - state->BindStringByName("request_referrer"_ns, aRequest.referrer()))); + QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringByName("request_referrer"_ns, + aRequest.referrer()))); QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName( "request_referrer_policy"_ns, @@ -2208,7 +2208,8 @@ Result<SavedRequest, nsresult> ReadRequest(mozIStorageConnection& aConn, MOZ_TO_RESULT(state->GetUTF8String(2, savedRequest.mValue.urlQuery()))); QM_TRY(MOZ_TO_RESULT( state->GetUTF8String(3, savedRequest.mValue.urlFragment()))); - QM_TRY(MOZ_TO_RESULT(state->GetString(4, savedRequest.mValue.referrer()))); + QM_TRY( + MOZ_TO_RESULT(state->GetUTF8String(4, savedRequest.mValue.referrer()))); QM_TRY_INSPECT(const int32_t& referrerPolicy, MOZ_TO_RESULT_INVOKE_MEMBER(state, GetInt32, 5)); diff --git a/dom/cache/FileUtils.cpp b/dom/cache/FileUtils.cpp index cd99591c91..4e84455d73 100644 --- a/dom/cache/FileUtils.cpp +++ b/dom/cache/FileUtils.cpp @@ -34,7 +34,7 @@ namespace mozilla::dom::cache { -static_assert(SNAPPY_VERSION == 0x010109); +static_assert(SNAPPY_VERSION == 0x010200); using mozilla::dom::quota::Client; using mozilla::dom::quota::CloneFileAndAppend; diff --git a/dom/cache/StreamList.cpp b/dom/cache/StreamList.cpp index b72b7f95d5..cbce695b60 100644 --- a/dom/cache/StreamList.cpp +++ b/dom/cache/StreamList.cpp @@ -85,6 +85,12 @@ void StreamList::Add(const nsID& aId, nsCOMPtr<nsIInputStream>&& aStream) { // All streams should be added on IO thread before we set the stream // control on the owning IPC thread. MOZ_DIAGNOSTIC_ASSERT(!mStreamControl); + + // Removal of the stream will be triggered when the stream is closed, + // which happens only once, so let's ensure nobody adds us twice. + MOZ_ASSERT_DEBUG_OR_FUZZING( + std::find_if(mList.begin(), mList.end(), MatchById(aId)) == mList.end()); + mList.EmplaceBack(aId, std::move(aStream)); } @@ -93,6 +99,9 @@ already_AddRefed<nsIInputStream> StreamList::Extract(const nsID& aId) { const auto it = std::find_if(mList.begin(), mList.end(), MatchById(aId)); + // Note that if the stream has not been opened with OpenMode::Eager we will + // find it nullptr here and it will have to be opened by the consumer. + return it != mList.end() ? it->mStream.forget() : nullptr; } @@ -101,6 +110,7 @@ void StreamList::NoteClosed(const nsID& aId) { const auto it = std::find_if(mList.begin(), mList.end(), MatchById(aId)); if (it != mList.end()) { + MOZ_ASSERT(!it->mStream, "We expect to find mStream already extracted."); mList.RemoveElementAt(it); mManager->ReleaseBodyId(aId); } @@ -124,6 +134,7 @@ void StreamList::NoteClosedAll() { void StreamList::CloseAll() { NS_ASSERT_OWNINGTHREAD(StreamList); + if (mStreamControl && mStreamControl->CanSend()) { auto* streamControl = std::exchange(mStreamControl, nullptr); diff --git a/dom/cache/TypeUtils.cpp b/dom/cache/TypeUtils.cpp index 5e42f0a079..818b1cf93b 100644 --- a/dom/cache/TypeUtils.cpp +++ b/dom/cache/TypeUtils.cpp @@ -74,7 +74,7 @@ nsTArray<HeadersEntry> ToHeadersEntryList(InternalHeaders* aHeaders) { } // namespace SafeRefPtr<InternalRequest> TypeUtils::ToInternalRequest( - JSContext* aCx, const RequestOrUSVString& aIn, BodyAction aBodyAction, + JSContext* aCx, const RequestOrUTF8String& aIn, BodyAction aBodyAction, ErrorResult& aRv) { if (aIn.IsRequest()) { Request& request = aIn.GetAsRequest(); @@ -89,12 +89,12 @@ SafeRefPtr<InternalRequest> TypeUtils::ToInternalRequest( return request.GetInternalRequest(); } - return ToInternalRequest(aIn.GetAsUSVString(), aRv); + return ToInternalRequest(aIn.GetAsUTF8String(), aRv); } SafeRefPtr<InternalRequest> TypeUtils::ToInternalRequest( - JSContext* aCx, const OwningRequestOrUSVString& aIn, BodyAction aBodyAction, - ErrorResult& aRv) { + JSContext* aCx, const OwningRequestOrUTF8String& aIn, + BodyAction aBodyAction, ErrorResult& aRv) { if (aIn.IsRequest()) { Request& request = aIn.GetAsRequest(); @@ -108,7 +108,7 @@ SafeRefPtr<InternalRequest> TypeUtils::ToInternalRequest( return request.GetInternalRequest(); } - return ToInternalRequest(aIn.GetAsUSVString(), aRv); + return ToInternalRequest(aIn.GetAsUTF8String(), aRv); } void TypeUtils::ToCacheRequest(CacheRequest& aOut, const InternalRequest& aIn, @@ -447,10 +447,10 @@ void TypeUtils::CheckAndSetBodyUsed(JSContext* aCx, Request& aRequest, } } -SafeRefPtr<InternalRequest> TypeUtils::ToInternalRequest(const nsAString& aIn, +SafeRefPtr<InternalRequest> TypeUtils::ToInternalRequest(const nsACString& aIn, ErrorResult& aRv) { - RequestOrUSVString requestOrString; - requestOrString.SetAsUSVString().ShareOrDependUpon(aIn); + RequestOrUTF8String requestOrString; + requestOrString.SetAsUTF8String().ShareOrDependUpon(aIn); // Re-create a GlobalObject stack object so we can use webidl Constructors. AutoJSAPI jsapi; diff --git a/dom/cache/TypeUtils.h b/dom/cache/TypeUtils.h index 5342ea3d7e..051b12cbbf 100644 --- a/dom/cache/TypeUtils.h +++ b/dom/cache/TypeUtils.h @@ -30,9 +30,9 @@ struct MultiCacheQueryOptions; class InternalHeaders; class InternalRequest; class InternalResponse; -class OwningRequestOrUSVString; +class OwningRequestOrUTF8String; class Request; -class RequestOrUSVString; +class RequestOrUTF8String; class Response; namespace cache { @@ -64,12 +64,12 @@ class TypeUtils { virtual mozilla::ipc::PBackgroundChild* GetIPCManager() = 0; SafeRefPtr<InternalRequest> ToInternalRequest(JSContext* aCx, - const RequestOrUSVString& aIn, + const RequestOrUTF8String& aIn, BodyAction aBodyAction, ErrorResult& aRv); SafeRefPtr<InternalRequest> ToInternalRequest( - JSContext* aCx, const OwningRequestOrUSVString& aIn, + JSContext* aCx, const OwningRequestOrUTF8String& aIn, BodyAction aBodyAction, ErrorResult& aRv); void ToCacheRequest(CacheRequest& aOut, const InternalRequest& aIn, @@ -120,7 +120,7 @@ class TypeUtils { void CheckAndSetBodyUsed(JSContext* aCx, Request& aRequest, BodyAction aBodyAction, ErrorResult& aRv); - SafeRefPtr<InternalRequest> ToInternalRequest(const nsAString& aIn, + SafeRefPtr<InternalRequest> ToInternalRequest(const nsACString& aIn, ErrorResult& aRv); void SerializeCacheStream(nsIInputStream* aStream, diff --git a/dom/canvas/CanvasRenderingContext2D.cpp b/dom/canvas/CanvasRenderingContext2D.cpp index 62f6e6443d..1a79a9f734 100644 --- a/dom/canvas/CanvasRenderingContext2D.cpp +++ b/dom/canvas/CanvasRenderingContext2D.cpp @@ -682,6 +682,8 @@ class AdjustedTarget { CompositionOp UsedOperation() const { return mUsedOperation; } + bool UseOptimizeShadow() const { return mOptimizeShadow; } + ShadowOptions ShadowParams() const { const ContextState& state = mCtx->CurrentState(); return ShadowOptions(ToDeviceColor(state.shadowColor), state.shadowOffset, @@ -5359,6 +5361,40 @@ static Matrix ComputeRotationMatrix(gfxFloat aRotatedWidth, .PostTranslate(shiftLeftTopToOrigin); } +static Maybe<layers::SurfaceDescriptor> +MaybeGetSurfaceDescriptorForRemoteCanvas( + const SurfaceFromElementResult& aResult) { + if (!StaticPrefs::gfx_canvas_remote_use_draw_image_fast_path()) { + return Nothing(); + } + + if (!aResult.mLayersImage) { + return Nothing(); + } + + Maybe<layers::SurfaceDescriptor> sd; + sd = aResult.mLayersImage->GetDesc(); + if (sd.isNothing() || + sd.ref().type() != + layers::SurfaceDescriptor::TSurfaceDescriptorGPUVideo) { + return Nothing(); + } + + const auto& sdv = sd.ref().get_SurfaceDescriptorGPUVideo(); + const auto& sdvType = sdv.type(); + if (sdvType == + layers::SurfaceDescriptorGPUVideo::TSurfaceDescriptorRemoteDecoder) { + const auto& sdrd = sdv.get_SurfaceDescriptorRemoteDecoder(); + const auto& subdesc = sdrd.subdesc(); + const auto& subdescType = subdesc.type(); + if (subdescType == layers::RemoteDecoderVideoSubDescriptor::Tnull_t) { + return sd; + } + } + + return Nothing(); +} + // drawImage(in HTMLImageElement image, in float dx, in float dy); // -- render image from 0,0 at dx,dy top-left coords // drawImage(in HTMLImageElement image, in float dx, in float dy, in float dw, @@ -5470,6 +5506,8 @@ void CanvasRenderingContext2D::DrawImage(const CanvasImageSource& aImage, } DirectDrawInfo drawInfo; + Maybe<layers::SurfaceDescriptor> surfaceDescriptor; + SurfaceFromElementResult res; if (!srcSurf) { // The canvas spec says that drawImage should draw the first frame @@ -5478,7 +5516,6 @@ void CanvasRenderingContext2D::DrawImage(const CanvasImageSource& aImage, nsLayoutUtils::SFE_NO_RASTERIZING_VECTORS | nsLayoutUtils::SFE_ALLOW_UNCROPPED_UNSCALED; - SurfaceFromElementResult res; if (offscreenCanvas) { res = nsLayoutUtils::SurfaceFromOffscreenCanvas(offscreenCanvas, sfeFlags, mTarget); @@ -5487,12 +5524,34 @@ void CanvasRenderingContext2D::DrawImage(const CanvasImageSource& aImage, } else { res = CanvasRenderingContext2D::CachedSurfaceFromElement(element); if (!res.mSourceSurface) { - res = nsLayoutUtils::SurfaceFromElement(element, sfeFlags, mTarget); + HTMLVideoElement* video = HTMLVideoElement::FromNodeOrNull(element); + if (video && mBufferProvider->IsAccelerated() && + mTarget->IsRecording() && + !(!NeedToApplyFilter() && NeedToDrawShadow())) { + res = nsLayoutUtils::SurfaceFromElement( + video, sfeFlags, mTarget, /* aOptimizeSourceSurface */ false); + surfaceDescriptor = MaybeGetSurfaceDescriptorForRemoteCanvas(res); + if (surfaceDescriptor.isNothing() && res.mLayersImage) { + if ((res.mSourceSurface = res.mLayersImage->GetAsSourceSurface())) { + RefPtr<SourceSurface> opt = + mTarget->OptimizeSourceSurface(res.mSourceSurface); + if (opt) { + res.mSourceSurface = opt; + } + } + } + } else { + res = nsLayoutUtils::SurfaceFromElement(element, sfeFlags, mTarget); + } } } - srcSurf = res.GetSourceSurface(); - if (!srcSurf && !res.mDrawInfo.mImgContainer) { + if (surfaceDescriptor.isNothing()) { + srcSurf = res.GetSourceSurface(); + } + + if (!srcSurf && surfaceDescriptor.isNothing() && + !res.mDrawInfo.mImgContainer) { // https://html.spec.whatwg.org/#check-the-usability-of-the-image-argument: // // Only throw if the request is broken and the element is an @@ -5612,10 +5671,10 @@ void CanvasRenderingContext2D::DrawImage(const CanvasImageSource& aImage, return; } - if (srcSurf) { + if (srcSurf || surfaceDescriptor.isSome()) { gfx::Rect sourceRect(aSx, aSy, aSw, aSh); - if ((element && element == mCanvasElement) || - (offscreenCanvas && offscreenCanvas == mOffscreenCanvas)) { + if (srcSurf && ((element && element == mCanvasElement) || + (offscreenCanvas && offscreenCanvas == mOffscreenCanvas))) { // srcSurf is a snapshot of mTarget. If we draw to mTarget now, we'll // trigger a COW copy of the whole canvas into srcSurf. That's a huge // waste if sourceRect doesn't cover the whole canvas. @@ -5656,10 +5715,24 @@ void CanvasRenderingContext2D::DrawImage(const CanvasImageSource& aImage, destRect.y = 0; } - tempTarget.DrawSurface( - srcSurf, destRect, sourceRect, - DrawSurfaceOptions(samplingFilter, SamplingBounds::UNBOUNDED), - DrawOptions(CurrentState().globalAlpha, op, antialiasMode)); + if (srcSurf) { + MOZ_ASSERT(surfaceDescriptor.isNothing()); + + tempTarget.DrawSurface( + srcSurf, destRect, sourceRect, + DrawSurfaceOptions(samplingFilter, SamplingBounds::UNBOUNDED), + DrawOptions(CurrentState().globalAlpha, op, antialiasMode)); + } else if (surfaceDescriptor.isSome()) { + MOZ_ASSERT(!tempTarget.UseOptimizeShadow()); + MOZ_ASSERT(res.mLayersImage); + + mTarget->DrawSurfaceDescriptor( + surfaceDescriptor.ref(), res.mLayersImage, destRect, sourceRect, + DrawSurfaceOptions(samplingFilter, SamplingBounds::UNBOUNDED), + DrawOptions(CurrentState().globalAlpha, op, antialiasMode)); + } else { + MOZ_ASSERT_UNREACHABLE("unexpected to be called"); + } if (rotationDeg != VideoRotation::kDegree_0) { tempTarget->SetTransform(currentTransform); diff --git a/dom/canvas/ClientWebGLContext.cpp b/dom/canvas/ClientWebGLContext.cpp index 1614d2ead2..236ea916d0 100644 --- a/dom/canvas/ClientWebGLContext.cpp +++ b/dom/canvas/ClientWebGLContext.cpp @@ -548,36 +548,42 @@ Maybe<layers::SurfaceDescriptor> ClientWebGLContext::GetFrontBuffer( const auto& child = mNotLost->outOfProcess; child->FlushPendingCmds(); - Maybe<layers::SurfaceDescriptor> ret; + // Always synchronously get the front buffer if not using a remote texture. + bool needsSync = true; + Maybe<layers::SurfaceDescriptor> syncDesc; + Maybe<layers::SurfaceDescriptor> remoteDesc; auto& info = child->GetFlushedCmdInfo(); // If valid remote texture data was set for async present, then use it. if (!fb && !vr && mRemoteTextureOwnerId && mLastRemoteTextureId) { const auto tooManyFlushes = 10; // If there are many flushed cmds, force synchronous IPC to avoid too many - // pending ipc messages. - if (XRE_IsParentProcess() || - gfx::gfxVars::WebglOopAsyncPresentForceSync() || - info.flushesSinceLastCongestionCheck > tooManyFlushes) { - // Request the front buffer from IPDL to cause a sync, even though we - // will continue to use the remote texture descriptor after. - (void)child->SendGetFrontBuffer(fb ? fb->mId : 0, vr, &ret); - } - // Reset flushesSinceLastCongestionCheck - info.flushesSinceLastCongestionCheck = 0; - info.congestionCheckGeneration++; + // pending ipc messages. Otherwise don't sync for other cases to avoid any + // performance penalty. + needsSync = XRE_IsParentProcess() || + gfx::gfxVars::WebglOopAsyncPresentForceSync() || + info.flushesSinceLastCongestionCheck > tooManyFlushes; - return Some(layers::SurfaceDescriptorRemoteTexture(*mLastRemoteTextureId, - *mRemoteTextureOwnerId)); + // Only send over a remote texture descriptor if the WebGLChild actor is + // alive to ensure the remote texture id is valid. + if (child->CanSend()) { + remoteDesc = Some(layers::SurfaceDescriptorRemoteTexture( + *mLastRemoteTextureId, *mRemoteTextureOwnerId)); + } } - if (!child->SendGetFrontBuffer(fb ? fb->mId : 0, vr, &ret)) return {}; + if (needsSync && + !child->SendGetFrontBuffer(fb ? fb->mId : 0, vr, &syncDesc)) { + return {}; + } // Reset flushesSinceLastCongestionCheck info.flushesSinceLastCongestionCheck = 0; info.congestionCheckGeneration++; - return ret; + // If there is a remote texture descriptor, use that preferentially, as the + // sync front buffer descriptor was only created to force a sync first. + return remoteDesc ? remoteDesc : syncDesc; } Maybe<layers::SurfaceDescriptor> ClientWebGLContext::PresentFrontBuffer( @@ -957,9 +963,31 @@ bool ClientWebGLContext::CreateHostContext(const uvec2& requestedSize) { .mCurrentQueryByTarget[LOCAL_GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN]; } + state.mIsEnabledMap = Some(webgl::MakeIsEnabledMap(mIsWebGL2)); + return true; } +std::unordered_map<GLenum, bool> webgl::MakeIsEnabledMap(const bool webgl2) { + auto ret = std::unordered_map<GLenum, bool>{}; + + ret[LOCAL_GL_BLEND] = false; + ret[LOCAL_GL_CULL_FACE] = false; + ret[LOCAL_GL_DEPTH_TEST] = false; + ret[LOCAL_GL_DITHER] = true; + ret[LOCAL_GL_POLYGON_OFFSET_FILL] = false; + ret[LOCAL_GL_SAMPLE_ALPHA_TO_COVERAGE] = false; + ret[LOCAL_GL_SAMPLE_COVERAGE] = false; + ret[LOCAL_GL_SCISSOR_TEST] = false; + ret[LOCAL_GL_STENCIL_TEST] = false; + + if (webgl2) { + ret[LOCAL_GL_RASTERIZER_DISCARD] = false; + } + + return ret; +} + // ------- uvec2 ClientWebGLContext::DrawingBufferSize() { @@ -1461,6 +1489,8 @@ already_AddRefed<WebGLSyncJS> ClientWebGLContext::FenceSync( availRunnable.mSyncs.push_back(ret.get()); ret->mCanBeAvailable = false; + AutoEnqueueFlush(); + return ret.forget(); } @@ -1895,24 +1925,40 @@ bool ClientWebGLContext::IsVertexArray( // ------------------------- GL State ------------------------- -void ClientWebGLContext::SetEnabledI(GLenum cap, Maybe<GLuint> i, - bool val) const { +void ClientWebGLContext::SetEnabledI(const GLenum cap, const Maybe<GLuint> i, + const bool val) const { + const FuncScope funcScope(*this, "enable/disable"); + if (IsContextLost()) return; + + auto& map = *mNotLost->state.mIsEnabledMap; + auto slot = MaybeFind(map, cap); + if (i && cap != LOCAL_GL_BLEND) { + slot = nullptr; + } + if (!slot) { + EnqueueError_ArgEnum("cap", cap); + return; + } + Run<RPROC(SetEnabled)>(cap, i, val); + + if (!i || *i == 0) { + *slot = val; + } } -bool ClientWebGLContext::IsEnabled(GLenum cap) const { +bool ClientWebGLContext::IsEnabled(const GLenum cap) const { const FuncScope funcScope(*this, "isEnabled"); if (IsContextLost()) return false; - const auto& inProcess = mNotLost->inProcess; - if (inProcess) { - return inProcess->IsEnabled(cap); + const auto& map = *mNotLost->state.mIsEnabledMap; + const auto slot = MaybeFind(map, cap); + if (!slot) { + EnqueueError_ArgEnum("cap", cap); + return false; } - const auto& child = mNotLost->outOfProcess; - child->FlushPendingCmds(); - bool ret = {}; - if (!child->SendIsEnabled(cap, &ret)) return false; - return ret; + + return *slot; } template <typename T, typename S> @@ -2178,6 +2224,10 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname, (void)ToJSValueOrNull(cx, state.mBoundDrawFb, retval); return; + case LOCAL_GL_MAX_CLIENT_WAIT_TIMEOUT_WEBGL: + retval.set(JS::NumberValue(webgl::kMaxClientWaitSyncTimeoutNS)); + return; + case LOCAL_GL_PIXEL_PACK_BUFFER_BINDING: fnSetRetval_Buffer(LOCAL_GL_PIXEL_PACK_BUFFER); return; @@ -2987,7 +3037,7 @@ void ClientWebGLContext::DepthRange(GLclampf zNear, GLclampf zFar) { Run<RPROC(DepthRange)>(zNear, zFar); } -void ClientWebGLContext::Flush(const bool flushGl) { +void ClientWebGLContext::Flush(const bool flushGl) const { const FuncScope funcScope(*this, "flush"); if (IsContextLost()) return; @@ -5417,13 +5467,11 @@ void ClientWebGLContext::GetSyncParameter( case LOCAL_GL_SYNC_FLAGS: return JS::NumberValue(0); case LOCAL_GL_SYNC_STATUS: { - if (!sync.mSignaled) { - const auto res = ClientWaitSync(sync, 0, 0); - sync.mSignaled = (res == LOCAL_GL_ALREADY_SIGNALED || - res == LOCAL_GL_CONDITION_SATISFIED); - } - return JS::NumberValue(sync.mSignaled ? LOCAL_GL_SIGNALED - : LOCAL_GL_UNSIGNALED); + const auto res = ClientWaitSync(sync, 0, 0); + const auto signaled = (res == LOCAL_GL_ALREADY_SIGNALED || + res == LOCAL_GL_CONDITION_SATISFIED); + return JS::NumberValue(signaled ? LOCAL_GL_SIGNALED + : LOCAL_GL_UNSIGNALED); } default: EnqueueError_ArgEnum("pname", pname); @@ -5432,6 +5480,8 @@ void ClientWebGLContext::GetSyncParameter( }()); } +// - + GLenum ClientWebGLContext::ClientWaitSync(WebGLSyncJS& sync, const GLbitfield flags, const GLuint64 timeout) const { @@ -5439,12 +5489,63 @@ GLenum ClientWebGLContext::ClientWaitSync(WebGLSyncJS& sync, if (IsContextLost()) return LOCAL_GL_WAIT_FAILED; if (!sync.ValidateUsable(*this, "sync")) return LOCAL_GL_WAIT_FAILED; - if (flags != 0 && flags != LOCAL_GL_SYNC_FLUSH_COMMANDS_BIT) { + static constexpr auto VALID_BITS = LOCAL_GL_SYNC_FLUSH_COMMANDS_BIT; + if ((flags | VALID_BITS) != VALID_BITS) { EnqueueError(LOCAL_GL_INVALID_VALUE, "`flags` must be SYNC_FLUSH_COMMANDS_BIT or 0."); return LOCAL_GL_WAIT_FAILED; } + if (timeout > webgl::kMaxClientWaitSyncTimeoutNS) { + EnqueueError( + LOCAL_GL_INVALID_OPERATION, + "`timeout` (%sns) must be less than MAX_CLIENT_WAIT_TIMEOUT_WEBGL " + "(%sns).", + ToStringWithCommas(timeout).c_str(), + ToStringWithCommas(webgl::kMaxClientWaitSyncTimeoutNS).c_str()); + return LOCAL_GL_WAIT_FAILED; + } + + const bool canBeAvailable = + (sync.mCanBeAvailable || StaticPrefs::webgl_allow_immediate_queries()); + if (!canBeAvailable) { + constexpr uint8_t WARN_AT = 100; + if (sync.mNumQueriesBeforeFirstFrameBoundary <= WARN_AT) { + sync.mNumQueriesBeforeFirstFrameBoundary += 1; + if (sync.mNumQueriesBeforeFirstFrameBoundary == WARN_AT) { + EnqueueWarning( + "ClientWaitSync must return TIMEOUT_EXPIRED until control has" + " returned to the user agent's main loop, but was polled %hhu " + "times. Are you spin-locking? (only warns once)", + sync.mNumQueriesBeforeFirstFrameBoundary); + } + } + return LOCAL_GL_TIMEOUT_EXPIRED; + } + + if (mCompletedSyncId >= sync.mId) { + return LOCAL_GL_ALREADY_SIGNALED; + } + if (flags & LOCAL_GL_SYNC_FLUSH_COMMANDS_BIT) { + Flush(); + } else { + constexpr uint8_t WARN_AT = 100; + if (sync.mNumQueriesWithoutFlushCommandsBit <= WARN_AT) { + sync.mNumQueriesWithoutFlushCommandsBit += 1; + if (sync.mNumQueriesWithoutFlushCommandsBit == WARN_AT) { + EnqueueWarning( + "ClientWaitSync with timeout=0 (or GetSyncParameter(SYNC_STATUS)) " + "called %hhu times without SYNC_FLUSH_COMMANDS_BIT. If you do not " + "flush, this sync object is not guaranteed to ever complete.", + sync.mNumQueriesWithoutFlushCommandsBit); + } + } + } + if (!timeout) return LOCAL_GL_TIMEOUT_EXPIRED; + + // - + // Fine, time to block: + const auto ret = [&]() { const auto& inProcess = mNotLost->inProcess; if (inProcess) { @@ -5462,29 +5563,10 @@ GLenum ClientWebGLContext::ClientWaitSync(WebGLSyncJS& sync, switch (ret) { case LOCAL_GL_CONDITION_SATISFIED: case LOCAL_GL_ALREADY_SIGNALED: - sync.mSignaled = true; + OnSyncComplete(sync.mId); break; } - // - - - const bool canBeAvailable = - (sync.mCanBeAvailable || StaticPrefs::webgl_allow_immediate_queries()); - if (!canBeAvailable) { - constexpr uint8_t WARN_AT = 100; - if (sync.mNumQueriesBeforeFirstFrameBoundary <= WARN_AT) { - sync.mNumQueriesBeforeFirstFrameBoundary += 1; - if (sync.mNumQueriesBeforeFirstFrameBoundary == WARN_AT) { - EnqueueWarning( - "ClientWaitSync must return TIMEOUT_EXPIRED until control has" - " returned to the user agent's main loop, but was polled %hhu " - "times. Are you spin-locking? (only warns once)", - sync.mNumQueriesBeforeFirstFrameBoundary); - } - } - return LOCAL_GL_TIMEOUT_EXPIRED; - } - return ret; } @@ -5688,6 +5770,7 @@ void ClientWebGLContext::DrawBuffers(const dom::Sequence<GLenum>& buffers) { void ClientWebGLContext::EnqueueErrorImpl(const GLenum error, const nsACString& text) const { if (!mNotLost) return; // Ignored if context is lost. + AutoEnqueueFlush(); Run<RPROC(GenerateError)>(error, ToString(text)); } @@ -6570,7 +6653,7 @@ Maybe<Span<uint8_t>> ClientWebGLContext::ValidateArrayBufferView( // --------------------------- webgl::ObjectJS::ObjectJS(const ClientWebGLContext& webgl) - : mGeneration(webgl.mNotLost), mId(webgl.mNotLost->state.NextId()) {} + : mGeneration(webgl.mNotLost), mId(webgl.NextId()) {} // - diff --git a/dom/canvas/ClientWebGLContext.h b/dom/canvas/ClientWebGLContext.h index 3c011a3027..e736235361 100644 --- a/dom/canvas/ClientWebGLContext.h +++ b/dom/canvas/ClientWebGLContext.h @@ -134,9 +134,6 @@ class ShaderKeepAlive final { }; class ContextGenerationInfo final { - private: - ObjectId mLastId = 0; - public: webgl::ExtensionBits mEnabledExtensions; RefPtr<WebGLProgramJS> mCurrentProgram; @@ -180,7 +177,7 @@ class ContextGenerationInfo final { webgl::ProvokingVertex mProvokingVertex = webgl::ProvokingVertex::LastVertex; - ObjectId NextId() { return mLastId += 1; } + mutable Maybe<std::unordered_map<GLenum, bool>> mIsEnabledMap; }; // - @@ -493,7 +490,7 @@ class WebGLSyncJS final : public nsWrapperCache, bool mCanBeAvailable = false; uint8_t mNumQueriesBeforeFirstFrameBoundary = 0; - bool mSignaled = false; + uint8_t mNumQueriesWithoutFlushCommandsBit = 0; public: NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(WebGLSyncJS) @@ -781,8 +778,11 @@ class ClientWebGLContext final : public nsICanvasRenderingContextInternal, mutable GLenum mNextError = 0; mutable webgl::LossStatus mLossStatus = webgl::LossStatus::Ready; mutable bool mAwaitingRestore = false; + mutable webgl::ObjectId mLastId = 0; public: + webgl::ObjectId NextId() const { return mLastId += 1; } + // Holds Some Id if async present is used mutable Maybe<layers::RemoteTextureId> mLastRemoteTextureId; mutable Maybe<layers::RemoteTextureOwnerId> mRemoteTextureOwnerId; @@ -1092,15 +1092,15 @@ class ClientWebGLContext final : public nsICanvasRenderingContextInternal, // - - bool mAutoFlushPending = false; + mutable bool mAutoFlushPending = false; - void AutoEnqueueFlush() { + void AutoEnqueueFlush() const { if (MOZ_LIKELY(mAutoFlushPending)) return; mAutoFlushPending = true; - const auto weak = WeakPtr<ClientWebGLContext>(this); - const auto DeferredFlush = [weak]() { - const auto strong = RefPtr<ClientWebGLContext>(weak); + const auto DeferredFlush = [weak = + WeakPtr<const ClientWebGLContext>(this)]() { + const auto strong = RefPtr<const ClientWebGLContext>(weak); if (!strong) return; if (!strong->mAutoFlushPending) return; strong->mAutoFlushPending = false; @@ -1111,12 +1111,12 @@ class ClientWebGLContext final : public nsICanvasRenderingContextInternal, }; already_AddRefed<mozilla::CancelableRunnable> runnable = - NS_NewCancelableRunnableFunction("enqueue Event_webglcontextrestored", + NS_NewCancelableRunnableFunction("ClientWebGLContext::DeferredFlush", DeferredFlush); NS_DispatchToCurrentThread(std::move(runnable)); } - void CancelAutoFlush() { mAutoFlushPending = false; } + void CancelAutoFlush() const { mAutoFlushPending = false; } // - @@ -1141,6 +1141,7 @@ class ClientWebGLContext final : public nsICanvasRenderingContextInternal, void Disable(GLenum cap) const { SetEnabledI(cap, {}, false); } void Enable(GLenum cap) const { SetEnabledI(cap, {}, true); } void SetEnabledI(GLenum cap, Maybe<GLuint> i, bool val) const; + bool IsEnabled(GLenum cap) const; private: @@ -1397,7 +1398,7 @@ class ClientWebGLContext final : public nsICanvasRenderingContextInternal, void DepthRange(GLclampf zNear, GLclampf zFar); - void Flush(bool flushGl = true); + void Flush(bool flushGl = true) const; void Finish(); @@ -2245,6 +2246,13 @@ class ClientWebGLContext final : public nsICanvasRenderingContextInternal, JS::MutableHandle<JS::Value> retval) const; void WaitSync(const WebGLSyncJS&, GLbitfield flags, GLint64 timeout) const; + mutable webgl::ObjectId mCompletedSyncId = 0; + void OnSyncComplete(webgl::ObjectId id) const { + if (mCompletedSyncId < id) { + mCompletedSyncId = id; + } + } + // -------------------------- Transform Feedback --------------------------- void BindTransformFeedback(GLenum target, WebGLTransformFeedbackJS*); diff --git a/dom/canvas/DrawTargetWebgl.cpp b/dom/canvas/DrawTargetWebgl.cpp index eae6dd78f3..fdc45cc842 100644 --- a/dom/canvas/DrawTargetWebgl.cpp +++ b/dom/canvas/DrawTargetWebgl.cpp @@ -21,6 +21,7 @@ #include "mozilla/gfx/Swizzle.h" #include "mozilla/layers/ImageDataSerializer.h" #include "mozilla/layers/RemoteTextureMap.h" +#include "mozilla/widget/ScreenManager.h" #include "skia/include/core/SkPixmap.h" #include "nsContentUtils.h" @@ -325,6 +326,11 @@ void SharedContextWebgl::UnlinkGlyphCaches() { void SharedContextWebgl::OnMemoryPressure() { mShouldClearCaches = true; } +void SharedContextWebgl::ClearCaches() { + OnMemoryPressure(); + ClearCachesIfNecessary(); +} + // Clear out the entire list of texture handles from any source. void SharedContextWebgl::ClearAllTextures() { while (!mTextureHandles.isEmpty()) { @@ -852,10 +858,14 @@ bool DrawTargetWebgl::CanCreate(const IntSize& aSize, SurfaceFormat aFormat) { // In addition, allow acceleration up to this size even if the screen is // smaller. A lot content expects this size to work well. See Bug 999841 static const int32_t kScreenPixels = 980 * 480; - IntSize screenSize = gfxPlatform::GetPlatform()->GetScreenSize(); - if (aSize.width * aSize.height > - std::max(screenSize.width * screenSize.height, kScreenPixels)) { - return false; + + if (RefPtr<widget::Screen> screen = + widget::ScreenManager::GetSingleton().GetPrimaryScreen()) { + LayoutDeviceIntSize screenSize = screen->GetRect().Size(); + if (aSize.width * aSize.height > + std::max(screenSize.width * screenSize.height, kScreenPixels)) { + return false; + } } } diff --git a/dom/canvas/DrawTargetWebgl.h b/dom/canvas/DrawTargetWebgl.h index 955ccaabf0..8dc9fff048 100644 --- a/dom/canvas/DrawTargetWebgl.h +++ b/dom/canvas/DrawTargetWebgl.h @@ -92,6 +92,8 @@ class SharedContextWebgl : public mozilla::RefCounted<SharedContextWebgl>, void OnMemoryPressure(); + void ClearCaches(); + private: SharedContextWebgl(); diff --git a/dom/canvas/HostWebGLContext.cpp b/dom/canvas/HostWebGLContext.cpp index 11051c756f..4eea15773a 100644 --- a/dom/canvas/HostWebGLContext.cpp +++ b/dom/canvas/HostWebGLContext.cpp @@ -180,6 +180,17 @@ void HostWebGLContext::CreateSync(const ObjectId id) { return; } slot = GetWebGL2Context()->FenceSync(LOCAL_GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + + if (!slot) return; + + slot->OnCompleteTaskAdd([host = WeakPtr{this}, id]() { + if (!host) return; + if (host->mOwnerData.inProcess) { + host->mOwnerData.inProcess->OnSyncComplete(id); + } else if (host->mOwnerData.outOfProcess) { + (void)host->mOwnerData.outOfProcess->SendOnSyncComplete(id); + } + }); } void HostWebGLContext::CreateTexture(const ObjectId id) { diff --git a/dom/canvas/HostWebGLContext.h b/dom/canvas/HostWebGLContext.h index cc385bc26a..23b836f9db 100644 --- a/dom/canvas/HostWebGLContext.h +++ b/dom/canvas/HostWebGLContext.h @@ -260,8 +260,6 @@ class HostWebGLContext final : public SupportsWeakPtr { mContext->SetEnabled(cap, i, val); } - bool IsEnabled(GLenum cap) const { return mContext->IsEnabled(cap); } - Maybe<double> GetNumber(GLenum pname) const { return mContext->GetParameter(pname); } diff --git a/dom/canvas/PWebGL.ipdl b/dom/canvas/PWebGL.ipdl index a49d41e582..82c813bfd5 100644 --- a/dom/canvas/PWebGL.ipdl +++ b/dom/canvas/PWebGL.ipdl @@ -101,7 +101,6 @@ parent: sync GetTexParameter(uint64_t id, uint32_t pname) returns (double? ret); sync GetUniform(uint64_t id, uint32_t loc) returns (GetUniformData ret); sync GetVertexAttrib(uint32_t index, uint32_t pname) returns (double? ret); - sync IsEnabled(uint32_t cap) returns (bool ret); sync OnMemoryPressure(); sync ValidateProgram(uint64_t id) returns (bool ret); @@ -110,6 +109,9 @@ child: // Tell client that this queue needs to be shut down async OnContextLoss(ContextLossReason aReason); + + // Triggered when the id from FenceSync completes. + async OnSyncComplete(uint64_t id); }; } // dom diff --git a/dom/canvas/QueueParamTraits.h b/dom/canvas/QueueParamTraits.h index a67f5abf51..87fe910675 100644 --- a/dom/canvas/QueueParamTraits.h +++ b/dom/canvas/QueueParamTraits.h @@ -262,15 +262,6 @@ struct QueueParamTraits<bool> { // --------------------------------------------------------------- template <class T> -Maybe<T> AsValidEnum(const std::underlying_type_t<T> raw_val) { - const auto raw_enum = T{raw_val}; // This is the risk we prevent! - if (!IsEnumCase(raw_enum)) return {}; - return Some(raw_enum); -} - -// - - -template <class T> struct QueueParamTraits_IsEnumCase { template <typename ProducerView> static bool Write(ProducerView& aProducerView, const T& aArg) { diff --git a/dom/canvas/TexUnpackBlob.cpp b/dom/canvas/TexUnpackBlob.cpp index e13dc1c064..c59253cfd8 100644 --- a/dom/canvas/TexUnpackBlob.cpp +++ b/dom/canvas/TexUnpackBlob.cpp @@ -25,6 +25,8 @@ namespace mozilla { bool webgl::PixelPackingState::AssertCurrentUnpack(gl::GLContext& gl, const bool isWebgl2) const { + if (!kIsDebug) return true; + auto actual = PixelPackingState{}; gl.GetInt(LOCAL_GL_UNPACK_ALIGNMENT, &actual.alignmentInTypeElems); if (isWebgl2) { diff --git a/dom/canvas/TiedFields.h b/dom/canvas/TiedFields.h index 2df225aeee..a071da9dab 100644 --- a/dom/canvas/TiedFields.h +++ b/dom/canvas/TiedFields.h @@ -131,42 +131,34 @@ constexpr bool AssertTiedFieldsAreExhaustive() { // - /** - * Padding<T> can be used to pad out a struct so that it's not implicitly - * padded by struct rules. - * You can also just add your padding to TiedFields, but by explicitly typing - * padding like this, serialization can make a choice whether to copy Padding, - * or instead to omit the copy. - * - * Omitting the copy isn't always faster. - * struct Entry { - * uint16_t key; - * Padding<uint16_t> padding; - * uint32_t val; - * auto MutTiedFields() { return std::tie(key, padding, val); } - * }; - * If you serialize Padding, the serialized size is 8, and the compiler will - * optimize serialization to a single 8-byte memcpy. - * If your serialization omits Padding, the serialized size of Entry shrinks - * by 25%. If you have a big list of Entrys, maybe this is a big savings! - * However, by optimizing for size here you sacrifice speed, because this splits - * the single memcpy into two: a 2-byte memcpy and a 4-byte memcpy. - * - * Explicitly marking padding gives callers the option of choosing. + * PaddingField<T,N=1> can be used to pad out a struct so that it's not + * implicitly padded by struct rules, but also can't be accidentally initialized + * via Aggregate Initialization. (TiedFields serialization checks rely on object + * fields leaving no implicit padding bytes, but explicit padding fields are + * fine) While you can use e.g. `uint8_t _padding[3];`, consider instead + * `PaddingField<uint8_t,3> _padding;` for clarity and to move the `3` nearer + * to the `uint8_t`. */ -template <class T> -struct Padding { - T ignored; +template <class T, size_t N = 1> +struct PaddingField { + static_assert(!std::is_array_v<T>, "Use PaddingField<T,N> not <T[N]>."); - friend constexpr bool operator==(const Padding&, const Padding&) { + std::array<T, N> ignored = {}; + + PaddingField() {} + + friend constexpr bool operator==(const PaddingField&, const PaddingField&) { return true; } - friend constexpr bool operator<(const Padding&, const Padding&) { + friend constexpr bool operator<(const PaddingField&, const PaddingField&) { return false; } + + auto MutTiedFields() { return std::tie(ignored); } }; -static_assert(sizeof(Padding<bool>) == 1); -static_assert(sizeof(Padding<bool[2]>) == 2); -static_assert(sizeof(Padding<int>) == 4); +static_assert(sizeof(PaddingField<bool>) == 1); +static_assert(sizeof(PaddingField<bool, 2>) == 2); +static_assert(sizeof(PaddingField<int>) == 4); // - @@ -202,7 +194,7 @@ static_assert(AreAllBytesTiedFields<Fish>()); struct Eel { // Like a Fish, but you can skip serializing the padding. bool b; - Padding<bool> padding[3]; + PaddingField<bool, 3> padding; int i; constexpr auto MutTiedFields() { return std::tie(i, b, padding); } diff --git a/dom/canvas/WebGL2Context.h b/dom/canvas/WebGL2Context.h index 018c81f298..c8fbdef5ee 100644 --- a/dom/canvas/WebGL2Context.h +++ b/dom/canvas/WebGL2Context.h @@ -104,12 +104,8 @@ class WebGL2Context final : public WebGLContext { // ------------------------------------------------------------------------- // Sync objects - WebGL2ContextSync.cpp - const GLuint64 kMaxClientWaitSyncTimeoutNS = - 1000 * 1000 * 1000; // 1000ms in ns. - RefPtr<WebGLSync> FenceSync(GLenum condition, GLbitfield flags); - GLenum ClientWaitSync(const WebGLSync& sync, GLbitfield flags, - GLuint64 timeout); + GLenum ClientWaitSync(WebGLSync& sync, GLbitfield flags, GLuint64 timeout); // ------------------------------------------------------------------------- // Transform Feedback - WebGL2ContextTransformFeedback.cpp diff --git a/dom/canvas/WebGL2ContextState.cpp b/dom/canvas/WebGL2ContextState.cpp index b97592b570..b7d2f44000 100644 --- a/dom/canvas/WebGL2ContextState.cpp +++ b/dom/canvas/WebGL2ContextState.cpp @@ -81,10 +81,6 @@ Maybe<double> WebGL2Context::GetParameter(GLenum pname) { return Some(4 * val); } - /* GLint64 */ - case LOCAL_GL_MAX_CLIENT_WAIT_TIMEOUT_WEBGL: - return Some(kMaxClientWaitSyncTimeoutNS); - case LOCAL_GL_MAX_ELEMENT_INDEX: // GL_MAX_ELEMENT_INDEX becomes available in GL 4.3 or via ES3 // compatibility diff --git a/dom/canvas/WebGL2ContextSync.cpp b/dom/canvas/WebGL2ContextSync.cpp index b0092ca682..e5a1b1bc7b 100644 --- a/dom/canvas/WebGL2ContextSync.cpp +++ b/dom/canvas/WebGL2ContextSync.cpp @@ -29,10 +29,12 @@ RefPtr<WebGLSync> WebGL2Context::FenceSync(GLenum condition, GLbitfield flags) { } RefPtr<WebGLSync> globj = new WebGLSync(this, condition, flags); + mPendingSyncs.emplace_back(globj); + EnsurePollPendingSyncs_Pending(); return globj; } -GLenum WebGL2Context::ClientWaitSync(const WebGLSync& sync, GLbitfield flags, +GLenum WebGL2Context::ClientWaitSync(WebGLSync& sync, GLbitfield flags, GLuint64 timeout) { const FuncScope funcScope(*this, "clientWaitSync"); if (IsContextLost()) return LOCAL_GL_WAIT_FAILED; @@ -44,19 +46,52 @@ GLenum WebGL2Context::ClientWaitSync(const WebGLSync& sync, GLbitfield flags, return LOCAL_GL_WAIT_FAILED; } - if (timeout > kMaxClientWaitSyncTimeoutNS) { - ErrorInvalidOperation("`timeout` must not exceed %s nanoseconds.", - "MAX_CLIENT_WAIT_TIMEOUT_WEBGL"); - return LOCAL_GL_WAIT_FAILED; - } - - const auto ret = gl->fClientWaitSync(sync.mGLName, flags, timeout); + const auto ret = sync.ClientWaitSync(flags, timeout); + return UnderlyingValue(ret); +} - if (ret == LOCAL_GL_CONDITION_SATISFIED || ret == LOCAL_GL_ALREADY_SIGNALED) { - sync.MarkSignaled(); +void WebGLContext::EnsurePollPendingSyncs_Pending() const { + if (mPollPendingSyncs_Pending) return; + mPollPendingSyncs_Pending = NS_NewRunnableFunction( + "WebGLContext::PollPendingSyncs", [weak = WeakPtr{this}]() { + if (const auto strong = RefPtr{weak.get()}) { + strong->mPollPendingSyncs_Pending = nullptr; + strong->PollPendingSyncs(); + if (strong->mPendingSyncs.size()) { + // Not done yet... + strong->EnsurePollPendingSyncs_Pending(); + } + } + }); + if (const auto eventTarget = GetCurrentSerialEventTarget()) { + eventTarget->DelayedDispatch(do_AddRef(mPollPendingSyncs_Pending), + kPollPendingSyncs_DelayMs); + } else { + NS_WARNING( + "[EnsurePollPendingSyncs_Pending] GetCurrentSerialEventTarget() -> " + "nullptr"); } +} - return ret; +void WebGLContext::PollPendingSyncs() const { + const FuncScope funcScope(*this, "<pollPendingSyncs>"); + if (IsContextLost()) return; + + while (mPendingSyncs.size()) { + if (const auto sync = RefPtr{mPendingSyncs.front().get()}) { + const auto res = sync->ClientWaitSync(0, 0); + switch (res) { + case ClientWaitSyncResult::WAIT_FAILED: + case ClientWaitSyncResult::TIMEOUT_EXPIRED: + return; + case ClientWaitSyncResult::CONDITION_SATISFIED: + case ClientWaitSyncResult::ALREADY_SIGNALED: + // Communication back to child happens in sync->lientWaitSync. + break; + } + } + mPendingSyncs.pop_front(); + } } } // namespace mozilla diff --git a/dom/canvas/WebGLChild.cpp b/dom/canvas/WebGLChild.cpp index ebfe35db88..81ea1dcdd6 100644 --- a/dom/canvas/WebGLChild.cpp +++ b/dom/canvas/WebGLChild.cpp @@ -162,4 +162,11 @@ mozilla::ipc::IPCResult WebGLChild::RecvOnContextLoss( return IPC_OK(); } +mozilla::ipc::IPCResult WebGLChild::RecvOnSyncComplete( + const webgl::ObjectId id) const { + if (!mContext) return IPC_OK(); + mContext->OnSyncComplete(id); + return IPC_OK(); +} + } // namespace mozilla::dom diff --git a/dom/canvas/WebGLChild.h b/dom/canvas/WebGLChild.h index 6848237d8f..f4229e1b9b 100644 --- a/dom/canvas/WebGLChild.h +++ b/dom/canvas/WebGLChild.h @@ -58,6 +58,7 @@ class WebGLChild final : public PWebGLChild, public SupportsWeakPtr { public: mozilla::ipc::IPCResult RecvJsWarning(const std::string&) const; mozilla::ipc::IPCResult RecvOnContextLoss(webgl::ContextLossReason) const; + mozilla::ipc::IPCResult RecvOnSyncComplete(webgl::ObjectId) const; }; } // namespace dom diff --git a/dom/canvas/WebGLContext.cpp b/dom/canvas/WebGLContext.cpp index ec19bfef3d..4bd189c46c 100644 --- a/dom/canvas/WebGLContext.cpp +++ b/dom/canvas/WebGLContext.cpp @@ -881,6 +881,8 @@ void WebGLContext::OnEndOfFrame() { mDrawCallsSinceLastFlush = 0; + PollPendingSyncs(); + BumpLru(); } diff --git a/dom/canvas/WebGLContext.h b/dom/canvas/WebGLContext.h index 5ab584174c..d144584f4f 100644 --- a/dom/canvas/WebGLContext.h +++ b/dom/canvas/WebGLContext.h @@ -303,6 +303,15 @@ class WebGLContext : public VRefCounted, public SupportsWeakPtr { uint64_t mNextFenceId = 1; uint64_t mCompletedFenceId = 0; + mutable std::list<WeakPtr<WebGLSync>> mPendingSyncs; + mutable RefPtr<nsIRunnable> mPollPendingSyncs_Pending; + static constexpr uint32_t kPollPendingSyncs_DelayMs = + 4; // Four times a frame. + public: + void EnsurePollPendingSyncs_Pending() const; + void PollPendingSyncs() const; + + protected: std::unique_ptr<gl::Texture> mIncompleteTexOverride; public: @@ -756,8 +765,6 @@ class WebGLContext : public VRefCounted, public SupportsWeakPtr { virtual Maybe<double> GetParameter(GLenum pname); Maybe<std::string> GetString(GLenum pname) const; - bool IsEnabled(GLenum cap); - private: static StaticMutex sLruMutex; static std::list<WebGLContext*> sLru MOZ_GUARDED_BY(sLruMutex); @@ -780,8 +787,7 @@ class WebGLContext : public VRefCounted, public SupportsWeakPtr { }; ScissorRect mScissorRect = {}; - bool ValidateCapabilityEnum(GLenum cap); - bool* GetStateTrackingSlot(GLenum cap, GLuint i); + bool* GetStateTrackingSlot(GLenum cap); // Allocation debugging variables mutable uint64_t mDataAllocGLCallCount = 0; diff --git a/dom/canvas/WebGLContextState.cpp b/dom/canvas/WebGLContextState.cpp index f81e590436..e057a0195b 100644 --- a/dom/canvas/WebGLContextState.cpp +++ b/dom/canvas/WebGLContextState.cpp @@ -27,27 +27,25 @@ void WebGLContext::SetEnabled(const GLenum cap, const Maybe<GLuint> i, const FuncScope funcScope(*this, "enable(i)/disable(i)"); if (IsContextLost()) return; - if (!ValidateCapabilityEnum(cap)) return; - - if (i) { - if (cap != LOCAL_GL_BLEND) { - ErrorInvalidEnumArg("cap", cap); - return; - } - - const auto limit = MaxValidDrawBuffers(); - if (*i >= limit) { - ErrorInvalidValue("`index` (%u) must be < %s (%u)", *i, - "MAX_DRAW_BUFFERS", limit); - return; - } + static const auto webgl1Map = webgl::MakeIsEnabledMap(false); + static const auto webgl2Map = webgl::MakeIsEnabledMap(true); + const auto* map = &webgl2Map; + if (!IsWebGL2()) { + map = &webgl1Map; + } + if (!MaybeFind(*map, cap)) { + MOZ_ASSERT(false, "Bad cap."); + return; } - const auto slot = GetStateTrackingSlot(cap, i ? *i : 0); - if (slot) { - *slot = enabled; - } else if (cap == LOCAL_GL_BLEND) { + if (cap == LOCAL_GL_BLEND) { if (i) { + const auto limit = MaxValidDrawBuffers(); + if (*i >= limit) { + ErrorInvalidValue("`index` (%u) must be < %s (%u)", *i, + "MAX_DRAW_BUFFERS", limit); + return; + } mBlendEnabled[*i] = enabled; } else { if (enabled) { @@ -56,6 +54,15 @@ void WebGLContext::SetEnabled(const GLenum cap, const Maybe<GLuint> i, mBlendEnabled.reset(); } } + } else { + if (i) { + MOZ_ASSERT(false, "i"); + return; + } + const auto slot = GetStateTrackingSlot(cap); + if (slot) { + *slot = enabled; + } } switch (cap) { @@ -431,39 +438,7 @@ Maybe<double> WebGLContext::GetParameter(const GLenum pname) { return Nothing(); } -bool WebGLContext::IsEnabled(GLenum cap) { - const FuncScope funcScope(*this, "isEnabled"); - if (IsContextLost()) return false; - - if (!ValidateCapabilityEnum(cap)) return false; - - const auto& slot = GetStateTrackingSlot(cap, 0); - if (slot) return *slot; - - return gl->fIsEnabled(cap); -} - -bool WebGLContext::ValidateCapabilityEnum(GLenum cap) { - switch (cap) { - case LOCAL_GL_BLEND: - case LOCAL_GL_CULL_FACE: - case LOCAL_GL_DEPTH_TEST: - case LOCAL_GL_DITHER: - case LOCAL_GL_POLYGON_OFFSET_FILL: - case LOCAL_GL_SAMPLE_ALPHA_TO_COVERAGE: - case LOCAL_GL_SAMPLE_COVERAGE: - case LOCAL_GL_SCISSOR_TEST: - case LOCAL_GL_STENCIL_TEST: - return true; - case LOCAL_GL_RASTERIZER_DISCARD: - return IsWebGL2(); - default: - ErrorInvalidEnumInfo("cap", cap); - return false; - } -} - -bool* WebGLContext::GetStateTrackingSlot(GLenum cap, GLuint i) { +bool* WebGLContext::GetStateTrackingSlot(GLenum cap) { switch (cap) { case LOCAL_GL_DEPTH_TEST: return &mDepthTestEnabled; diff --git a/dom/canvas/WebGLFramebuffer.cpp b/dom/canvas/WebGLFramebuffer.cpp index 108d2178cc..949177a217 100644 --- a/dom/canvas/WebGLFramebuffer.cpp +++ b/dom/canvas/WebGLFramebuffer.cpp @@ -352,10 +352,17 @@ Maybe<double> WebGLFBAttachPoint::GetParameter(WebGLContext* webgl, case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE: case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE: case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE: - case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE: isPNameValid = webgl->IsWebGL2(); break; + case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE: + isPNameValid = (webgl->IsWebGL2() || + webgl->IsExtensionEnabled( + WebGLExtensionID::WEBGL_color_buffer_float) || + webgl->IsExtensionEnabled( + WebGLExtensionID::EXT_color_buffer_half_float)); + break; + case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING: isPNameValid = (webgl->IsWebGL2() || webgl->IsExtensionEnabled(WebGLExtensionID::EXT_sRGB)); @@ -404,7 +411,14 @@ Maybe<double> WebGLFBAttachPoint::GetParameter(WebGLContext* webgl, break; case LOCAL_GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE: - MOZ_ASSERT(attachment != LOCAL_GL_DEPTH_STENCIL_ATTACHMENT); + if (attachment == LOCAL_GL_DEPTH_STENCIL_ATTACHMENT) { + webgl->ErrorInvalidOperation( + "Querying" + " FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE" + " against DEPTH_STENCIL_ATTACHMENT is an" + " error."); + return Nothing(); + } if (format->unsizedFormat == webgl::UnsizedFormat::DEPTH_STENCIL) { MOZ_ASSERT(attachment == LOCAL_GL_DEPTH_ATTACHMENT || @@ -1222,17 +1236,6 @@ Maybe<double> WebGLFramebuffer::GetAttachmentParameter(GLenum attachEnum, auto attach = maybeAttach.value(); if (mContext->IsWebGL2() && attachEnum == LOCAL_GL_DEPTH_STENCIL_ATTACHMENT) { - // There are a couple special rules for this one. - - if (pname == LOCAL_GL_FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE) { - mContext->ErrorInvalidOperation( - "Querying" - " FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE" - " against DEPTH_STENCIL_ATTACHMENT is an" - " error."); - return Nothing(); - } - if (mDepthAttachment.Renderbuffer() != mStencilAttachment.Renderbuffer() || mDepthAttachment.Texture() != mStencilAttachment.Texture()) { mContext->ErrorInvalidOperation( diff --git a/dom/canvas/WebGLIpdl.h b/dom/canvas/WebGLIpdl.h index 5a35c05909..45a5c5ad64 100644 --- a/dom/canvas/WebGLIpdl.h +++ b/dom/canvas/WebGLIpdl.h @@ -245,6 +245,45 @@ struct ParamTraits<mozilla::dom::PredefinedColorSpace> final mozilla::dom::PredefinedColorSpace> {}; // - +// ParamTraits_IsEnumCase + +/* +`IsEnumCase(T) -> bool` guarantees that we never have false negatives or false +positives due to adding or removing enum cases to enums, and forgetting to +update their serializations. Also, it allows enums to be non-continguous, unlike +ContiguousEnumSerializer. +*/ + +template <class T> +struct ParamTraits_IsEnumCase { + static bool Write(MessageWriter* const writer, const T& in) { + MOZ_ASSERT(IsEnumCase(in)); + const auto shadow = static_cast<std::underlying_type_t<T>>(in); + WriteParam(writer, shadow); + return true; + } + + static bool Read(MessageReader* const reader, T* const out) { + auto shadow = std::underlying_type_t<T>{}; + if (!ReadParam(reader, &shadow)) return false; + const auto e = mozilla::AsValidEnum<T>(shadow); + if (!e) return false; + *out = *e; + return true; + } +}; + +// - + +#define USE_IS_ENUM_CASE(T) \ + template <> \ + struct ParamTraits<T> : public ParamTraits_IsEnumCase<T> {}; + +USE_IS_ENUM_CASE(mozilla::webgl::OptionalRenderableFormatBits) + +#undef USE_IS_ENUM_CASE + +// - // ParamTraits_TiedFields template <class T> @@ -272,6 +311,10 @@ struct ParamTraits_TiedFields { } }; +template <class U, size_t N> +struct ParamTraits<mozilla::PaddingField<U, N>> final + : public ParamTraits_TiedFields<mozilla::PaddingField<U, N>> {}; + // - template <> @@ -609,35 +652,6 @@ struct ParamTraits<mozilla::avec3<U>> final { } }; -// - - -template <class TT> -struct ParamTraits_IsEnumCase { - using T = TT; - - static void Write(IPC::MessageWriter* const writer, const T& in) { - MOZ_RELEASE_ASSERT(IsEnumCase(in)); - WriteParam(writer, mozilla::UnderlyingValue(in)); - } - - static bool Read(IPC::MessageReader* const reader, T* const out) { - std::underlying_type_t<T> rawVal; - if (!ReadParam(reader, &rawVal)) return false; - *out = static_cast<T>(rawVal); - return IsEnumCase(*out); - } -}; - -// - - -#define USE_IS_ENUM_CASE(T) \ - template <> \ - struct ParamTraits<T> : public ParamTraits_IsEnumCase<T> {}; - -USE_IS_ENUM_CASE(mozilla::webgl::OptionalRenderableFormatBits) - -#undef USE_IS_ENUM_CASE - } // namespace IPC #endif diff --git a/dom/canvas/WebGLParent.cpp b/dom/canvas/WebGLParent.cpp index 44a0692b3f..e107d6cb50 100644 --- a/dom/canvas/WebGLParent.cpp +++ b/dom/canvas/WebGLParent.cpp @@ -462,15 +462,6 @@ IPCResult WebGLParent::RecvGetVertexAttrib(GLuint index, GLenum pname, return IPC_OK(); } -IPCResult WebGLParent::RecvIsEnabled(GLenum cap, bool* const ret) { - if (!mHost) { - return IPC_FAIL(this, "HostWebGLContext is not initialized."); - } - - *ret = mHost->IsEnabled(cap); - return IPC_OK(); -} - IPCResult WebGLParent::RecvOnMemoryPressure() { if (!mHost) { return IPC_FAIL(this, "HostWebGLContext is not initialized."); diff --git a/dom/canvas/WebGLParent.h b/dom/canvas/WebGLParent.h index 4cdc03e0ec..b3b4511e76 100644 --- a/dom/canvas/WebGLParent.h +++ b/dom/canvas/WebGLParent.h @@ -103,7 +103,6 @@ class WebGLParent : public PWebGLParent, public SupportsWeakPtr { IPCResult RecvGetUniform(ObjectId id, uint32_t loc, webgl::GetUniformData* ret); IPCResult RecvGetVertexAttrib(GLuint index, GLenum pname, Maybe<double>* ret); - IPCResult RecvIsEnabled(GLenum cap, bool* ret); IPCResult RecvOnMemoryPressure(); IPCResult RecvValidateProgram(ObjectId id, bool* ret); diff --git a/dom/canvas/WebGLSync.cpp b/dom/canvas/WebGLSync.cpp index e4b196ab04..6a8e9403f7 100644 --- a/dom/canvas/WebGLSync.cpp +++ b/dom/canvas/WebGLSync.cpp @@ -23,10 +23,39 @@ WebGLSync::~WebGLSync() { mContext->gl->fDeleteSync(mGLName); } -void WebGLSync::MarkSignaled() const { - if (mContext->mCompletedFenceId < mFenceId) { - mContext->mCompletedFenceId = mFenceId; +ClientWaitSyncResult WebGLSync::ClientWaitSync(const GLbitfield flags, + const GLuint64 timeout) { + if (!mContext) return ClientWaitSyncResult::WAIT_FAILED; + if (IsKnownComplete()) return ClientWaitSyncResult::ALREADY_SIGNALED; + + auto ret = ClientWaitSyncResult::WAIT_FAILED; + bool newlyComplete = false; + const auto status = static_cast<ClientWaitSyncResult>( + mContext->gl->fClientWaitSync(mGLName, 0, 0)); + switch (status) { + case ClientWaitSyncResult::TIMEOUT_EXPIRED: // Poll() -> false + case ClientWaitSyncResult::WAIT_FAILED: // Error + ret = status; + break; + case ClientWaitSyncResult::CONDITION_SATISFIED: // Should never happen, but + // tolerate it. + case ClientWaitSyncResult::ALREADY_SIGNALED: // Poll() -> true + newlyComplete = true; + ret = status; + break; + } + + if (newlyComplete) { + if (mContext->mCompletedFenceId < mFenceId) { + mContext->mCompletedFenceId = mFenceId; + } + MOZ_RELEASE_ASSERT(mOnCompleteTasks); + for (const auto& task : *mOnCompleteTasks) { + (*task)(); + } + mOnCompleteTasks = {}; } + return ret; } } // namespace mozilla diff --git a/dom/canvas/WebGLSync.h b/dom/canvas/WebGLSync.h index 22c77941e2..48ed2ee58f 100644 --- a/dom/canvas/WebGLSync.h +++ b/dom/canvas/WebGLSync.h @@ -11,9 +11,30 @@ namespace mozilla { namespace webgl { class AvailabilityRunnable; + +struct Task { + virtual ~Task() = default; + virtual void operator()() const = 0; +}; + +template <class F> +struct FnTask : public Task { + const F fn; + + explicit FnTask(F&& fn) : fn(std::move(fn)) {} + + virtual void operator()() const override { fn(); } +}; } // namespace webgl -class WebGLSync final : public WebGLContextBoundObject { +enum class ClientWaitSyncResult : GLenum { + WAIT_FAILED = LOCAL_GL_WAIT_FAILED, + TIMEOUT_EXPIRED = LOCAL_GL_TIMEOUT_EXPIRED, + CONDITION_SATISFIED = LOCAL_GL_CONDITION_SATISFIED, + ALREADY_SIGNALED = LOCAL_GL_ALREADY_SIGNALED, +}; + +class WebGLSync final : public WebGLContextBoundObject, public SupportsWeakPtr { friend class WebGL2Context; friend class webgl::AvailabilityRunnable; @@ -23,10 +44,22 @@ class WebGLSync final : public WebGLContextBoundObject { const uint64_t mFenceId; bool mCanBeAvailable = false; + std::optional<std::vector<std::unique_ptr<webgl::Task>>> mOnCompleteTasks = + std::vector<std::unique_ptr<webgl::Task>>{}; + public: WebGLSync(WebGLContext* webgl, GLenum condition, GLbitfield flags); - void MarkSignaled() const; + ClientWaitSyncResult ClientWaitSync(GLbitfield flags, GLuint64 timeout); + + template <class F> + void OnCompleteTaskAdd(F&& fn) { + MOZ_RELEASE_ASSERT(mOnCompleteTasks); + auto task = std::make_unique<webgl::FnTask<F>>(std::move(fn)); + mOnCompleteTasks->push_back(std::move(task)); + } + + bool IsKnownComplete() const { return !mOnCompleteTasks; } private: ~WebGLSync() override; diff --git a/dom/canvas/WebGLTypes.h b/dom/canvas/WebGLTypes.h index c268047930..f5f78e98cb 100644 --- a/dom/canvas/WebGLTypes.h +++ b/dom/canvas/WebGLTypes.h @@ -1224,6 +1224,15 @@ inline bool StartsWith(const std::string_view str, // - +template <class T> +Maybe<T> AsValidEnum(const std::underlying_type_t<T> raw_val) { + const auto raw_enum = T{raw_val}; // This is the risk we prevent! + if (!IsEnumCase(raw_enum)) return {}; + return Some(raw_enum); +} + +// - + namespace webgl { // In theory, this number can be unbounded based on the driver. However, no @@ -1302,6 +1311,50 @@ struct ReinterpretToSpan { } }; +// - + +inline std::string Join(Span<const std::string> ss, + const std::string_view& delim) { + if (!ss.size()) return ""; + auto ret = std::string(); + { + auto chars = delim.size() * (ss.size() - 1); + for (const auto& s : ss) { + chars += s.size(); + } + ret.reserve(chars); + } + + ret = ss[0]; + ss = ss.subspan(1); + for (const auto& s : ss) { + ret += delim; + ret += s; + } + return ret; +} + +inline std::string ToStringWithCommas(uint64_t v) { + if (!v) return "0"; + std::vector<std::string> chunks; + while (v) { + const auto chunk = v % 1000; + v /= 1000; + chunks.insert(chunks.begin(), std::to_string(chunk)); + } + return Join(chunks, ","); +} + +// - + +namespace webgl { + +std::unordered_map<GLenum, bool> MakeIsEnabledMap(bool webgl2); + +static constexpr uint32_t kMaxClientWaitSyncTimeoutNS = + 1000 * 1000 * 1000; // 1000ms in ns. + +} // namespace webgl } // namespace mozilla #endif diff --git a/dom/canvas/test/crossorigin/mochitest.toml b/dom/canvas/test/crossorigin/mochitest.toml index e45b2624cd..0b62b02eab 100644 --- a/dom/canvas/test/crossorigin/mochitest.toml +++ b/dom/canvas/test/crossorigin/mochitest.toml @@ -7,6 +7,10 @@ support-files = [ "image.png", "video.sjs", ] +# The video used in those tests crash the Android emulator's VP9 decoder. +prefs = [ + "media.android-media-codec.enabled=false", +] ["test_canvas2d_crossorigin.html"] skip-if = [ diff --git a/dom/canvas/test/mochitest.toml b/dom/canvas/test/mochitest.toml index 3e96fff26b..6ab48c09c6 100644 --- a/dom/canvas/test/mochitest.toml +++ b/dom/canvas/test/mochitest.toml @@ -36,7 +36,11 @@ support-files = [ "offscreencanvas_serviceworker_inner.html", "crossorigin/image.png", "crossorigin/video.sjs", - "../../media/test/320x240.ogv", + "../../media/test/320x240.webm", +] +# The video used in those tests crash the Android emulator's VP9 decoder. +prefs = [ + "media.android-media-codec.enabled=false", ] ["test_2d.clearRect.image.offscreen.html"] diff --git a/dom/canvas/test/test_imagebitmap.html b/dom/canvas/test/test_imagebitmap.html index 7a68db08df..3c7958242b 100644 --- a/dom/canvas/test/test_imagebitmap.html +++ b/dom/canvas/test/test_imagebitmap.html @@ -6,7 +6,7 @@ <body> <img src="image_anim-gr.gif" id="image" class="resource"> -<video width="320" height="240" src="http://example.com/tests/dom/canvas/test/crossorigin/video.sjs?name=tests/dom/canvas/test/320x240.ogv&type=video/ogg&cors=anonymous" id="video" crossOrigin="anonymous" autoplay></video> +<video width="320" height="240" src="http://example.com/tests/dom/canvas/test/crossorigin/video.sjs?name=tests/dom/canvas/test/320x240.webm&type=video/webm&cors=anonymous" id="video" crossOrigin="anonymous" autoplay></video> <canvas id="c1" class="output" width="128" height="128"></canvas> <canvas id="c2" width="128" height="128"></canvas> @@ -226,7 +226,7 @@ function testSecurityErrors() { reject(); } - uncleanVideo.src = "http://example.com/tests/dom/canvas/test/crossorigin/video.sjs?name=tests/dom/canvas/test/320x240.ogv&type=video/ogg"; + uncleanVideo.src = "http://example.com/tests/dom/canvas/test/crossorigin/video.sjs?name=tests/dom/canvas/test/320x240.webm&type=video/webm"; uncleanVideo.play(); }); } diff --git a/dom/canvas/test/test_imagebitmap_cropping.html b/dom/canvas/test/test_imagebitmap_cropping.html index 56ccbf62e2..bb0f06f423 100644 --- a/dom/canvas/test/test_imagebitmap_cropping.html +++ b/dom/canvas/test/test_imagebitmap_cropping.html @@ -33,7 +33,7 @@ function isPixel(ctx, x, y, c, d) { } // -// The pattern of the 320x240.ogv video. +// The pattern of the 320x240.webm video. // .------------------------------------------------. // | 255 | 255 | 0 | 0 | 255 | 255 | 0 | // | 255 | 255 | 255 | 255 | 0 | 0 | 0 | @@ -135,7 +135,7 @@ var gJPEGBlob; function prepareSources() { gVideo = document.createElement("video"); - gVideo.src = "http://example.com/tests/dom/canvas/test/crossorigin/video.sjs?name=tests/dom/canvas/test/320x240.ogv&type=video/ogg&cors=anonymous"; + gVideo.src = "http://example.com/tests/dom/canvas/test/crossorigin/video.sjs?name=tests/dom/canvas/test/320x240.webm&type=video/webm&cors=anonymous"; gVideo.crossOrigin = "anonymous"; gVideo.autoplay = "true" diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/canvas/00_test_list.txt b/dom/canvas/test/webgl-conf/checkout/conformance/canvas/00_test_list.txt index 558163de17..c0cdadcd03 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/canvas/00_test_list.txt +++ b/dom/canvas/test/webgl-conf/checkout/conformance/canvas/00_test_list.txt @@ -4,6 +4,8 @@ canvas-test.html canvas-zero-size.html drawingbuffer-static-canvas-test.html --min-version 1.0.2 drawingbuffer-hd-dpi-test.html +# Uncomment once fully passing on WebGL 1 +# --min-version 1.0.4 --max-version 1.9.9 drawingbuffer-storage-test.html drawingbuffer-test.html --min-version 1.0.3 draw-webgl-to-canvas-test.html --min-version 1.0.3 draw-static-webgl-to-multiple-canvas-test.html diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/canvas/drawingbuffer-storage-test.html b/dom/canvas/test/webgl-conf/checkout/conformance/canvas/drawingbuffer-storage-test.html new file mode 100644 index 0000000000..285cd047d1 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance/canvas/drawingbuffer-storage-test.html @@ -0,0 +1,27 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL drawingBufferStorage Conformance Tests</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +<script src="../../js/tests/drawingbuffer-storage-test.js"></script> +</head> +<body> +<div id="description"></div> +<div id="console"></div> +<script> +runTest(1); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/00_test_list.txt b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/00_test_list.txt index 9a72b67ef0..e121e90676 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/00_test_list.txt +++ b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/00_test_list.txt @@ -1,15 +1,19 @@ --min-version 1.0.3 --max-version 1.9.9 angle-instanced-arrays.html --min-version 1.0.3 --max-version 1.9.9 angle-instanced-arrays-out-of-bounds.html --min-version 1.0.3 --max-version 1.9.9 ext-blend-minmax.html +--min-version 1.0.4 ext-clip-control.html --min-version 1.0.4 ext-color-buffer-half-float.html +--min-version 1.0.4 ext-depth-clamp.html --min-version 1.0.4 ext-float-blend.html --min-version 1.0.4 ext-texture-compression-bptc.html --min-version 1.0.4 ext-texture-compression-rgtc.html --min-version 1.0.4 ext-disjoint-timer-query.html --min-version 1.0.3 --max-version 1.9.9 ext-frag-depth.html +--min-version 1.0.4 ext-polygon-offset-clamp.html --min-version 1.0.3 --max-version 1.9.9 ext-shader-texture-lod.html --min-version 1.0.3 --max-version 1.9.9 ext-sRGB.html --min-version 1.0.2 ext-texture-filter-anisotropic.html +--min-version 1.0.4 ext-texture-mirror-clamp-to-edge.html --min-version 1.0.2 get-extension.html --min-version 1.0.4 khr-parallel-shader-compile.html --max-version 1.9.9 oes-standard-derivatives.html @@ -29,6 +33,7 @@ --min-version 1.0.3 --max-version 1.9.9 oes-texture-half-float-with-video.html --min-version 1.0.2 --max-version 1.9.9 oes-element-index-uint.html --min-version 1.0.4 --max-version 1.9.9 oes-fbo-render-mipmap.html +--min-version 1.0.4 --max-version 1.9.9 webgl-blend-func-extended.html webgl-debug-renderer-info.html webgl-debug-shaders.html --min-version 1.0.4 webgl-compressed-texture-astc.html @@ -44,3 +49,4 @@ webgl-debug-shaders.html --min-version 1.0.4 --max-version 1.9.9 webgl-draw-buffers-framebuffer-unsupported.html --min-version 1.0.4 --max-version 1.9.9 webgl-draw-buffers-max-draw-buffers.html --min-version 1.0.4 webgl-multi-draw.html +--min-version 1.0.4 webgl-polygon-mode.html diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-clip-control.html b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-clip-control.html new file mode 100644 index 0000000000..f5c557c839 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-clip-control.html @@ -0,0 +1,185 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL EXT_clip_control Conformance Tests</title> +<LINK rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<canvas width="32" height="32" id="c"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the EXT_clip_control extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext("c"); +var ext; +const w = gl.drawingBufferWidth; +const h = gl.drawingBufferHeight; + +function runTestNoExtension() { + debug(""); + debug("Check the parameters without the extension"); + shouldBeNull("gl.getParameter(0x935C /* CLIP_ORIGIN_EXT */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + shouldBeNull("gl.getParameter(0x935D /* CLIP_DEPTH_MODE_EXT */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); +} + +function checkEnums() { + debug(""); + debug("Check enums"); + shouldBe("ext.LOWER_LEFT_EXT", "0x8CA1"); + shouldBe("ext.UPPER_LEFT_EXT", "0x8CA2"); + + shouldBe("ext.NEGATIVE_ONE_TO_ONE_EXT", "0x935E"); + shouldBe("ext.ZERO_TO_ONE_EXT", "0x935F"); + + shouldBe("ext.CLIP_ORIGIN_EXT", "0x935C"); + shouldBe("ext.CLIP_DEPTH_MODE_EXT", "0x935D"); +} + +function checkQueries() { + debug(""); + debug("Check default state"); + shouldBe('gl.getParameter(ext.CLIP_ORIGIN_EXT)', 'ext.LOWER_LEFT_EXT'); + shouldBe('gl.getParameter(ext.CLIP_DEPTH_MODE_EXT)', 'ext.NEGATIVE_ONE_TO_ONE_EXT'); + debug(""); + debug("Check state updates using the new function"); + ext.clipControlEXT(ext.UPPER_LEFT_EXT, ext.ZERO_TO_ONE_EXT); + shouldBe('gl.getParameter(ext.CLIP_ORIGIN_EXT)', 'ext.UPPER_LEFT_EXT'); + shouldBe('gl.getParameter(ext.CLIP_DEPTH_MODE_EXT)', 'ext.ZERO_TO_ONE_EXT'); + ext.clipControlEXT(ext.LOWER_LEFT_EXT, ext.ZERO_TO_ONE_EXT); + shouldBe('gl.getParameter(ext.CLIP_ORIGIN_EXT)', 'ext.LOWER_LEFT_EXT'); + shouldBe('gl.getParameter(ext.CLIP_DEPTH_MODE_EXT)', 'ext.ZERO_TO_ONE_EXT'); + ext.clipControlEXT(ext.UPPER_LEFT_EXT, ext.NEGATIVE_ONE_TO_ONE_EXT); + shouldBe('gl.getParameter(ext.CLIP_ORIGIN_EXT)', 'ext.UPPER_LEFT_EXT'); + shouldBe('gl.getParameter(ext.CLIP_DEPTH_MODE_EXT)', 'ext.NEGATIVE_ONE_TO_ONE_EXT'); + ext.clipControlEXT(ext.LOWER_LEFT_EXT, ext.NEGATIVE_ONE_TO_ONE_EXT); + shouldBe('gl.getParameter(ext.CLIP_ORIGIN_EXT)', 'ext.LOWER_LEFT_EXT'); + shouldBe('gl.getParameter(ext.CLIP_DEPTH_MODE_EXT)', 'ext.NEGATIVE_ONE_TO_ONE_EXT'); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); +} + +function checkDepthMode() { + debug(""); + debug("Check depth mode toggling"); + + gl.enable(gl.DEPTH_TEST); + gl.clear(gl.DEPTH_BUFFER_BIT); + + const program = wtu.setupProgram(gl, [wtu.simpleVertexShader, + wtu.simpleColorFragmentShader]); + gl.useProgram(program); + const colorLoc = gl.getUniformLocation(program, "u_color"); + wtu.setupUnitQuad(gl); + + // Draw red at 0 with the default depth mode + gl.uniform4f(colorLoc, 1, 0, 0, 1); + ext.clipControlEXT(ext.LOWER_LEFT_EXT, ext.NEGATIVE_ONE_TO_ONE_EXT); + wtu.drawUnitQuad(gl); + wtu.checkCanvasRect(gl, 0, 0, w, h, [255, 0, 0, 255]); + + // Draw green at 0, depth test must fail + gl.uniform4f(colorLoc, 0, 1, 0, 1); + wtu.drawUnitQuad(gl); + wtu.checkCanvasRect(gl, 0, 0, w, h, [255, 0, 0, 255]); + + // Draw green at 0 after switching the depth mode + ext.clipControlEXT(ext.LOWER_LEFT_EXT, ext.ZERO_TO_ONE_EXT); + wtu.drawUnitQuad(gl); + wtu.checkCanvasRect(gl, 0, 0, w, h, [0, 255, 0, 255]); + + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); +} + +function checkClipOrigin() { + debug(""); + debug("Check clip origin toggling"); + + gl.disable(gl.DEPTH_TEST); + + const vertexShader = ` + attribute vec4 vPosition; + varying float y; + void main() { + gl_Position = vPosition; + y = vPosition.y; + }`; + const fragmentShader = ` + precision mediump float; + varying float y; + void main() { + if (y > 0.0) { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + } else { + gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); + } + }`; + + const program = wtu.setupProgram(gl, [vertexShader, fragmentShader]); + gl.useProgram(program); + + wtu.setupUnitQuad(gl); + + ext.clipControlEXT(ext.LOWER_LEFT_EXT, ext.NEGATIVE_ONE_TO_ONE_EXT); + wtu.drawUnitQuad(gl); + wtu.checkCanvasRect(gl, 0, 0, w, h / 2 - 2, [0, 255, 0, 255]); + wtu.checkCanvasRect(gl, 0, h / 2 + 2, w, h / 2 - 2, [255, 0, 0, 255]); + + ext.clipControlEXT(ext.UPPER_LEFT_EXT, ext.NEGATIVE_ONE_TO_ONE_EXT); + wtu.drawUnitQuad(gl); + wtu.checkCanvasRect(gl, 0, 0, w, h / 2 - 2, [255, 0, 0, 255]); + wtu.checkCanvasRect(gl, 0, h / 2 + 2, w, h / 2 - 2, [0, 255, 0, 255]); + + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); +} + +function runTestExtension() { + checkEnums(); + checkQueries(); + checkDepthMode(); + checkClipOrigin(); +} + +function runTest() { + if (!gl) { + testFailed("WebGL context does not exist"); + return; + } + testPassed("WebGL context exists"); + + runTestNoExtension(); + + ext = gl.getExtension("EXT_clip_control"); + + wtu.runExtensionSupportedTest(gl, "EXT_clip_control", ext !== null); + + if (ext !== null) { + runTestExtension(); + } else { + testPassed("No EXT_clip_control support -- this is legal"); + } +} + +runTest(); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-depth-clamp.html b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-depth-clamp.html new file mode 100644 index 0000000000..6082c9880c --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-depth-clamp.html @@ -0,0 +1,168 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL EXT_depth_clamp Conformance Tests</title> +<LINK rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<canvas width="32" height="32" id="c"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the EXT_depth_clamp extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext("c"); +var ext; +const w = gl.drawingBufferWidth; +const h = gl.drawingBufferHeight; + +function runTestNoExtension() { + debug(""); + debug("Check the parameter without the extension"); + shouldBeNull("gl.getParameter(0x864F /* DEPTH_CLAMP_EXT */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + debug("Check the cap without the extension"); + gl.disable(0x864F /* DEPTH_CLAMP_EXT */); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "cap unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + gl.enable(0x864F /* DEPTH_CLAMP_EXT */); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "cap unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + shouldBeFalse("gl.isEnabled(0x864F /* DEPTH_CLAMP_EXT */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "cap unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); +} + +function checkEnums() { + debug(""); + debug("Check enums"); + shouldBe("ext.DEPTH_CLAMP_EXT", "0x864F"); +} + +function checkQueries() { + debug(""); + debug("Check default state"); + shouldBeFalse('gl.isEnabled(ext.DEPTH_CLAMP_EXT)'); + shouldBeFalse('gl.getParameter(ext.DEPTH_CLAMP_EXT)'); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + debug(""); + debug("Check state update using the new capability"); + gl.enable(ext.DEPTH_CLAMP_EXT); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + shouldBeTrue('gl.isEnabled(ext.DEPTH_CLAMP_EXT)'); + shouldBeTrue('gl.getParameter(ext.DEPTH_CLAMP_EXT)'); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + gl.disable(ext.DEPTH_CLAMP_EXT); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + shouldBeFalse('gl.isEnabled(ext.DEPTH_CLAMP_EXT)'); + shouldBeFalse('gl.getParameter(ext.DEPTH_CLAMP_EXT)'); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); +} + +function checkClamping() { + debug(""); + debug("Check depth clamp operation"); + + // Draw a red quad located beyond the clipping volume. + // When depth clamping is enabled, it must be drawn. + const vertexShader = ` + attribute vec2 vPosition; + uniform float u_depth; + void main() { + gl_Position = vec4(vPosition, u_depth, 1.0); + }`; + const program = wtu.setupProgram(gl, [vertexShader, + wtu.simpleColorFragmentShader]); + gl.useProgram(program); + const colorLoc = gl.getUniformLocation(program, "u_color"); + const depthLoc = gl.getUniformLocation(program, "u_depth"); + + wtu.setupUnitQuad(gl); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + // Red beyond the far plane + gl.uniform4f(colorLoc, 1, 0, 0, 1); + gl.uniform1f(depthLoc, +2); + + // Clipped + wtu.clearAndDrawUnitQuad(gl); + wtu.checkCanvasRect(gl, 0, 0, w, h, [255, 255, 255, 255]); + + // Enable depth clamping, disable depth clipping + gl.enable(ext.DEPTH_CLAMP_EXT); + + // Not clipped + wtu.clearAndDrawUnitQuad(gl); + wtu.checkCanvasRect(gl, 0, 0, w, h, [255, 0, 0, 255]); + + // Green beyond the near plane + gl.uniform4f(colorLoc, 0, 1, 0, 1); + gl.uniform1f(depthLoc, -2); + + // Not clipped + wtu.clearAndDrawUnitQuad(gl); + wtu.checkCanvasRect(gl, 0, 0, w, h, [0, 255, 0, 255]); + + // Blue beyond the near plane with clamping disabled + gl.disable(ext.DEPTH_CLAMP_EXT); + gl.uniform4f(colorLoc, 0, 0, 1, 1); + + // Clipped + wtu.clearAndDrawUnitQuad(gl); + wtu.checkCanvasRect(gl, 0, 0, w, h, [255, 255, 255, 255]); + + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); +} + +function runTestExtension() { + checkEnums(); + checkQueries(); + checkClamping(); +} + +function runTest() { + if (!gl) { + testFailed("WebGL context does not exist"); + return; + } + testPassed("WebGL context exists"); + + runTestNoExtension(); + + ext = gl.getExtension("EXT_depth_clamp"); + + wtu.runExtensionSupportedTest(gl, "EXT_depth_clamp", ext !== null); + + if (ext !== null) { + runTestExtension(); + } else { + testPassed("No EXT_depth_clamp support -- this is legal"); + } +} + +runTest(); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-disjoint-timer-query.html b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-disjoint-timer-query.html index 1fc29cc445..7965987c64 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-disjoint-timer-query.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-disjoint-timer-query.html @@ -25,7 +25,9 @@ description("This test verifies the functionality of the EXT_disjoint_timer_quer var wtu = WebGLTestUtils; var canvas = document.getElementById("canvas"); var gl = wtu.create3DContext(canvas); +var gl2 = null; var ext = null; +var ext2 = null; var query = null; var query2 = null; var elapsed_query = null; @@ -61,6 +63,8 @@ if (!gl) { runTimeStampTest(); } verifyQueryResultsNotAvailable(); + verifyDeleteQueryBehavior(); + verifyDeleteQueryErrorBehavior(); window.requestAnimationFrame(checkQueryResults); } @@ -245,6 +249,61 @@ function verifyQueryResultsNotAvailable() { testPassed("Queries' results didn't become available in a spin loop"); } +function verifyDeleteQueryBehavior() { + debug(""); + debug("Testing deleting an active query should end it."); + + // Use a new context for this test + gl2 = wtu.create3DContext(null, null, 1); + if (!gl2) return; + ext2 = gl2.getExtension("EXT_disjoint_timer_query"); + if (!ext2) return; + + query = ext2.createQueryEXT(); + ext2.beginQueryEXT(ext2.TIME_ELAPSED_EXT, query); + wtu.glErrorShouldBe(gl2, gl2.NONE, "The query began successfully"); + ext2.deleteQueryEXT(query); + wtu.glErrorShouldBe(gl2, gl2.NONE, "Deletion of the active query succeeds"); + shouldBeNull("ext2.getQueryEXT(ext2.TIME_ELAPSED_EXT, ext2.CURRENT_QUERY_EXT)"); + shouldBeFalse("ext2.isQueryEXT(query)"); + query = ext2.createQueryEXT(); + ext2.beginQueryEXT(ext2.TIME_ELAPSED_EXT, query); + wtu.glErrorShouldBe(gl2, gl2.NONE, "Beginning a new query succeeds"); + ext2.endQueryEXT(ext2.TIME_ELAPSED_EXT); + ext2.deleteQueryEXT(query); + wtu.glErrorShouldBe(gl2, gl2.NONE); + query = null; + ext2 = null; + gl2 = null; +} + +function verifyDeleteQueryErrorBehavior() { + debug(""); + debug("Testing deleting a query created by another context."); + + // Use new contexts for this test + gl2 = wtu.create3DContext(null, null, 1); + var gl3 = wtu.create3DContext(null, null, 1); + if (!gl2 || !gl3) return; + ext2 = gl2.getExtension("EXT_disjoint_timer_query"); + var ext3 = gl3.getExtension("EXT_disjoint_timer_query"); + if (!ext2 || !ext3) return; + + query = ext2.createQueryEXT(); + ext2.beginQueryEXT(ext2.TIME_ELAPSED_EXT, query); + ext3.deleteQueryEXT(query); + wtu.glErrorShouldBe(gl3, gl3.INVALID_OPERATION); + shouldBeTrue("ext2.isQueryEXT(query)"); + shouldBe("ext2.getQueryEXT(ext2.TIME_ELAPSED_EXT, ext2.CURRENT_QUERY_EXT)", "query"); + ext2.endQueryEXT(ext2.TIME_ELAPSED_EXT); + ext2.deleteQueryEXT(query); + wtu.glErrorShouldBe(gl2, gl2.NONE); + query = null; + ext2 = null; + gl2 = null; + gl3 = null; +} + function checkQueryResults() { if (availability_retry > 0) { // Make a reasonable attempt to wait for the queries' results to become available. diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-polygon-offset-clamp.html b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-polygon-offset-clamp.html new file mode 100644 index 0000000000..ce14e96fc7 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-polygon-offset-clamp.html @@ -0,0 +1,172 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL EXT_polygon_offset_clamp Conformance Tests</title> +<LINK rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<canvas width="32" height="32" id="c"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the EXT_polygon_offset_clamp extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext(); +var ext; +const w = gl.drawingBufferWidth; +const h = gl.drawingBufferHeight; + +function runTestNoExtension() { + debug(""); + debug("Check the parameter without the extension"); + shouldBeNull("gl.getParameter(0x8E1B /* POLYGON_OFFSET_CLAMP_EXT */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); +} + +function checkEnums() { + debug(""); + debug("Check enums"); + shouldBe("ext.POLYGON_OFFSET_CLAMP_EXT", "0x8E1B"); +} + +function checkQueries() { + debug(""); + debug("Check default state"); + shouldBe('gl.getParameter(gl.POLYGON_OFFSET_FACTOR)', '0.0'); + shouldBe('gl.getParameter(gl.POLYGON_OFFSET_UNITS)', '0.0'); + shouldBe('gl.getParameter(ext.POLYGON_OFFSET_CLAMP_EXT)', '0.0'); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + debug(""); + debug("Check state update using the new function"); + ext.polygonOffsetClampEXT(1.0, 2.0, 3.0); + shouldBe('gl.getParameter(gl.POLYGON_OFFSET_FACTOR)', '1.0'); + shouldBe('gl.getParameter(gl.POLYGON_OFFSET_UNITS)', '2.0'); + shouldBe('gl.getParameter(ext.POLYGON_OFFSET_CLAMP_EXT)', '3.0'); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + debug(""); + debug("Check that the unextended function resets the clamp value to zero"); + gl.polygonOffset(4.0, 5.0); + shouldBe('gl.getParameter(gl.POLYGON_OFFSET_FACTOR)', '4.0'); + shouldBe('gl.getParameter(gl.POLYGON_OFFSET_UNITS)', '5.0'); + shouldBe('gl.getParameter(ext.POLYGON_OFFSET_CLAMP_EXT)', '0.0'); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); +} + +function checkClamping() { + debug(""); + debug("Check polygon offset clamp operation"); + + // The shader creates a depth slope from left (0) to right (1). + // + // This test issues two draw calls: + // + // Draw 2 (green): factor width, offset 0, clamp 0.5, depth test: Greater + // ^ | __________________ + // | | __/ __/ + // | V __/ __/ <--- Draw 1 (red): factor width, offset 0, clamp 0.25 + // | __/ __/ + // | __/ __/ + // |/ __/ + // | __/ + // | __/ + // |/ + // | + // | + // +-------------------------------> + // + // Result: <---------green--------><--red--> + + + const program = wtu.setupProgram(gl, [wtu.simpleVertexShader, + wtu.simpleColorFragmentShader]); + gl.useProgram(program); + const colorLoc = gl.getUniformLocation(program, "u_color"); + + const buf = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([-1, -1, -1, 1, -1, 1, -1, 1, -1, 1, 1, 1]), + gl.STATIC_DRAW); + gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(0); + + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + gl.enable(gl.POLYGON_OFFSET_FILL); + gl.clearColor(0, 0, 0, 1); + gl.clearDepth(0); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + gl.enable(gl.DEPTH_TEST); + + gl.depthFunc(gl.ALWAYS); + gl.uniform4f(colorLoc, 1, 0, 0, 1); + ext.polygonOffsetClampEXT(w, 0, 0.25); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + + gl.depthFunc(gl.GREATER); + gl.uniform4f(colorLoc, 0, 1, 0, 1); + ext.polygonOffsetClampEXT(w, 0, 0.5); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + + wtu.checkCanvasRect( + gl, + 0, 0, 3 * w / 4 - 1, h, + [0, 255, 0, 255], "should be green"); + + wtu.checkCanvasRect( + gl, + 3 * w / 4 + 1, 0, w / 4 - 1, h, + [255, 0, 0, 255], "should be red"); + + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); +} + +function runTestExtension() { + checkEnums(); + checkQueries(); + checkClamping(); +} + +function runTest() { + if (!gl) { + testFailed("WebGL context does not exist"); + return; + } + testPassed("WebGL context exists"); + + runTestNoExtension(); + + ext = gl.getExtension("EXT_polygon_offset_clamp"); + + wtu.runExtensionSupportedTest(gl, "EXT_polygon_offset_clamp", ext !== null); + + if (ext !== null) { + runTestExtension(); + } else { + testPassed("No EXT_polygon_offset_clamp support -- this is legal"); + } +} + +runTest(); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-sRGB.html b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-sRGB.html index 7e8353f7f2..a997bdb4ab 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-sRGB.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-sRGB.html @@ -222,6 +222,40 @@ if (!gl) { runFormatTest(textureFormatFixture, false); runFormatTest(renderbufferFormatFixture, false); + { + var fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + + debug("Checking getFramebufferAttachmentParameter with a renderbuffer"); + { + var rbo = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGB565, 1, 1); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo); + wtu.glErrorShouldBe(gl, gl.NO_ERROR); + shouldBeNull('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, 0x8210 /* FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING_EXT */)'); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + gl.deleteRenderbuffer(rbo); + } + + debug("Checking getFramebufferAttachmentParameter with a texture"); + { + var tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); + wtu.glErrorShouldBe(gl, gl.NO_ERROR); + shouldBeNull('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, 0x8210 /* FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING_EXT */)'); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.deleteTexture(tex); + } + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.deleteFramebuffer(fbo); + } + debug(""); debug("Checking sRGB texture support"); diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-texture-compression-rgtc.html b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-texture-compression-rgtc.html index 70dcf9ba7b..d9c34af25f 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-texture-compression-rgtc.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-texture-compression-rgtc.html @@ -120,16 +120,16 @@ function runTestExtension() { gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, ext.COMPRESSED_RED_RGTC1_EXT, 4, 4, 0, gl.RED, gl.UNSIGNED_BYTE, null); - wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "COMPRESSED_RED_RGTC1_EXT fails with texImage2D"); + wtu.glErrorShouldBe(gl, [gl.INVALID_VALUE, gl.INVALID_OPERATION], "COMPRESSED_RED_RGTC1_EXT fails with texImage2D"); gl.texImage2D(gl.TEXTURE_2D, 0, ext.COMPRESSED_SIGNED_RED_RGTC1_EXT, 4, 4, 0, gl.RED, gl.BYTE, null); - wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "COMPRESSED_SIGNED_RED_RGTC1_EXT fails with texImage2D"); + wtu.glErrorShouldBe(gl, [gl.INVALID_VALUE, gl.INVALID_OPERATION], "COMPRESSED_SIGNED_RED_RGTC1_EXT fails with texImage2D"); gl.texImage2D(gl.TEXTURE_2D, 0, ext.COMPRESSED_RED_GREEN_RGTC2_EXT, 4, 4, 0, gl.RG, gl.UNSIGNED_BYTE, null); - wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "COMPRESSED_RED_GREEN_RGTC2_EXT fails with texImage2D"); + wtu.glErrorShouldBe(gl, [gl.INVALID_VALUE, gl.INVALID_OPERATION], "COMPRESSED_RED_GREEN_RGTC2_EXT fails with texImage2D"); gl.texImage2D(gl.TEXTURE_2D, 0, ext.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT, 4, 4, 0, gl.RG, gl.BYTE, null); - wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT fails with texImage2D"); + wtu.glErrorShouldBe(gl, [gl.INVALID_VALUE, gl.INVALID_OPERATION], "COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT fails with texImage2D"); gl.deleteTexture(tex); } diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-texture-mirror-clamp-to-edge.html b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-texture-mirror-clamp-to-edge.html new file mode 100644 index 0000000000..4e2f980b4b --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/ext-texture-mirror-clamp-to-edge.html @@ -0,0 +1,217 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL EXT_texture_mirror_clamp_to_edge Conformance Tests</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<canvas width="32" height="32" id="c"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the EXT_texture_mirror_clamp_to_edge extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext("c"); +const w = gl.drawingBufferWidth; +const h = gl.drawingBufferHeight; +var ext; +var sampler; + +const pnames = ['TEXTURE_WRAP_S', 'TEXTURE_WRAP_T']; +if (gl.TEXTURE_WRAP_R) { + pnames.push('TEXTURE_WRAP_R'); +} + +function runTestNoExtension() { + debug(""); + debug("Check the texture parameter without the extension"); + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + + const MIRROR_CLAMP_TO_EDGE_EXT = 0x8743; + + for (const pname of pnames) { + gl.texParameteri(gl.TEXTURE_2D, gl[pname], MIRROR_CLAMP_TO_EDGE_EXT); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `value unknown for ${pname} via texParameteri without enabling the extension`); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no more errors"); + gl.texParameterf(gl.TEXTURE_2D, gl[pname], MIRROR_CLAMP_TO_EDGE_EXT); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `value unknown for ${pname} via texParameterf without enabling the extension`); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no more errors"); + } + + if (!gl.createSampler) return; + + const sampler = gl.createSampler(); + for (const pname of pnames) { + gl.samplerParameteri(sampler, gl[pname], MIRROR_CLAMP_TO_EDGE_EXT); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `value unknown for ${pname} via samplerParameteri without enabling the extension`); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no more errors"); + gl.samplerParameterf(sampler, gl[pname], MIRROR_CLAMP_TO_EDGE_EXT); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `value unknown for ${pname} via samplerParameterf without enabling the extension`); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no more errors"); + } +} + +function checkEnums() { + debug(""); + debug("Check enums"); + shouldBe("ext.MIRROR_CLAMP_TO_EDGE_EXT", "0x8743"); +} + +function checkQueries() { + debug(""); + debug("Check texture and sampler state updates"); + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + + for (const pname of pnames) { + gl.texParameteri(gl.TEXTURE_2D, gl[pname], ext.MIRROR_CLAMP_TO_EDGE_EXT); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors from texParameteri"); + shouldBe(`gl.getTexParameter(gl.TEXTURE_2D, gl.${pname})`, "ext.MIRROR_CLAMP_TO_EDGE_EXT"); + gl.texParameteri(gl.TEXTURE_2D, gl[pname], gl.REPEAT); + + gl.texParameterf(gl.TEXTURE_2D, gl[pname], ext.MIRROR_CLAMP_TO_EDGE_EXT); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors from texParameterf"); + shouldBe(`gl.getTexParameter(gl.TEXTURE_2D, gl.${pname})`, "ext.MIRROR_CLAMP_TO_EDGE_EXT"); + gl.texParameterf(gl.TEXTURE_2D, gl[pname], gl.REPEAT); + } + + if (!gl.createSampler) return; + + sampler = gl.createSampler(); + for (const pname of pnames) { + gl.samplerParameteri(sampler, gl[pname], ext.MIRROR_CLAMP_TO_EDGE_EXT); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors from samplerParameteri"); + shouldBe(`gl.getSamplerParameter(sampler, gl.${pname})`, "ext.MIRROR_CLAMP_TO_EDGE_EXT"); + gl.samplerParameteri(sampler, gl[pname], gl.REPEAT); + + gl.samplerParameterf(sampler, gl[pname], ext.MIRROR_CLAMP_TO_EDGE_EXT); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors from samplerParameterf"); + shouldBe(`gl.getSamplerParameter(sampler, gl.${pname})`, "ext.MIRROR_CLAMP_TO_EDGE_EXT"); + gl.samplerParameterf(sampler, gl[pname], gl.REPEAT); + } +} + +function checkSampling() { + debug(""); + debug(`Check texture sampling with mirror-clamp-to-edge mode`); + + wtu.setupUnitQuad(gl); + const vs = `precision highp float; + attribute vec4 vPosition; + varying vec2 texCoord; + void main() { + gl_Position = vec4(vPosition.xy, 0.0, 1.0); + texCoord = vPosition.xy * 2.0; + }`; + const program = wtu.setupProgram(gl, [vs, wtu.simpleTextureFragmentShader]); + gl.useProgram(program); + + const black = [ 0, 0, 0, 255]; + const red = [255, 0, 0, 255]; + const green = [ 0, 255, 0, 255]; + const blue = [ 0, 0, 255, 255]; + const data = new Uint8Array([...black, ...red, ...green, ...blue]); + + function checkPixels() { + function checkPixel(x, y, color) { + const screen = (s, t) => s * (t * 0.5 + 0.5); + wtu.checkCanvasRect(gl, screen(w, x), screen(h, y), 1, 1, color, + `(${x.toFixed(3)}, ${y.toFixed(3)}): ${color} `); + } + for (const signX of [+1, -1]) { + for (const signY of [+1, -1]) { + // This function expects screen-space coordinates + // normalized to [-1, +1]. The region from [0, 0] + // to [+1, +1] behaves like regular clamp-to-edge. + // Other three quadrants should be mirrored. + checkPixel(signX * 0.125, signY * 0.125, black); + checkPixel(signX * 0.375, signY * 0.125, red); + checkPixel(signX * 0.750, signY * 0.125, red); + checkPixel(signX * 0.125, signY * 0.375, green); + checkPixel(signX * 0.125, signY * 0.750, green); + checkPixel(signX * 0.375, signY * 0.375, blue); + checkPixel(signX * 0.750, signY * 0.375, blue); + checkPixel(signX * 0.375, signY * 0.750, blue); + checkPixel(signX * 0.750, signY * 0.750, blue); + } + } + } + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, ext.MIRROR_CLAMP_TO_EDGE_EXT); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, ext.MIRROR_CLAMP_TO_EDGE_EXT); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "texture created and configured"); + + wtu.drawUnitQuad(gl); + checkPixels(); + + if (!gl.createSampler) return; + + debug(""); + debug(`Check texture sampling with mirror-clamp-to-edge mode using a sampler object`); + + const texWithSampler = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texWithSampler); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 2, 2, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); + + sampler = gl.createSampler(); + gl.bindSampler(0, sampler); + gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_S, ext.MIRROR_CLAMP_TO_EDGE_EXT); + gl.samplerParameteri(sampler, gl.TEXTURE_WRAP_T, ext.MIRROR_CLAMP_TO_EDGE_EXT); + gl.samplerParameteri(sampler, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.samplerParameteri(sampler, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "texture created and sampler configured"); + + wtu.drawUnitQuad(gl); + checkPixels(); +} + +function runTest() { + if (!gl) { + testFailed("context does not exist"); + return; + } + testPassed("context exists"); + + runTestNoExtension(); + + ext = gl.getExtension("EXT_texture_mirror_clamp_to_edge"); + wtu.runExtensionSupportedTest(gl, "EXT_texture_mirror_clamp_to_edge", ext !== null); + + if (ext !== null) { + checkEnums(); + checkQueries(); + checkSampling(); + } else { + testPassed("No EXT_texture_mirror_clamp_to_edge support -- this is legal"); + } +} + +runTest(); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/khr-parallel-shader-compile.html b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/khr-parallel-shader-compile.html index 2915a2d2de..6a8b1981e5 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/khr-parallel-shader-compile.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/khr-parallel-shader-compile.html @@ -169,6 +169,32 @@ let extraCode = ''; testPassed(`COMPLETION_STATUS_KHR sucessfully transitioned from false to true`); } + debug("Checking that compiling lots of programs in parallel eventually completes."); + let programs = []; + for (let i = 0; i < 256; ++i) { + gl.shaderSource(vs, vertexSource()); + gl.shaderSource(fs, fragmentSource()); + gl.compileShader(vs); + gl.compileShader(fs); + let program = gl.createProgram(); + gl.attachShader(program, vs); + gl.attachShader(program, fs); + gl.linkProgram(program); + programs.push(program); + } + let allDone = false; + while (!allDone) { + allDone = true; + for (let i = 0; i < programs.length; ++i) { + if (!gl.getProgramParameter(programs[i], COMPLETION_STATUS_KHR)) { + allDone = false; + break; + } + } + if (!allDone) { + await new Promise(requestAnimationFrame); + } + } debug("Checking that status is true when context is lost."); if (loseContext) { diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/oes-texture-float-with-video.html b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/oes-texture-float-with-video.html index 3ccbd50f15..cb406706f9 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/oes-texture-float-with-video.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/oes-texture-float-with-video.html @@ -33,7 +33,6 @@ function testPrologue(gl) { <video width="640" height="228" id="vid" controls muted> <source src="../../resources/red-green.mp4" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"' /> <source src="../../resources/red-green.webmvp8.webm" type='video/webm; codecs="vp8, vorbis"' /> - <source src="../../resources/red-green.theora.ogv" type='video/ogg; codecs="theora, vorbis"' /> </video> </body> </html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/oes-texture-float.html b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/oes-texture-float.html index a757cb22ec..8bec35b9cf 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/oes-texture-float.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/oes-texture-float.html @@ -60,6 +60,7 @@ debug(""); var wtu = WebGLTestUtils; var canvas = document.getElementById("canvas"); var gl = wtu.create3DContext(canvas); +var ext = null; if (!gl) { testFailed("WebGL context does not exist"); @@ -81,6 +82,33 @@ if (!gl) { // the extension has not been enabled yet. runTextureCreationTest(testProgram, false); + { + debug(""); + debug("Testing that component type framebuffer attachment queries are rejected with the extension disabled"); + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + + const rbo = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,gl.RENDERBUFFER, rbo); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGB565, 8, 8); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Setup renderbuffer should succeed."); + shouldBeNull('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, 0x8211 /* FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT */)'); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "Query must fail."); + gl.deleteRenderbuffer(rbo); + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 8, 8, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Setup texture should succeed."); + shouldBeNull('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, 0x8211 /* FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT */)'); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "Query must fail."); + gl.deleteTexture(tex); + + gl.deleteFramebuffer(fbo); + } + if (!gl.getExtension("OES_texture_float")) { testPassed("No OES_texture_float support -- this is legal"); } else { @@ -109,6 +137,43 @@ if (!gl) { runRenderTargetAndReadbackTest(testProgram, gl.RGBA, 4, [10000, 10000, 10000, 10000], 0.5, true); runFramebufferTest(); + { + debug(""); + debug("Testing that component type framebuffer attachment queries are accepted with the extension enabled"); + ext = gl.getExtension("WEBGL_color_buffer_float"); + shouldBeNonNull('ext'); + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + + const rbo = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,gl.RENDERBUFFER, rbo); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGB565, 8, 8); + shouldBe('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, ext.FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT)', 'ext.UNSIGNED_NORMALIZED_EXT'); + gl.renderbufferStorage(gl.RENDERBUFFER, ext.RGBA32F_EXT, 8, 8); + shouldBe('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, ext.FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT)', 'gl.FLOAT'); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "No errors after valid renderbuffer attachment queries."); + + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT,gl.RENDERBUFFER, rbo); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, 8, 8); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "No errors after depth-stencil renderbuffer setup."); + shouldBeNull('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, ext.FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT)'); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Component type query is not allowed for combined depth-stencil attachments."); + gl.deleteRenderbuffer(rbo); + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 8, 8, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + shouldBe('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, ext.FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT)', 'ext.UNSIGNED_NORMALIZED_EXT'); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 8, 8, 0, gl.RGBA, gl.FLOAT, null); + shouldBe('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, ext.FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT)', 'gl.FLOAT'); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "No errors after valid texture attachment queries."); + gl.deleteTexture(tex); + + gl.deleteFramebuffer(fbo); + } + debug(""); debug("Test float32 blending without EXT_float_blend."); testExtFloatBlend(gl.RGBA); diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/oes-texture-half-float-with-video.html b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/oes-texture-half-float-with-video.html index 719b332113..d6076b29f9 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/oes-texture-half-float-with-video.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/oes-texture-half-float-with-video.html @@ -38,7 +38,6 @@ function testPrologue(gl) { <video width="640" height="228" id="vid" controls muted> <source src="../../resources/red-green.mp4" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"' /> <source src="../../resources/red-green.webmvp8.webm" type='video/webm; codecs="vp8, vorbis"' /> - <source src="../../resources/red-green.theora.ogv" type='video/ogg; codecs="theora, vorbis"' /> </video> </body> </html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/webgl-blend-func-extended.html b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/webgl-blend-func-extended.html new file mode 100644 index 0000000000..62e25adaa2 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/webgl-blend-func-extended.html @@ -0,0 +1,26 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL 1.0 WEBGL_blend_func_extended Conformance Tests</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<canvas width="32" height="32" id="c"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +var contextVersion = 1; +</script> +<script src="../../js/tests/webgl-blend-func-extended.js"></script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/extensions/webgl-polygon-mode.html b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/webgl-polygon-mode.html new file mode 100644 index 0000000000..fd65f692a6 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance/extensions/webgl-polygon-mode.html @@ -0,0 +1,185 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL WEBGL_polygon_mode Conformance Tests</title> +<LINK rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<canvas width="32" height="32" id="c"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the WEBGL_polygon_mode extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext("c"); +var ext; +const w = gl.drawingBufferWidth; +const h = gl.drawingBufferHeight; + +function runTestNoExtension() { + debug(""); + debug("Check the parameters without the extension"); + shouldBeNull("gl.getParameter(0x0B40 /* POLYGON_MODE_WEBGL */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + shouldBeNull("gl.getParameter(0x2A02 /* POLYGON_OFFSET_LINE_WEBGL */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + debug("Check the cap without the extension"); + gl.disable(0x2A02 /* POLYGON_OFFSET_LINE_WEBGL */); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "cap unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + gl.enable(0x2A02 /* POLYGON_OFFSET_LINE_WEBGL */); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "cap unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + shouldBeFalse("gl.isEnabled(0x2A02 /* POLYGON_OFFSET_LINE_WEBGL */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "cap unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); +} + +function checkEnums() { + debug(""); + debug("Check enums"); + shouldBe("ext.POLYGON_MODE_WEBGL", "0x0B40"); + shouldBe("ext.POLYGON_OFFSET_LINE_WEBGL", "0x2A02"); + shouldBe("ext.LINE_WEBGL", "0x1B01"); + shouldBe("ext.FILL_WEBGL", "0x1B02"); +} + +function checkQueries() { + debug(""); + debug("Check default state"); + shouldBe('gl.getParameter(ext.POLYGON_MODE_WEBGL)', 'ext.FILL_WEBGL'); + shouldBeFalse('gl.getParameter(ext.POLYGON_OFFSET_LINE_WEBGL)'); + shouldBeFalse('gl.isEnabled(ext.POLYGON_OFFSET_LINE_WEBGL)'); + debug(""); + debug("Check state updates"); + ext.polygonModeWEBGL(gl.FRONT_AND_BACK, ext.LINE_WEBGL); + shouldBe('gl.getParameter(ext.POLYGON_MODE_WEBGL)', 'ext.LINE_WEBGL'); + ext.polygonModeWEBGL(gl.FRONT_AND_BACK, ext.FILL_WEBGL); + shouldBe('gl.getParameter(ext.POLYGON_MODE_WEBGL)', 'ext.FILL_WEBGL'); + debug(""); + debug("Check errors"); + ext.polygonModeWEBGL(gl.FRONT, ext.LINE_WEBGL); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "invalid face"); + ext.polygonModeWEBGL(gl.FRONT_AND_BACK, 0); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "invalid mode"); + shouldBe('gl.getParameter(ext.POLYGON_MODE_WEBGL)', 'ext.FILL_WEBGL'); + debug(""); + debug("Check cap updates"); + gl.enable(ext.POLYGON_OFFSET_LINE_WEBGL); + shouldBeTrue('gl.getParameter(ext.POLYGON_OFFSET_LINE_WEBGL)'); + shouldBeTrue('gl.isEnabled(ext.POLYGON_OFFSET_LINE_WEBGL)'); + gl.disable(ext.POLYGON_OFFSET_LINE_WEBGL); + shouldBeFalse('gl.getParameter(ext.POLYGON_OFFSET_LINE_WEBGL)'); + shouldBeFalse('gl.isEnabled(ext.POLYGON_OFFSET_LINE_WEBGL)'); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); +} + +function checkDiagonal(r, g, b) { + const pixels = new Uint8Array(w * h * 4); + gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + for (let i = 0; i < w; i++) + { + const baseOffset = (i * w + i) * 4; + if (pixels[baseOffset + 0] != r || + pixels[baseOffset + 1] != g || + pixels[baseOffset + 2] != b) { + testFailed(`Unexpected diagonal color at (${i}, ${i})`); + return; + } + } + testPassed("Expected diagonal color"); +} + +function checkLineMode() { + debug(""); + debug("Check line polygon mode"); + + gl.enable(gl.DEPTH_TEST); + + const program = wtu.setupProgram(gl, [wtu.simpleVertexShader, + wtu.simpleColorFragmentShader]); + gl.useProgram(program); + const colorLoc = gl.getUniformLocation(program, "u_color"); + + wtu.setupUnitQuad(gl); + + // Draw red quad with lines + gl.uniform4f(colorLoc, 1, 0, 0, 1); + ext.polygonModeWEBGL(gl.FRONT_AND_BACK, ext.LINE_WEBGL); + wtu.clearAndDrawUnitQuad(gl); + + // Nothing is drawn inside triangles + wtu.checkCanvasRect(gl, 2, 17, 13, 13, [255, 255, 255, 255]); + wtu.checkCanvasRect(gl, 17, 2, 13, 13, [255, 255, 255, 255]); + + // Main diagonal is drawn + checkDiagonal(255, 0, 0); + + // Test polygon offset + gl.polygonOffset(0, -2); + gl.enable(gl.POLYGON_OFFSET_FILL); + + // Depth test must fail because line mode uses its own polygon offset toggle + gl.uniform4f(colorLoc, 0, 1, 0, 1); + wtu.drawUnitQuad(gl); + checkDiagonal(255, 0, 0); + + // Depth test must pass + gl.enable(ext.POLYGON_OFFSET_LINE_WEBGL) + wtu.drawUnitQuad(gl); + checkDiagonal(0, 255, 0); + + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); +} + +function runTestExtension() { + checkEnums(); + checkQueries(); + checkLineMode(); +} + +function runTest() { + if (!gl) { + testFailed("WebGL context does not exist"); + return; + } + testPassed("WebGL context exists"); + + runTestNoExtension(); + + ext = gl.getExtension("WEBGL_polygon_mode"); + + wtu.runExtensionSupportedTest(gl, "WEBGL_polygon_mode", ext !== null); + + if (ext !== null) { + runTestExtension(); + } else { + testPassed("No WEBGL_polygon_mode support -- this is legal"); + } +} + +runTest(); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/reading/read-pixels-test.html b/dom/canvas/test/webgl-conf/checkout/conformance/reading/read-pixels-test.html index 078b436427..3791be2448 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/reading/read-pixels-test.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance/reading/read-pixels-test.html @@ -39,6 +39,43 @@ function runTest(canvas, antialias) { gl = wtu.create3DContext(canvas, {antialias: antialias}); var contextVersion = wtu.getDefault3DContextVersion(); + debug(""); + debug("Test null pixels"); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, null); + wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "null pixels"); + + debug(""); + debug("Test pixels size"); + gl.readPixels(0, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(0)); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "empty pixels array with 0x0 read data"); + gl.readPixels(0, 0, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(0)); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "empty pixels array with 1x0 read data"); + gl.readPixels(0, 0, 0, 1, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(0)); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "empty pixels array with 0x1 read data"); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(3)); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "small pixels array for 1x1 read data"); + if (contextVersion >= 2) { + gl.readPixels(0, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(0), 1); + wtu.glErrorShouldBe(gl, gl.INVALID_VALUE, "offset is greater than array size"); + gl.readPixels(0, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(1), 1); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "no space left in pixels array with 0x0 read data"); + gl.readPixels(0, 0, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(1), 1); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "no space left in pixels array with 1x0 read data"); + gl.readPixels(0, 0, 0, 1, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(1), 1); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "no space left in pixels array with 0x1 read data"); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(4), 1); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "no space left in pixels array with 1x1 read data"); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(5), 1); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "read 1x1 data fits into pixels with offset"); + } + + debug(""); + debug("Test combined depth-stencil type"); + // The combined type is undefined in WebGL 1.0 and never allowed as a read type in WebGL 2.0 + gl.readPixels(0, 0, 1, 1, gl.RGBA, 0x8DAD /* FLOAT_32_UNSIGNED_INT_24_8_REV */, new Uint8Array(32)); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "FLOAT_32_UNSIGNED_INT_24_8_REV is rejected"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "no extra error generated"); + var width = 2; var height = 2; var continueTestFunc = continueTestPart1; diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/exif-orientation.html b/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/exif-orientation.html index 3fd596d445..5a4b88e5b8 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/exif-orientation.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/exif-orientation.html @@ -56,35 +56,20 @@ function checkPixels(flipY) } } -async function testImageBitmapFromBlobWithFlipY(blob, flipY) +async function testImageBitmapWithFlipY(source, flipY) { - let bitmap; - // As a concession to Firefox, which doesn't yet implement - // createImageBitmap with creation options, skip the tests - // involving flipY=true if ImageBitmap creation throws an - // exception, and use the single-argument constructor for the - // flipY=false case. - if (flipY) { - try { - bitmap = await createImageBitmap(blob, {imageOrientation: flipY}); - } catch (e) { - output(" (createImageBitmap options not supported - skipping flipY=true case)"); - return; - } - } else { - bitmap = await createImageBitmap(blob); - } + const bitmap = await createImageBitmap(source, flipY ? {imageOrientation: flipY} : undefined); output(" Testing texImage2D, flipY = " + flipY); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmap); wtu.clearAndDrawUnitQuad(gl, [0, 0, 0, 255]); - checkPixels(flipY); + checkPixels(flipY == "flipY"); output(" Testing texSubImage2D, flipY = " + flipY); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, bitmap.width, bitmap.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, bitmap); wtu.clearAndDrawUnitQuad(gl, [0, 0, 0, 255]); - checkPixels(flipY); + checkPixels(flipY == "flipY"); } async function testImageBitmapFromBlob(filename) @@ -93,8 +78,16 @@ async function testImageBitmapFromBlob(filename) let blob = await response.blob(); output("----------------------------------------------------------------"); output("Testing " + filename + " via ImageBitmap from Blob"); - await testImageBitmapFromBlobWithFlipY(blob, true); - await testImageBitmapFromBlobWithFlipY(blob, false); + await testImageBitmapWithFlipY(blob, "flipY"); + await testImageBitmapWithFlipY(blob, "none"); + await testImageBitmapWithFlipY(blob, undefined); +} + +async function testImageBitmapFromImage(image) +{ + await testImageBitmapWithFlipY(image, "flipY"); + await testImageBitmapWithFlipY(image, "none"); + await testImageBitmapWithFlipY(image, undefined); } async function testImageElementWithFlipY(image, flipY) @@ -124,6 +117,11 @@ async function testImageElement(filename) await testImageElementWithFlipY(image, true); await testImageElementWithFlipY(image, false); + + output("----------------------------------------------------------------"); + output("Testing " + filename + " via ImageBitmap from HTMLImageElement"); + + await testImageBitmapFromImage(image); } async function testSingleImage(filename) @@ -149,9 +147,9 @@ async function run() "exif-orientation-test-3-rotate-180.jpg", "exif-orientation-test-4-mirror-vertical.jpg", "exif-orientation-test-5-mirror-horizontal-90-ccw.jpg", - "exif-orientation-test-6-90-ccw.jpg", + "exif-orientation-test-6-90-cw.jpg", "exif-orientation-test-7-mirror-horizontal-90-cw.jpg", - "exif-orientation-test-8-90-cw.jpg", + "exif-orientation-test-8-90-ccw.jpg", ]; for (let fn of filenames) { diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/tex-video-using-tex-unit-non-zero.html b/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/tex-video-using-tex-unit-non-zero.html index ee9bad4341..1acc3a4380 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/tex-video-using-tex-unit-non-zero.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/tex-video-using-tex-unit-non-zero.html @@ -96,8 +96,6 @@ found in the LICENSE.txt file. type: 'video/webm; codecs="vp8, vorbis"' }, { src: resourcePath + "red-green.bt601.vp9.webm", type: 'video/webm; codecs="vp9"' }, - { src: resourcePath + "red-green.theora.ogv", - type: 'video/ogg; codecs="theora, vorbis"' }, ]; var currentVideo = null; diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/texture-npot-video.html b/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/texture-npot-video.html index ef979d4c5a..9a8e3198ba 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/texture-npot-video.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/texture-npot-video.html @@ -154,7 +154,6 @@ function runTest(videoElement) <video id="vid" style="display:none;" muted> <source src="../../../resources/npot-video.mp4" type='video/mp4; codecs="avc1.42E01E"' /> <source src="../../../resources/npot-video.webmvp8.webm" type='video/webm; codecs="vp8"' /> - <source src="../../../resources/npot-video.theora.ogv" type='video/ogg; codecs="theora"' /> </video> </body> </html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/texture-srgb-upload.html b/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/texture-srgb-upload.html index 3508670563..6f66bfdde6 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/texture-srgb-upload.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/texture-srgb-upload.html @@ -62,12 +62,22 @@ function invoke(fn) { return fn(); } invoke(async () => { const video = document.createElement("video"); video.src = DATA_URL_FOR_720p_png_bt709_bt709_tv_yuv420p_vp9_webm; - //video.src = "Big_Buck_Bunny_360_10s_1MB.mp4"; - //video.src = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; + if (!video.canPlayType('video/webm')) { + debug('Browser can not play webm videos. Skipping test.'); + finishTest(); + return; + } + video.muted = true; video.loop = true; video.crossOrigin = "anonymous"; - await video.play(); + try { + await video.play(); + } catch (e) { + debug('Browser could not play this specific video. Skipping test.'); + finishTest(); + return; + } function renderTex(canvas, fn_tex_image) { const gl = canvas.gl = wtu.create3DContext(canvas); diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/texture-upload-size.html b/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/texture-upload-size.html index a0f30dc89f..b5ba6afed3 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/texture-upload-size.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance/textures/misc/texture-upload-size.html @@ -99,7 +99,6 @@ var tests = [ {type: "video", src: "../../../resources/red-green.mp4", videoType: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'}, {type: "video", src: "../../../resources/red-green.bt601.vp9.webm", videoType: 'video/webm; codecs="vp9"'}, {type: "video", src: "../../../resources/red-green.webmvp8.webm", videoType: 'video/webm; codecs="vp8, vorbis"'}, - {type: "video", src: "../../../resources/red-green.theora.ogv", videoType: 'video/ogg; codecs="theora, vorbis"'}, ]; var testIndex = 0; diff --git a/dom/canvas/test/webgl-conf/checkout/conformance/uniforms/uniform-location.html b/dom/canvas/test/webgl-conf/checkout/conformance/uniforms/uniform-location.html index 3b1c185caf..6de8114651 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance/uniforms/uniform-location.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance/uniforms/uniform-location.html @@ -68,6 +68,10 @@ contextA.stencilMask(1); wtu.shouldGenerateGLError(contextA, contextA.NO_ERROR, "contextA.linkProgram(programS)"); wtu.shouldGenerateGLError(contextA, contextA.INVALID_OPERATION, "contextA.uniform1i(locationSx, 3)"); wtu.shouldGenerateGLError(contextA, contextA.INVALID_OPERATION, "contextA.getUniform(programS, locationSx)"); +// Make sure that with no current program, uniform location validation doesn't get confused. +wtu.shouldGenerateGLError(contextA, contextA.NO_ERROR, "contextA.useProgram(null)"); +wtu.shouldGenerateGLError(contextA, contextA.INVALID_OPERATION, "contextA.uniform1i(locationSx, 3)"); +wtu.shouldGenerateGLError(contextA, contextA.NO_ERROR, "contextA.useProgram(programS)"); // Retrieve the locations again, and they should be good. locationSx = contextA.getUniformLocation(programS, "u_struct.x"); diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/00_test_list.txt b/dom/canvas/test/webgl-conf/checkout/conformance2/00_test_list.txt index e251dc9758..fabc72c1da 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/00_test_list.txt +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/00_test_list.txt @@ -18,3 +18,4 @@ textures/00_test_list.txt transform_feedback/00_test_list.txt uniforms/00_test_list.txt vertex_arrays/00_test_list.txt +--min-version 2.0.1 wasm/00_test_list.txt diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/canvas/00_test_list.txt b/dom/canvas/test/webgl-conf/checkout/conformance2/canvas/00_test_list.txt index 35e011f3bf..878a2797c4 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/canvas/00_test_list.txt +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/canvas/00_test_list.txt @@ -1 +1,2 @@ +--min-version 2.0.1 drawingbuffer-storage-test.html --min-version 2.0.1 to-data-url-with-pack-params.html diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/canvas/drawingbuffer-storage-test.html b/dom/canvas/test/webgl-conf/checkout/conformance2/canvas/drawingbuffer-storage-test.html new file mode 100644 index 0000000000..6f18d441c0 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/canvas/drawingbuffer-storage-test.html @@ -0,0 +1,27 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL2 drawingBufferStorage Conformance Tests</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +<script src="../../js/tests/drawingbuffer-storage-test.js"></script> +</head> +<body> +<div id="description"></div> +<div id="console"></div> +<script> +runTest(2); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/00_test_list.txt b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/00_test_list.txt index 559071ff06..2cc4456ecb 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/00_test_list.txt +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/00_test_list.txt @@ -1,11 +1,16 @@ ext-color-buffer-float.html --min-version 2.0.1 ext-color-buffer-half-float.html +--min-version 2.0.1 ext-conservative-depth.html ext-disjoint-timer-query-webgl2.html +--min-version 2.0.1 ext-render-snorm.html --min-version 2.0.1 ext-texture-filter-anisotropic.html --min-version 2.0.1 ext-texture-norm16.html promoted-extensions.html promoted-extensions-in-shaders.html +--min-version 2.0.1 nv-shader-noperspective-interpolation.html --min-version 2.0.1 oes-draw-buffers-indexed.html +--min-version 2.0.1 oes-sample-variables.html +--min-version 2.0.1 oes-shader-multisample-interpolation.html --min-version 2.0.1 ovr_multiview2.html --min-version 2.0.1 ovr_multiview2_depth.html --min-version 2.0.1 ovr_multiview2_draw_buffers.html @@ -16,4 +21,10 @@ promoted-extensions-in-shaders.html --min-version 2.0.1 ovr_multiview2_timer_query.html --min-version 2.0.1 ovr_multiview2_transform_feedback.html --min-version 2.0.1 required-extensions.html +--min-version 2.0.1 webgl-blend-func-extended.html +--min-version 2.0.1 webgl-clip-cull-distance.html --min-version 2.0.1 webgl-multi-draw-instanced-base-vertex-base-instance.html +--min-version 2.0.1 webgl-provoking-vertex.html +--min-version 2.0.1 webgl-render-shared-exponent.html +--min-version 2.0.1 webgl-shader-pixel-local-storage.html +--min-version 2.0.1 webgl-stencil-texturing.html diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/ext-conservative-depth.html b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/ext-conservative-depth.html new file mode 100644 index 0000000000..c9c9f85bdb --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/ext-conservative-depth.html @@ -0,0 +1,145 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL EXT_conservative_depth Conformance Tests</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<canvas width="32" height="32" id="c"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the EXT_conservative_depth extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext("c", null, 2); +var ext; + +function runShaderTests(extensionEnabled) { + debug(""); + debug("Testing various shader compiles with extension " + (extensionEnabled ? "enabled" : "disabled")); + + const macro = `#version 300 es + precision mediump float; + out vec4 my_FragColor; + void main() { + #ifdef GL_EXT_conservative_depth + my_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + #else + #error no GL_EXT_conservative_depth; + #endif + }`; + + const missingExtension = `#version 300 es + precision mediump float; + out vec4 my_FragColor; + layout (depth_any) out float gl_FragDepth; + void main() { + my_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + gl_FragDepth = 1.0; + }`; + + const valid = `#version 300 es + #extension GL_EXT_conservative_depth : enable + precision mediump float; + out vec4 my_FragColor; + layout (depth_any) out float gl_FragDepth; + void main() { + my_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + gl_FragDepth = 1.0; + }`; + + const invalid = `#version 300 es + #extension GL_EXT_conservative_depth : enable + precision mediump float; + out vec4 my_FragColor; + layout (depth_unchanged) out float gl_FragDepth; + void main() { + my_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + gl_FragDepth = 1.0; + }`; + + // Always expect the shader missing the #extension pragma to fail (whether enabled or not) + if (wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, missingExtension])) { + testFailed("Depth layout qualifier allowed without #extension pragma"); + } else { + testPassed("Depth layout qualifier disallowed without #extension pragma"); + } + + // Expect the macro shader to succeed ONLY if enabled + if (wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, macro])) { + if (extensionEnabled) { + testPassed("Macro defined in shaders when extension is enabled"); + } else { + testFailed("Macro defined in shaders when extension is disabled"); + } + } else { + if (extensionEnabled) { + testFailed("Macro not defined in shaders when extension is enabled"); + } else { + testPassed("Macro not defined in shaders when extension is disabled"); + } + } + + // Try to compile a shader using a layout qualifier that should only succeed if enabled + if (wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, valid])) { + if (extensionEnabled) { + testPassed("Depth layout qualifier compiled successfully when extension enabled"); + } else { + testFailed("Depth layout qualifier compiled successfully when extension disabled"); + } + } else { + if (extensionEnabled) { + testFailed("Depth layout qualifier failed to compile when extension enabled"); + } else { + testPassed("Depth layout qualifier failed to compile when extension disabled"); + } + } + + // Try to compile a shader using a disallowed layout qualifier + if (wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, invalid])) { + testFailed("Unsupported depth layout qualifier compiled successfully"); + } else { + testPassed("Unsupported depth layout qualifier failed to compile"); + } +} + +function runTest() { + if (!gl) { + testFailed("WebGL context does not exist"); + return; + } + testPassed("WebGL context exists"); + + runShaderTests(false); + + ext = gl.getExtension("EXT_conservative_depth"); + wtu.runExtensionSupportedTest(gl, "EXT_conservative_depth", ext !== null); + + if (!ext) { + testPassed("No EXT_conservative_depth support -- this is legal"); + } else { + testPassed("Successfully enabled EXT_conservative_depth extension"); + runShaderTests(true); + } +} + +runTest(); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/ext-disjoint-timer-query-webgl2.html b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/ext-disjoint-timer-query-webgl2.html index c051fa36a3..f1e9a82d8a 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/ext-disjoint-timer-query-webgl2.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/ext-disjoint-timer-query-webgl2.html @@ -25,7 +25,9 @@ description("This test verifies the functionality of the EXT_disjoint_timer_quer var wtu = WebGLTestUtils; var canvas = document.getElementById("canvas"); var gl = wtu.create3DContext(canvas, null, 2); +var gl2 = null; var ext = null; +var ext2 = null; var query = null; var query2 = null; var elapsed_query = null; @@ -62,6 +64,8 @@ if (!gl) { wtu.glErrorShouldBe(gl, gl.NO_ERROR); } verifyQueryResultsNotAvailable(); + verifyDeleteQueryBehavior(); + verifyDeleteQueryErrorBehavior(); wtu.glErrorShouldBe(gl, gl.NO_ERROR); window.requestAnimationFrame(checkQueryResults); @@ -249,6 +253,60 @@ function verifyQueryResultsNotAvailable() { testPassed("Queries' results didn't become available in a spin loop"); } +function verifyDeleteQueryBehavior() { + debug(""); + debug("Testing deleting an active query should end it."); + + // Use a new context for this test + gl2 = wtu.create3DContext(null, null, 2); + if (!gl2) return; + ext2 = gl2.getExtension("EXT_disjoint_timer_query_webgl2"); + if (!ext2) return; + + query = gl2.createQuery(); + gl2.beginQuery(ext.TIME_ELAPSED_EXT, query); + wtu.glErrorShouldBe(gl2, gl2.NONE, "The query began successfully"); + gl2.deleteQuery(query); + wtu.glErrorShouldBe(gl2, gl2.NONE, "Deletion of the active query succeeds"); + shouldBeNull("gl2.getQuery(ext2.TIME_ELAPSED_EXT, gl2.CURRENT_QUERY)"); + shouldBeFalse("gl2.isQuery(query)"); + query = gl2.createQuery(); + gl2.beginQuery(ext2.TIME_ELAPSED_EXT, query); + wtu.glErrorShouldBe(gl, gl2.NONE, "Beginning a new query succeeds"); + gl2.endQuery(gl2.TIME_ELAPSED_EXT); + gl2.deleteQuery(query); + wtu.glErrorShouldBe(gl, gl.NONE); + query = null; + ext2 = null; + gl2 = null; +} + +function verifyDeleteQueryErrorBehavior() { + debug(""); + debug("Testing deleting a query created by another context."); + + // Use new contexts for this test + gl2 = wtu.create3DContext(null, null, 2); + var gl3 = wtu.create3DContext(null, null, 2); + if (!gl2 || !gl3) return; + ext2 = gl2.getExtension("EXT_disjoint_timer_query_webgl2"); + if (!ext2) return; + + query = gl2.createQuery(); + gl2.beginQuery(ext2.TIME_ELAPSED_EXT, query); + gl3.deleteQuery(query); + wtu.glErrorShouldBe(gl3, gl3.INVALID_OPERATION); + shouldBeTrue("gl2.isQuery(query)"); + shouldBe("gl2.getQuery(ext2.TIME_ELAPSED_EXT, gl2.CURRENT_QUERY)", "query"); + gl2.endQuery(ext2.TIME_ELAPSED_EXT); + gl2.deleteQuery(query); + wtu.glErrorShouldBe(gl2, gl2.NONE); + query = null; + ext2 = null; + gl2 = null; + gl3 = null; +} + function checkQueryResults() { if (availability_retry > 0) { // Make a reasonable attempt to wait for the queries' results to become available. diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/ext-render-snorm.html b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/ext-render-snorm.html new file mode 100644 index 0000000000..723e762773 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/ext-render-snorm.html @@ -0,0 +1,201 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL EXT_render_snorm Conformance Tests</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the EXT_render_snorm extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext(null, null, 2); +var ext; + +function createTypedArray(type) { + switch (type) { + case gl.BYTE: + return new Int8Array(4); + case gl.UNSIGNED_BYTE: + return new Uint8Array(4); + case gl.SHORT: + return new Int16Array(4); + case gl.UNSIGNED_SHORT: + return new Uint16Array(4); + default: + return null; + } +} + +function drawTest(config) { + wtu.drawUnitQuad(gl); + + const implementationType = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_TYPE); + const implementationFormat = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_FORMAT); + + // Support for reading signed data with unsigned read type is not required + // but implementations may allow such conversions. Do not expect the error + // when the type matches the buffer type or when it's explicitly supported. + for (const type of [gl.BYTE, gl.UNSIGNED_BYTE, gl.SHORT, gl.UNSIGNED_SHORT]) { + if (type == config.type) continue; + if (implementationFormat != gl.RGBA || implementationType != type) { + gl.readPixels(0, 0, 1, 1, gl.RGBA, type, createTypedArray(type)); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "reading with unsupported type fails"); + } + } + + const defaultPixel = createTypedArray(config.type); + wtu.checkCanvasRect(gl, 0, 0, 1, 1, config.color, + "reading with the RGBA format and matching type", 1, + defaultPixel, + config.type, gl.RGBA); + + if (implementationFormat == config.format && implementationType == config.type) { + const implementationPixel = createTypedArray(implementationType); + const color = [config.color[0]]; + if (config.format != gl.RED) color.push(config.color[1]); + if (config.format == gl.RGBA) color.push(config.color[2], config.color[3]); + wtu.checkCanvasRect(gl, 0, 0, 1, 1, color, + "reading with the exact format/type", 1, + implementationPixel, + implementationType, implementationFormat); + } +} + +function renderbufferTest(config, isSupported) { + debug(""); + debug(`${config.name} renderbuffer: ` + + `${!isSupported || !config.color ? "NOT " : ""}supported`); + + const rbo = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.renderbufferStorage(gl.RENDERBUFFER, config.internalFormat, 1, 1); + if (!isSupported || !config.color) { + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "renderbuffer allocation failed"); + return; + } + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "renderbuffer allocation succeeded"); + + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo); + + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE); + + drawTest(config); +} + +function textureTest(config, isRenderable, isTexturable) { + debug(""); + debug(`${config.name} texture: ` + + `${!isRenderable || !config.color ? "NOT " : ""}renderable, ` + + `${!isTexturable ? "NOT " : ""}texturable`); + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texImage2D(gl.TEXTURE_2D, 0, config.internalFormat, 1, 1, 0, config.format, config.type, null); + if (!isTexturable) { + wtu.glErrorShouldBe(gl, + [gl.INVALID_ENUM, gl.INVALID_VALUE, gl.INVALID_OPERATION], + "texture allocation failed"); + return; + } + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "texture allocation succeeded"); + + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); + + if (!isRenderable || !config.color) { + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT); + return; + } + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE); + + drawTest(config); +} + +function formatTest(isSnormEnabled, isNorm16Enabled) { + const program = wtu.setupProgram(gl, [wtu.simpleVertexShader, + wtu.simpleColorFragmentShader]); + gl.useProgram(program); + gl.uniform4f(gl.getUniformLocation(program, "u_color"), -0.0625, -0.125, -0.25, -0.5); + + wtu.setupUnitQuad(gl); + + const configs8 = [ + {name: "R8_SNORM", format: gl.RED, type: gl.BYTE, internalFormat: gl.R8_SNORM, color: [-8, 0, 0, 127]}, + {name: "RG8_SNORM", format: gl.RG, type: gl.BYTE, internalFormat: gl.RG8_SNORM, color: [-8, -16, 0, 127]}, + {name: "RGB8_SNORM", format: gl.RGB, type: gl.BYTE, internalFormat: gl.RGB8_SNORM, color: null}, + {name: "RGBA8_SNORM", format: gl.RGBA, type: gl.BYTE, internalFormat: gl.RGBA8_SNORM, color: [-8, -16, -32, -64]} + ]; + + const configs16 = [ + {name: "R16_SNORM", format: gl.RED, type: gl.SHORT, internalFormat: 0x8F98 /* R16_SNORM_EXT */, color: [-2048, 0, 0, 32767]}, + {name: "RG16_SNORM", format: gl.RG, type: gl.SHORT, internalFormat: 0x8F99 /* RG16_SNORM_EXT */, color: [-2048, -4096, 0, 32767]}, + {name: "RGB16_SNORM", format: gl.RGB, type: gl.SHORT, internalFormat: 0x8F9A /* RGB16_SNORM_EXT */, color: null}, + {name: "RGBA16_SNORM", format: gl.RGBA, type: gl.SHORT, internalFormat: 0x8F9B /* RGBA16_SNORM_EXT */, color: [-2048, -4096, -8192, -16384]} + ]; + + for (const config of configs8) { + renderbufferTest(config, isSnormEnabled); + textureTest(config, isSnormEnabled, true); + } + + for (const config of configs16) { + renderbufferTest(config, isSnormEnabled && isNorm16Enabled); + textureTest(config, isSnormEnabled && isNorm16Enabled, isNorm16Enabled); + } +} + +function runTest() { + if (!gl) { + testFailed("context does not exist"); + return; + } + + testPassed("context exists"); + + debug(""); + debug("Testing signed normalized formats with EXT_render_snorm disabled"); + formatTest(false, false); + + ext = gl.getExtension("EXT_render_snorm"); + wtu.runExtensionSupportedTest(gl, "EXT_render_snorm", ext !== null); + + if (ext !== null) { + debug(""); + debug("Testing signed normalized formats with only EXT_render_snorm enabled"); + formatTest(true, false); + + if (gl.getExtension("EXT_texture_norm16")) { + debug(""); + debug("Testing signed normalized formats with EXT_render_snorm and EXT_texture_norm16 enabled"); + formatTest(true, true); + } + } else { + testPassed("No EXT_render_snorm support -- this is legal"); + } +} + +runTest(); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/nv-shader-noperspective-interpolation.html b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/nv-shader-noperspective-interpolation.html new file mode 100644 index 0000000000..2198c17b1a --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/nv-shader-noperspective-interpolation.html @@ -0,0 +1,251 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL NV_shader_noperspective_interpolation Conformance Tests</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<canvas width="128" height="128" id="c"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the NV_shader_noperspective_interpolation extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext("c", null, 2); +var ext; + +function runShaderTests(extensionEnabled) { + debug(""); + debug("Testing various shader compiles with extension " + (extensionEnabled ? "enabled" : "disabled")); + + const macroVertex = `#version 300 es + in vec4 vPosition; + void main() { + #ifdef GL_NV_shader_noperspective_interpolation + gl_Position = vPosition; + #else + #error no GL_NV_shader_noperspective_interpolation; + #endif + }`; + + const macroFragment = `#version 300 es + precision highp float; + out vec4 my_FragColor; + void main() { + #ifdef GL_NV_shader_noperspective_interpolation + my_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + #else + #error no GL_NV_shader_noperspective_interpolation; + #endif + }`; + + for (const shaders of [[wtu.simpleVertexShaderESSL300, macroFragment], + [macroVertex, wtu.simpleColorFragmentShaderESSL300]]) { + // Expect the macro shader to succeed ONLY if enabled + if (wtu.setupProgram(gl, shaders)) { + if (extensionEnabled) { + testPassed("Macro defined in shaders when extension is enabled"); + } else { + testFailed("Macro defined in shaders when extension is disabled"); + } + } else { + if (extensionEnabled) { + testFailed("Macro not defined in shaders when extension is enabled"); + } else { + testPassed("Macro not defined in shaders when extension is disabled"); + } + } + } + + const missingVertex = `#version 300 es + noperspective out float interpolant; + in vec4 vPosition; + void main() { + gl_Position = vPosition; + }`; + + const missingFragment = `#version 300 es + precision highp float; + noperspective in float interpolant; + out vec4 my_FragColor; + void main() { + my_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + }`; + + // Always expect the shader missing the #extension pragma to fail (whether enabled or not) + for (const shaders of [[missingVertex, wtu.simpleColorFragmentShaderESSL300], + [wtu.simpleVertexShaderESSL300, missingFragment], + [missingVertex, missingFragment]]) { + if (wtu.setupProgram(gl, shaders)) { + testFailed("Noperspective interpolation qualifier allowed without #extension pragma"); + } else { + testPassed("Noperspective interpolation qualifier disallowed without #extension pragma"); + } + } + + const validVertex = `#version 300 es + #extension GL_NV_shader_noperspective_interpolation : enable + noperspective out float interpolant; + in vec4 vPosition; + void main() { + gl_Position = vPosition; + }`; + + const validFragment = `#version 300 es + #extension GL_NV_shader_noperspective_interpolation : enable + precision highp float; + noperspective in float interpolant; + out vec4 my_FragColor; + void main() { + my_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + }`; + + // Try to compile a shader using a noperspective qualifier that should only succeed if enabled + if (wtu.setupProgram(gl, [validVertex, validFragment])) { + if (extensionEnabled) { + testPassed("Noperspective interpolation qualifier compiled successfully when extension enabled"); + } else { + testFailed("Noperspective interpolation qualifier compiled successfully when extension disabled"); + } + } else { + if (extensionEnabled) { + testFailed("Noperspective interpolation qualifier failed to compile when extension enabled"); + } else { + testPassed("Noperspective interpolation qualifier failed to compile when extension disabled"); + } + } + + debug(""); +} + +function runInterpolationTest() { + function draw(program, skew) { + gl.useProgram(program); + + const posLoc = gl.getAttribLocation(program, "position"); + const colLoc = gl.getAttribLocation(program, "color"); + + const buf = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([ + -1.0, -1.0, 0.0, 1.0, + +1.0, -1.0, 0.0, 1.0, + 0.0, +1.0 * skew, 0.0, skew, + + 1.0, 0.0, 0.0, 1.0, + 0.0, 1.0, 0.0, 1.0, + 0.0, 0.0, 1.0, 1.0]), + gl.STATIC_DRAW); + + gl.vertexAttribPointer(posLoc, 4, gl.FLOAT, false, 0, 0); + gl.vertexAttribPointer(colLoc, 4, gl.FLOAT, false, 0, 48); + gl.enableVertexAttribArray(posLoc); + gl.enableVertexAttribArray(colLoc); + gl.drawArrays(gl.TRIANGLES, 0, 3); + } + + const vertexSmooth = `#version 300 es + in vec4 position; + in vec4 color; + smooth out vec4 interp_color; + void main() { + gl_Position = position; + interp_color = color; + }`; + + const fragmentSmooth = `#version 300 es + precision highp float; + smooth in vec4 interp_color; + out vec4 fragColor; + void main() { + fragColor = interp_color; + }`; + const programSmooth = wtu.setupProgram(gl, [vertexSmooth, fragmentSmooth]); + + debug("Get non-skewed value with smooth interpolation"); + gl.clearColor(0.0, 0.0, 0.0, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT); + draw(programSmooth, 1.0); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + const smoothColor = new Uint8Array(4); + gl.readPixels(64, 64, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, smoothColor); + + const vertexNoperspective = `#version 300 es + #extension GL_NV_shader_noperspective_interpolation : require + in vec4 position; + in vec4 color; + noperspective out vec4 interp_color; + void main() { + gl_Position = position; + interp_color = color; + }`; + + const fragmentNoperspective = `#version 300 es + #extension GL_NV_shader_noperspective_interpolation : require + precision highp float; + noperspective in vec4 interp_color; + out vec4 fragColor; + void main() { + fragColor = interp_color; + }`; + const programNoperspective = wtu.setupProgram(gl, [vertexNoperspective, fragmentNoperspective]); + + debug(""); + debug("Check non-skewed value with noperspective interpolation"); + gl.clear(gl.COLOR_BUFFER_BIT); + draw(programNoperspective, 1.0); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + wtu.checkCanvasRect(gl, 64, 64, 1, 1, smoothColor, "Non-skewed noperspective should match smooth"); + + debug(""); + debug("Check skewed value with noperspective interpolation"); + gl.clear(gl.COLOR_BUFFER_BIT); + draw(programNoperspective, 2.0); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + wtu.checkCanvasRect(gl, 64, 64, 1, 1, smoothColor, "Skewed noperspective should match smooth"); +} + +function runTest() { + if (!gl) { + testFailed("WebGL context does not exist"); + return; + } + testPassed("WebGL context exists"); + + runShaderTests(false); + + ext = gl.getExtension("NV_shader_noperspective_interpolation"); + wtu.runExtensionSupportedTest(gl, "NV_shader_noperspective_interpolation", ext !== null); + + if (!ext) { + testPassed("No NV_shader_noperspective_interpolation support -- this is legal"); + } else { + testPassed("Successfully enabled NV_shader_noperspective_interpolation extension"); + runShaderTests(true); + runInterpolationTest(); + } +} + +runTest(); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/oes-sample-variables.html b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/oes-sample-variables.html new file mode 100644 index 0000000000..41fc8f8242 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/oes-sample-variables.html @@ -0,0 +1,474 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL OES_sample_variables Conformance Tests</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<canvas width="32" height="32" id="c"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the OES_sample_variables extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext("c", { antialias: false }, 2); +var ext; + +function runShaderTests(extensionEnabled) { + debug(""); + debug("Testing various shader compiles with extension " + (extensionEnabled ? "enabled" : "disabled")); + + const macro = `#version 300 es + precision highp float; + out vec4 my_FragColor; + void main() { + #ifdef GL_OES_sample_variables + my_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + #else + #error no GL_OES_sample_variables; + #endif + }`; + + // Expect the macro shader to succeed ONLY if enabled + if (wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, macro])) { + if (extensionEnabled) { + testPassed("Macro defined in shaders when extension is enabled"); + } else { + testFailed("Macro defined in shaders when extension is disabled"); + } + } else { + if (extensionEnabled) { + testFailed("Macro not defined in shaders when extension is enabled"); + } else { + testPassed("Macro not defined in shaders when extension is disabled"); + } + } + + const missing = `#version 300 es + precision highp float; + out vec4 my_FragColor; + void main() { + gl_SampleMask[0] = gl_SampleMaskIn[0] & 0x55555555; + my_FragColor = vec4(gl_SamplePosition.yx, float(gl_SampleID), float(gl_MaxSamples + gl_NumSamples)); + }`; + + // Always expect the shader missing the #extension pragma to fail (whether enabled or not) + if (wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, missing])) { + testFailed("Sample variables allowed without #extension pragma"); + } else { + testPassed("Sample variables disallowed without #extension pragma"); + } + + const valid = `#version 300 es + #extension GL_OES_sample_variables : enable + precision highp float; + out vec4 my_FragColor; + void main() { + gl_SampleMask[0] = gl_SampleMaskIn[0] & 0x55555555; + my_FragColor = vec4(gl_SamplePosition.yx, float(gl_SampleID), float(gl_MaxSamples + gl_NumSamples)); + }`; + + // Try to compile a shader using sample variables that should only succeed if enabled + if (wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, valid])) { + if (extensionEnabled) { + testPassed("Sample variables compiled successfully when extension enabled"); + } else { + testFailed("Sample variables compiled successfully when extension disabled"); + } + } else { + if (extensionEnabled) { + testFailed("Sample variables failed to compile when extension enabled"); + } else { + testPassed("Sample variables failed to compile when extension disabled"); + } + } + + debug(""); +} + +function runMaxSamplesTest() { + debug(""); + debug("Testing gl_MaxSamples"); + + const frag = `#version 300 es + #extension GL_OES_sample_variables : require + precision highp float; + out vec4 color; + void main() { + color = vec4(float(gl_MaxSamples * 4) / 255.0, 0.0, 0.0, 1.0); + }`; + gl.useProgram(wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, frag])); + + wtu.setupUnitQuad(gl); + wtu.drawUnitQuad(gl); + + wtu.checkCanvas(gl, [gl.getParameter(gl.MAX_SAMPLES) * 4, 0, 0, 255], "should match MAX_SAMPLES", 1); +} + +function runNumSamplesTest() { + debug(""); + debug("Testing gl_NumSamples"); + + const rbo = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.renderbufferStorageMultisample(gl.RENDERBUFFER, 4, gl.RGBA8, 32, 32); + + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo); + + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE); + + const frag = `#version 300 es + #extension GL_OES_sample_variables : require + precision highp float; + out vec4 color; + void main() { + if (gl_NumSamples == 4) { + color = vec4(0.0, 1.0, 0.0, 1.0); + } else { + color = vec4(1.0, 0.0, 0.0, 1.0); + } + }`; + gl.useProgram(wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, frag])); + + wtu.setupUnitQuad(gl); + wtu.drawUnitQuad(gl); + + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, fbo); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null); + gl.blitFramebuffer(0, 0, 32, 32, 0, 0, 32, 32, gl.COLOR_BUFFER_BIT, gl.NEAREST); + + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null); + wtu.checkCanvas(gl, [0, 255, 0, 255], "should be green"); +} + +function runSampleIDTest() { + debug(""); + debug("Testing gl_SampleID"); + + const frag = `#version 300 es + #extension GL_OES_sample_variables : require + precision highp float; + out vec4 color; + uniform int id; + void main() { + // Special value when the selected sample is processed, 0.0 otherwise + float r = float(gl_SampleID == id ? (1 << gl_SampleID) : 0) * 32.0 / 255.0; + // Must always be 0.0 + float g = float(gl_SampleID < 0 || gl_SampleID >= gl_NumSamples); + color = vec4(r, g, 0.0, 1.0); + }`; + const program = wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, frag]); + gl.useProgram(program); + + wtu.setupUnitQuad(gl); + + const rbo = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.renderbufferStorageMultisample(gl.RENDERBUFFER, 4, gl.RGBA8, 32, 32); + + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo); + + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE); + + for (let sample = 0; sample < 4; sample++) { + debug(`Sample ${sample} is selected`); + + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, fbo); + gl.uniform1i(gl.getUniformLocation(program, "id"), sample); + wtu.drawUnitQuad(gl); + + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, fbo); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null); + gl.blitFramebuffer(0, 0, 32, 32, 0, 0, 32, 32, gl.COLOR_BUFFER_BIT, gl.NEAREST); + + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null); + wtu.checkCanvas(gl, [(1 << sample) * 8, 0, 0, 255], undefined, 1); + } +} + +function runSampleMaskInTest() { + debug(""); + debug("Testing gl_SampleMaskIn"); + + const frag = `#version 300 es + #extension GL_OES_sample_variables : require + precision highp float; + out vec4 color; + uint popcount(uint v) { + uint c = 0u; + for (; v != 0u; v >>= 1) c += v & 1u; + return c; + } + void main() { + float r = float(popcount(uint(gl_SampleMaskIn[0]))); + color = vec4(r * 4.0 / 255.0, 0, 0, 1); + }`; + + const program = wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, frag]); + gl.useProgram(program); + + // Use a triangle instead of the WTU's quad + // to avoid artifacts along the diagonal + const vertices = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vertices); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + -1.0, 1.0, + 1.0, -1.0, + -1.0, -1.0]), gl.STATIC_DRAW); + gl.enableVertexAttribArray(0); + gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); + + function test(sampleCount, sampleCoverageEnabled, coverage) { + if (sampleCoverageEnabled) { + gl.enable(gl.SAMPLE_COVERAGE); + } else { + gl.disable(gl.SAMPLE_COVERAGE); + } + + gl.sampleCoverage(coverage, false); + + const rbo = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.renderbufferStorageMultisample(gl.RENDERBUFFER, sampleCount, gl.RGBA8, 32, 32); + + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo); + + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE); + + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, fbo); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.drawArrays(gl.TRIANGLES, 0, 3); + + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, fbo); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null); + gl.blitFramebuffer(0, 0, 32, 32, 0, 0, 32, 32, gl.COLOR_BUFFER_BIT, gl.NEAREST); + + // Shader scales up the number of input samples to increase precision in unorm8 space. + let expected = Math.max(sampleCount, 1) * 4; + + // Sample coverage must not affect single sampled buffers + if (sampleCoverageEnabled && sampleCount > 0) { + // The number of samples in gl_SampleMaskIn must be affected by the sample + // coverage GL state and then the resolved value must be scaled down again. + expected *= coverage * coverage; + } + + // Check only the red channel + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null); + const pixel = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel); + const message = `Expected: ${expected}, Actual: ${pixel[0]}, ` + + `Samples: ${sampleCount}, Sample Coverage: ${sampleCoverageEnabled}, Coverage: ${coverage}`; + if (Math.abs(pixel[0] - expected) > 2) { + testFailed(message); + } else { + testPassed(message); + } + } + + // Include all exposed sample counts and additionally test single-sampled rendering + const sampleCounts = [...gl.getInternalformatParameter(gl.RENDERBUFFER, gl.RGBA8, gl.SAMPLES), 0]; + + for (const sampleCount of sampleCounts) { + if (sampleCount > 32) { + // This test will not work with more than 32 samples. + continue; + } + + for (const sampleCoverageEnabled of [false, true]) { + for (const coverage of [0.0, 0.5, 1.0]) { + if (sampleCount == 1 && coverage != 0.0 && coverage != 1.0) { + continue; + } + test(sampleCount, sampleCoverageEnabled, coverage); + } + } + } +} + +function runSampleMaskInPerSampleTest() { + debug(""); + debug("Testing gl_SampleMaskIn with per-sample shading"); + + const frag = `#version 300 es + #extension GL_OES_sample_variables : require + precision highp float; + out vec4 color; + void main() { + float r = float(gl_SampleMaskIn[0] == (1 << gl_SampleID)); + color = vec4(r, 0, 0, 1); + }`; + const program = wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, frag]); + gl.useProgram(program); + + wtu.setupUnitQuad(gl); + + // Include all exposed sample counts and additionally test single-sampled rendering + const sampleCounts = [...gl.getInternalformatParameter(gl.RENDERBUFFER, gl.RGBA8, gl.SAMPLES), 0]; + for (const sampleCount of sampleCounts) { + if (sampleCount > 32) { + // This test will not work with more than 32 samples. + continue; + } + + const rbo = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.renderbufferStorageMultisample(gl.RENDERBUFFER, sampleCount, gl.RGBA8, 32, 32); + + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo); + + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE); + + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, fbo); + wtu.drawUnitQuad(gl); + + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, fbo); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null); + gl.blitFramebuffer(0, 0, 32, 32, 0, 0, 32, 32, gl.COLOR_BUFFER_BIT, gl.NEAREST); + + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null); + wtu.checkCanvas(gl, [255, 0, 0, 255], `Samples: ${sampleCount}`, 1); + } +} + +function runSampleMaskTest() { + debug(""); + debug("Testing gl_SampleMask"); + + const frag = `#version 300 es + #extension GL_OES_sample_variables : require + precision highp float; + uniform highp int sampleMask; + out vec4 color; + void main() { + gl_SampleMask[0] = sampleMask; + color = vec4(1, 0, 0, 1); + }`; + const program = wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, frag]); + gl.useProgram(program); + + // Use a triangle instead of the WTU's quad + // to avoid artifacts along the diagonal + const vertices = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vertices); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + -1.0, 1.0, + 1.0, -1.0, + -1.0, -1.0]), gl.STATIC_DRAW); + gl.enableVertexAttribArray(0); + gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); + + function test(sampleCount, sampleMask) { + const rbo = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.renderbufferStorageMultisample(gl.RENDERBUFFER, sampleCount, gl.RGBA8, 32, 32); + + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo); + + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE); + + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, fbo); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.uniform1i(gl.getUniformLocation(program, "sampleMask"), sampleMask); + gl.drawArrays(gl.TRIANGLES, 0, 3); + + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, fbo); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null); + gl.blitFramebuffer(0, 0, 32, 32, 0, 0, 32, 32, gl.COLOR_BUFFER_BIT, gl.NEAREST); + + let expected = 1.0; + if (sampleCount > 0) { + let mask = sampleMask & ((1 << Math.max(sampleCount, 1)) - 1); + let bits = 0; + for (; mask != 0; mask >>= 1) bits += mask & 1; + expected = bits / Math.max(sampleCount, 1); + } + expected *= 255; + + // Check only the red channel + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null); + const pixel = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel); + const message = `Samples: ${sampleCount}, ` + + `gl_SampleMask[0]: 0x${sampleMask.toString(16).padStart(8, "0").toUpperCase()}, ` + + `Actual: ${pixel[0]}, Expected: ${expected}`; + if (Math.abs(pixel[0] - expected) > 2) { + testFailed(message); + } else { + testPassed(message); + } + } + + // Include all exposed sample counts and additionally test single-sampled rendering + const sampleCounts = [...gl.getInternalformatParameter(gl.RENDERBUFFER, gl.RGBA8, gl.SAMPLES), 0]; + + for (const sampleCount of sampleCounts) { + if (sampleCount > 31) { + // This test will not work with more than 31 samples. + continue; + } + + for (const sampleMask of [0xFFFFFFFF, 0x55555555, 0xAAAAAAAA, 0x00000000]) { + test(sampleCount, sampleMask); + } + } +} + +function runTest() { + if (!gl) { + testFailed("WebGL context does not exist"); + return; + } + testPassed("WebGL context exists"); + + runShaderTests(false); + + ext = gl.getExtension("OES_sample_variables"); + wtu.runExtensionSupportedTest(gl, "OES_sample_variables", ext !== null); + + if (!ext) { + testPassed("No OES_sample_variables support -- this is legal"); + } else { + testPassed("Successfully enabled OES_sample_variables extension"); + runShaderTests(true); + + debug("Testing sample variables"); + runMaxSamplesTest(); + runNumSamplesTest(); + runSampleIDTest(); + runSampleMaskInTest(); + runSampleMaskInPerSampleTest(); + runSampleMaskTest(); + } +} + +runTest(); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/oes-shader-multisample-interpolation.html b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/oes-shader-multisample-interpolation.html new file mode 100644 index 0000000000..dcb272c2f0 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/oes-shader-multisample-interpolation.html @@ -0,0 +1,313 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL OES_shader_multisample_interpolation Conformance Tests</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<canvas width="32" height="32" id="c"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the OES_shader_multisample_interpolation extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext("c", { antialias: false }, 2); +var ext; + +function runShaderTests(extensionEnabled) { + debug(""); + debug("Testing various shader compiles with extension " + (extensionEnabled ? "enabled" : "disabled")); + + const macroVertex = `#version 300 es + in vec4 vPosition; + void main() { + #ifdef GL_OES_shader_multisample_interpolation + gl_Position = vPosition; + #else + #error no GL_OES_shader_multisample_interpolation; + #endif + }`; + + const macroFragment = `#version 300 es + precision highp float; + out vec4 my_FragColor; + void main() { + #ifdef GL_OES_shader_multisample_interpolation + my_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + #else + #error no GL_OES_shader_multisample_interpolation; + #endif + }`; + + for (const shaders of [[wtu.simpleVertexShaderESSL300, macroFragment], + [macroVertex, wtu.simpleColorFragmentShaderESSL300]]) { + // Expect the macro shader to succeed ONLY if enabled + if (wtu.setupProgram(gl, shaders)) { + if (extensionEnabled) { + testPassed("Macro defined in shaders when extension is enabled"); + } else { + testFailed("Macro defined in shaders when extension is disabled"); + } + } else { + if (extensionEnabled) { + testFailed("Macro not defined in shaders when extension is enabled"); + } else { + testPassed("Macro not defined in shaders when extension is disabled"); + } + } + } + + const missingVertex = `#version 300 es + sample out float interpolant; + in vec4 vPosition; + void main() { + gl_Position = vPosition; + }`; + + const missingFragment = `#version 300 es + precision highp float; + sample in float interpolant; + out vec4 my_FragColor; + void main() { + my_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + }`; + + // Always expect the shader missing the #extension pragma to fail (whether enabled or not) + for (const shaders of [[missingVertex, wtu.simpleColorFragmentShaderESSL300], + [wtu.simpleVertexShaderESSL300, missingFragment], + [missingVertex, missingFragment]]) { + if (wtu.setupProgram(gl, shaders)) { + testFailed("Sample interpolation qualifier allowed without #extension pragma"); + } else { + testPassed("Sample interpolation qualifier disallowed without #extension pragma"); + } + } + + const validVertex = `#version 300 es + #extension GL_OES_shader_multisample_interpolation : enable + sample out float interpolant; + in vec4 vPosition; + void main() { + gl_Position = vPosition; + }`; + + const validFragment = `#version 300 es + #extension GL_OES_shader_multisample_interpolation : enable + precision highp float; + sample in float interpolant; + out vec4 my_FragColor; + void main() { + my_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + }`; + + // Try to compile a shader using a sample qualifier that should only succeed if enabled + if (wtu.setupProgram(gl, [validVertex, validFragment])) { + if (extensionEnabled) { + testPassed("Sample interpolation qualifier compiled successfully when extension enabled"); + } else { + testFailed("Sample interpolation qualifier compiled successfully when extension disabled"); + } + } else { + if (extensionEnabled) { + testFailed("Sample interpolation qualifier failed to compile when extension enabled"); + } else { + testPassed("Sample interpolation qualifier failed to compile when extension disabled"); + } + } +} + +function runQueryTests(extensionEnabled) { + debug(""); + debug("Testing parameters with extension " + (extensionEnabled ? "enabled" : "disabled")); + if (extensionEnabled) { + shouldBeGreaterThanOrEqual("gl.getParameter(ext.FRAGMENT_INTERPOLATION_OFFSET_BITS_OES)", "4"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + const limit = 0.5 - Math.pow(2, -gl.getParameter(ext.FRAGMENT_INTERPOLATION_OFFSET_BITS_OES)); + shouldBeLessThanOrEqual("gl.getParameter(ext.MIN_FRAGMENT_INTERPOLATION_OFFSET_OES)", `-${limit}`); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + shouldBeGreaterThanOrEqual("gl.getParameter(ext.MAX_FRAGMENT_INTERPOLATION_OFFSET_OES)", `${limit}`); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + } else { + shouldBeNull("gl.getParameter(0x8E5B /* MIN_FRAGMENT_INTERPOLATION_OFFSET_OES */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + shouldBeNull("gl.getParameter(0x8E5C /* MAX_FRAGMENT_INTERPOLATION_OFFSET_OES */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + shouldBeNull("gl.getParameter(0x8E5D /* FRAGMENT_INTERPOLATION_OFFSET_BITS_OES */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + } +} + +function checkEnums() { + debug(""); + debug("Check enums"); + shouldBe("ext.MIN_FRAGMENT_INTERPOLATION_OFFSET_OES", "0x8E5B"); + shouldBe("ext.MAX_FRAGMENT_INTERPOLATION_OFFSET_OES", "0x8E5C"); + shouldBe("ext.FRAGMENT_INTERPOLATION_OFFSET_BITS_OES", "0x8E5D"); +} + +/* + * This test renders a triangle using MSAAx4 and 1x1 viewport + * with the following vertex colors. + * + * | Position | Color | + * |==========|===========| + * | (-1, -1) | (0, 0, 0) | + * | (-1, +1) | (0, 1, 0) | + * | (+1, -1) | (1, 0, 0) | + * + * This triangle cannot cover all four samples. + * + * When default interpolation is used, the vertex color is interpolated + * once, most likely in the pixel center. + * + * When per-sample interpolation is used, the vertex color is interpolated + * several times, producing a distinct value for each covered sample. + * Due to the asymmetry of sample positions, the resolved pixel color must + * not match the color produced by default interpolation. + * + * OpenGL specs do not guarantee specific sample positions, so the test + * checks only that the resolved colors are different. + */ +function runInterpolationTest() { + debug(""); + debug("Testing multisample interpolation"); + + function draw(program) { + gl.viewport(0, 0, 1, 1); + gl.useProgram(program); + + const posLoc = gl.getAttribLocation(program, "position"); + const buf = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([ + -1.0, -1.0, + -1.0, +1.0, + +1.0, -1.0]), + gl.STATIC_DRAW); + + gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0); + gl.enableVertexAttribArray(posLoc); + + const rbo = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.renderbufferStorageMultisample(gl.RENDERBUFFER, 4, gl.RGBA8, 1, 1); + + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo); + + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE); + + gl.clear(gl.COLOR_BUFFER_BIT); + gl.drawArrays(gl.TRIANGLES, 0, 3); + + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, fbo); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null); + gl.blitFramebuffer(0, 0, 1, 1, 0, 0, 1, 1, gl.COLOR_BUFFER_BIT, gl.NEAREST); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null); + } + + const vertexCenter = `#version 300 es + in vec4 position; + out vec4 interp_color; + void main() { + gl_Position = position; + interp_color = vec4(position.xy * 0.5 + 0.5, 0.0, 1.0); + }`; + + const fragmentCenter = `#version 300 es + precision highp float; + in vec4 interp_color; + out vec4 fragColor; + void main() { + fragColor = interp_color; + }`; + const programCenter = wtu.setupProgram(gl, [vertexCenter, fragmentCenter]); + + draw(programCenter); + const centerColor = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, centerColor); + + const vertexSample = `#version 300 es + #extension GL_OES_shader_multisample_interpolation : require + in vec4 position; + sample out vec4 interp_color; + void main() { + gl_Position = position; + interp_color = vec4(position.xy * 0.5 + 0.5, 0.0, 1.0); + }`; + + const fragmentSample = `#version 300 es + #extension GL_OES_shader_multisample_interpolation : require + precision highp float; + sample in vec4 interp_color; + out vec4 fragColor; + void main() { + fragColor = interp_color; + }`; + const programSample = wtu.setupProgram(gl, [vertexSample, fragmentSample]); + + draw(programSample); + const sampleColor = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, sampleColor); + + const message = `Pixel-center value: ${centerColor}, sample-average value: ${sampleColor}`; + if (centerColor[0] == sampleColor[0] && centerColor[1] == sampleColor[1]) { + testFailed(message); + } else { + testPassed(message); + } +} + +function runTest() { + if (!gl) { + testFailed("WebGL context does not exist"); + return; + } + testPassed("WebGL context exists"); + + runQueryTests(false); + runShaderTests(false); + + debug(""); + ext = gl.getExtension("OES_shader_multisample_interpolation"); + wtu.runExtensionSupportedTest(gl, "OES_shader_multisample_interpolation", ext !== null); + + if (!ext) { + testPassed("No OES_shader_multisample_interpolation support -- this is legal"); + } else { + testPassed("Successfully enabled OES_shader_multisample_interpolation extension"); + runQueryTests(true); + runShaderTests(true); + runInterpolationTest(); + } +} + +runTest(); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-blend-func-extended.html b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-blend-func-extended.html new file mode 100644 index 0000000000..792e9aafa7 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-blend-func-extended.html @@ -0,0 +1,26 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL 2.0 WEBGL_blend_func_extended Conformance Tests</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<canvas width="32" height="32" id="c"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +var contextVersion = 2; +</script> +<script src="../../js/tests/webgl-blend-func-extended.js"></script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-clip-cull-distance.html b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-clip-cull-distance.html new file mode 100644 index 0000000000..cb2253c326 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-clip-cull-distance.html @@ -0,0 +1,475 @@ +<!-- +Copyright (c) 2022 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL WEBGL_clip_cull_distance Conformance Tests</title> +<LINK rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<canvas width="32" height="32" id="c"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the WEBGL_clip_cull_distance extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext("c", null, 2); +var ext; +const w = gl.drawingBufferWidth; +const h = gl.drawingBufferHeight; + +function runTestNoExtension() { + debug(""); + debug("Check parameters and capabilities without the extension"); + + shouldBeNull("gl.getParameter(0x0D32 /* MAX_CLIP_DISTANCES_WEBGL */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + shouldBeNull("gl.getParameter(0x82F9 /* MAX_CULL_DISTANCES_WEBGL */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + shouldBeNull("gl.getParameter(0x82FA /* MAX_COMBINED_CLIP_AND_CULL_DISTANCES_WEBGL */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + const assertState = (i) => { + shouldBeFalse(`gl.isEnabled(${0x3000 + i} /* CLIP_DISTANCE${i}_WEBGL */)`); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + + shouldBeNull(`gl.getParameter(${0x3000 + i} /* CLIP_DISTANCE${i}_WEBGL */)`); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + }; + + for (let i = 0; i < 8; i++) { + assertState(i); + + gl.enable(0x3000 + i /* CLIP_DISTANCEi_WEBGL */); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "capability unknown without enabling the extension"); + + assertState(i); + + gl.disable(0x3000 + i /* CLIP_DISTANCEi_WEBGL */); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "capability unknown without enabling the extension"); + + assertState(i); + } + + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + debug(""); +} + +function checkEnums() { + debug(""); + debug("Check enums"); + shouldBe("ext.MAX_CLIP_DISTANCES_WEBGL", "0x0D32"); + shouldBe("ext.MAX_CULL_DISTANCES_WEBGL", "0x82F9"); + shouldBe("ext.MAX_COMBINED_CLIP_AND_CULL_DISTANCES_WEBGL", "0x82FA"); + shouldBe("ext.CLIP_DISTANCE0_WEBGL", "0x3000"); + shouldBe("ext.CLIP_DISTANCE1_WEBGL", "0x3001"); + shouldBe("ext.CLIP_DISTANCE2_WEBGL", "0x3002"); + shouldBe("ext.CLIP_DISTANCE3_WEBGL", "0x3003"); + shouldBe("ext.CLIP_DISTANCE4_WEBGL", "0x3004"); + shouldBe("ext.CLIP_DISTANCE5_WEBGL", "0x3005"); + shouldBe("ext.CLIP_DISTANCE6_WEBGL", "0x3006"); + shouldBe("ext.CLIP_DISTANCE7_WEBGL", "0x3007"); +} + +function checkQueries() { + debug(""); + debug("Check parameters"); + shouldBeGreaterThanOrEqual('gl.getParameter(ext.MAX_CLIP_DISTANCES_WEBGL)', '8'); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + const maxCullDistances = gl.getParameter(ext.MAX_CULL_DISTANCES_WEBGL); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + if (maxCullDistances == 0) { + testPassed("No cull distance support"); + shouldBe("gl.getParameter(ext.MAX_COMBINED_CLIP_AND_CULL_DISTANCES_WEBGL)", "0"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + } else if (maxCullDistances >= 8) { + testPassed("Optional cull distance support"); + shouldBeGreaterThanOrEqual("gl.getParameter(ext.MAX_COMBINED_CLIP_AND_CULL_DISTANCES_WEBGL)", "8"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + } else { + testFailed("Invalid number of supported cull distances"); + } + + debug(""); + debug("Check clip distance capabilities"); + + const assertState = (i, s) => { + shouldBe(`gl.isEnabled(${0x3000 + i} /* CLIP_DISTANCE${i}_WEBGL */)`, s ? "true" : "false"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + shouldBe(`gl.getParameter(${0x3000 + i} /* CLIP_DISTANCE${i}_WEBGL */)`, s ? "true" : "false"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + }; + + for (let i = 0; i < 8; i++) { + assertState(i, false); + + gl.enable(ext.CLIP_DISTANCE0_WEBGL + i); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + assertState(i, true); + + gl.disable(ext.CLIP_DISTANCE0_WEBGL + i); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + assertState(i, false); + } +} + +function checkClipDistance() { + debug(""); + debug("Check clip distance operation"); + + const vs = `#version 300 es +#extension GL_ANGLE_clip_cull_distance : require + +uniform vec4 u_plane; +in vec2 a_position; +void main() +{ + gl_Position = vec4(a_position, 0.0, 1.0); + gl_ClipDistance[0] = dot(gl_Position, u_plane); +}`; + + const program = wtu.setupProgram(gl, [vs, wtu.simpleColorFragmentShaderESSL300]); + gl.useProgram(program); + gl.uniform4fv(gl.getUniformLocation(program, 'u_color'), [1.0, 0.0, 0.0, 1.0]); + + gl.enable(ext.CLIP_DISTANCE0_WEBGL); + + // Clear to blue + gl.clearColor(0, 0, 1, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + + wtu.setupUnitQuad(gl); + + // Draw full screen quad with color red + gl.uniform4f(gl.getUniformLocation(program, "u_plane"), 1, 0, 0, 0.5); + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + // All pixels on the left of the plane x = -0.5 must be blue + let x = 0; + let y = 0; + let width = w / 4 - 1; + let height = h; + wtu.checkCanvasRect(gl, x, y, width, height, + [0, 0, 255, 255], "should be blue"); + + // All pixels on the right of the plane x = -0.5 must be red + x = w / 4 + 2; + y = 0; + width = w - x; + height = h; + wtu.checkCanvasRect(gl, x, y, width, height, + [255, 0, 0, 255], "should be red"); + + // Clear to green + gl.clearColor(0, 1, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + + // Draw full screen quad with color red + gl.uniform4f(gl.getUniformLocation(program, "u_plane"), -1, 0, 0, -0.5); + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + // All pixels on the left of the plane x = -0.5 must be red + x = 0; + y = 0; + width = w / 4 - 1; + height = h; + wtu.checkCanvasRect(gl, x, y, width, height, + [255, 0, 0, 255], "should be red"); + + // All pixels on the right of the plane x = -0.5 must be green + x = w / 4 + 2; + y = 0; + width = w - x; + height = h; + wtu.checkCanvasRect(gl, x, y, width, height, + [0, 255, 0, 255], "should be green"); + + // Disable CLIP_DISTANCE0 and draw again + gl.disable(ext.CLIP_DISTANCE0_WEBGL); + wtu.drawUnitQuad(gl); + + // All pixels must be red + wtu.checkCanvas(gl, [255, 0, 0, 255], "should be red"); +} + +function checkClipDistanceInterpolation() { + debug(""); + debug("Check clip distance interpolation"); + + const vs = `#version 300 es +#extension GL_ANGLE_clip_cull_distance : require +in vec2 a_position; +void main() +{ + gl_Position = vec4(a_position, 0.0, 1.0); + gl_ClipDistance[0] = dot(gl_Position, vec4( 1, 0, 0, 0.5)); + gl_ClipDistance[1] = dot(gl_Position, vec4(-1, 0, 0, 0.5)); + gl_ClipDistance[2] = dot(gl_Position, vec4( 0, 1, 0, 0.5)); + gl_ClipDistance[3] = dot(gl_Position, vec4( 0, -1, 0, 0.5)); + gl_ClipDistance[4] = gl_ClipDistance[0]; + gl_ClipDistance[5] = gl_ClipDistance[1]; + gl_ClipDistance[6] = gl_ClipDistance[2]; + gl_ClipDistance[7] = gl_ClipDistance[3]; +}`; + + const fs = `#version 300 es +#extension GL_ANGLE_clip_cull_distance : require +precision highp float; +out vec4 my_FragColor; +void main() +{ + float r = gl_ClipDistance[0] + gl_ClipDistance[1]; + float g = gl_ClipDistance[2] + gl_ClipDistance[3]; + float b = gl_ClipDistance[4] + gl_ClipDistance[5]; + float a = gl_ClipDistance[6] + gl_ClipDistance[7]; + my_FragColor = vec4(r, g, b, a) * 0.5; +}`; + + const program = wtu.setupProgram(gl, [vs, fs]); + gl.useProgram(program); + + gl.enable(ext.CLIP_DISTANCE0_WEBGL); + gl.enable(ext.CLIP_DISTANCE1_WEBGL); + gl.enable(ext.CLIP_DISTANCE2_WEBGL); + gl.enable(ext.CLIP_DISTANCE3_WEBGL); + gl.enable(ext.CLIP_DISTANCE4_WEBGL); + gl.enable(ext.CLIP_DISTANCE5_WEBGL); + gl.enable(ext.CLIP_DISTANCE6_WEBGL); + gl.enable(ext.CLIP_DISTANCE7_WEBGL); + + wtu.setupUnitQuad(gl); + + // Clear to blue + gl.clearColor(0, 0, 1, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + + // Draw full screen quad with color gray + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + const data = new Uint8Array(w * h * 4); + gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, data); + let passed = true; + for (let x = 0; x < w; x++) { + for (let y = 0; y < h; y++) { + const currentPosition = (y * h + x) * 4; + const inside = (x >= w / 4 && x < w * 3 / 4 && y >= h / 4 && y < h * 3 / 4); + const expected = inside ? [127, 127, 127, 127] : [0, 0, 255, 255]; + const actual = data.slice(currentPosition, currentPosition + 4); + if (Math.abs(actual[0] - expected[0]) > 1 || + Math.abs(actual[1] - expected[1]) > 1 || + Math.abs(actual[2] - expected[2]) > 1 || + Math.abs(actual[3] - expected[3]) > 1) { + passed = false; + } + } + } + if (passed) { + testPassed("Correct clip distance interpolation"); + } else { + testFailed("Incorrect clip distance interpolation"); + } +} + +function checkCullDistance() { + debug(""); + debug("Check cull distance operation"); + + if (gl.getParameter(ext.MAX_CULL_DISTANCES_WEBGL) == 0) { + testPassed("No cull distance support"); + return; + } + + const vs = `#version 300 es +#extension GL_ANGLE_clip_cull_distance : require + +uniform vec4 u_plane; +in vec2 a_position; +void main() +{ + gl_Position = vec4(a_position, 0.0, 1.0); + gl_CullDistance[0] = dot(gl_Position, u_plane); +}`; + + const program = wtu.setupProgram(gl, [vs, wtu.simpleColorFragmentShaderESSL300]); + gl.useProgram(program); + gl.uniform4fv(gl.getUniformLocation(program, 'u_color'), [1.0, 0.0, 0.0, 1.0]); + + // Clear to blue + gl.clearColor(0, 0, 1, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + + wtu.setupUnitQuad(gl); + + // Draw full screen quad with color red + gl.uniform4f(gl.getUniformLocation(program, "u_plane"), 1, 0, 0, 0.5); + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + // All pixels must be red + wtu.checkCanvas(gl, [255, 0, 0, 255], "should be red"); + + // Clear to green + gl.clearColor(0, 1, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + + // Draw full screen quad with color red + gl.uniform4f(gl.getUniformLocation(program, "u_plane"), -1, 1, 0, -0.5); + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + // All pixels above the y > x line must be red + const data = new Uint8Array(w * h * 4); + gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, data); + let passed = true; + for (let x = 0; x < w; ++x) { + for (let y = 0; y < h; ++y) { + if (y <= x + 2 && y >= x - 2) continue; // skip the edge + const currentPosition = (y * h + x) * 4; + const actual = data.slice(currentPosition, currentPosition + 2); + const expected = (y > x) ? [255, 0] : [0, 255]; + if (actual[0] != expected[0] || actual[1] != expected[1]) { + passed = false; + } + } + } + if (passed) { + testPassed("Correct cull distance operation"); + } else { + testFailed("Incorrect cull distance operation"); + } +} + +function checkCullDistanceInterpolation() { + debug(""); + debug("Check cull distance interpolation"); + + if (gl.getParameter(ext.MAX_CULL_DISTANCES_WEBGL) == 0) { + testPassed("No cull distance support"); + return; + } + + const vs = `#version 300 es +#extension GL_ANGLE_clip_cull_distance : require +in vec2 a_position; +void main() +{ + gl_Position = vec4(a_position, 0.0, 1.0); + gl_CullDistance[0] = dot(gl_Position, vec4( 1, 0, 0, 1)); + gl_CullDistance[1] = dot(gl_Position, vec4(-1, 0, 0, 1)); + gl_CullDistance[2] = dot(gl_Position, vec4( 0, 1, 0, 1)); + gl_CullDistance[3] = dot(gl_Position, vec4( 0, -1, 0, 1)); + gl_CullDistance[4] = gl_CullDistance[0]; + gl_CullDistance[5] = gl_CullDistance[1]; + gl_CullDistance[6] = gl_CullDistance[2]; + gl_CullDistance[7] = gl_CullDistance[3]; +}`; + + const fs = `#version 300 es +#extension GL_ANGLE_clip_cull_distance : require +precision highp float; +out vec4 my_FragColor; +void main() +{ + float r = gl_CullDistance[0] + gl_CullDistance[1]; + float g = gl_CullDistance[2] + gl_CullDistance[3]; + float b = gl_CullDistance[4] + gl_CullDistance[5]; + float a = gl_CullDistance[6] + gl_CullDistance[7]; + my_FragColor = vec4(r, g, b, a) * 0.25; +}`; + + const program = wtu.setupProgram(gl, [vs, fs]); + gl.useProgram(program); + + // Clear to blue + gl.clearColor(0, 0, 1, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + + wtu.setupQuad(gl, {scale: 0.5}); + + // Draw a small quad with color gray + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + const data = new Uint8Array(w * h * 4); + gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, data); + let passed = true; + for (let x = 0; x < w; x++) { + for (let y = 0; y < h; y++) { + const currentPosition = (y * h + x) * 4; + const inside = (x >= w / 4 && x < w * 3 / 4 && y >= h / 4 && y < h * 3 / 4); + const expected = inside ? [127, 127, 127, 127] : [0, 0, 255, 255]; + const actual = data.slice(currentPosition, currentPosition + 4); + if (Math.abs(actual[0] - expected[0]) > 1 || + Math.abs(actual[1] - expected[1]) > 1 || + Math.abs(actual[2] - expected[2]) > 1 || + Math.abs(actual[3] - expected[3]) > 1) { + passed = false; + } + } + } + if (passed) { + testPassed("Correct cull distance interpolation"); + } else { + testFailed("Incorrect cull distance interpolation"); + } +} + +function runTestExtension() { + checkEnums(); + checkQueries(); + + checkClipDistance(); + checkClipDistanceInterpolation(); + + checkCullDistance(); + checkCullDistanceInterpolation(); +} + +function runTest() { + if (!gl) { + testFailed("context does not exist"); + } else { + testPassed("context exists"); + + runTestNoExtension(); + + ext = gl.getExtension("WEBGL_clip_cull_distance"); + + wtu.runExtensionSupportedTest(gl, "WEBGL_clip_cull_distance", ext !== null); + + if (ext !== null) { + runTestExtension(); + } else { + testPassed("No WEBGL_clip_cull_distance support -- this is legal"); + } + } +} + +runTest(); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-provoking-vertex.html b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-provoking-vertex.html new file mode 100644 index 0000000000..3737409b3a --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-provoking-vertex.html @@ -0,0 +1,165 @@ +<!-- +Copyright (c) 2022 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL WEBGL_provoking_vertex Conformance Tests</title> +<LINK rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<canvas width="16" height="16" id="c"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the WEBGL_provoking_vertex extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext("c", null, 2); +var ext; + +function runTestNoExtension() { + debug(""); + debug("Check getParameter without the extension"); + shouldBeNull("gl.getParameter(0x8E4F /* PROVOKING_VERTEX_WEBGL */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + debug(""); +} + +function runTestExtension() { + debug(""); + debug("Check enums"); + shouldBe("ext.FIRST_VERTEX_CONVENTION_WEBGL", "0x8E4D"); + shouldBe("ext.LAST_VERTEX_CONVENTION_WEBGL", "0x8E4E"); + shouldBe("ext.PROVOKING_VERTEX_WEBGL", "0x8E4F"); + + debug(""); + debug("Check default state"); + shouldBe("gl.getParameter(ext.PROVOKING_VERTEX_WEBGL)", "ext.LAST_VERTEX_CONVENTION_WEBGL"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "parameter known with the extension enabled"); + + debug(""); + debug("Check state updates"); + ext.provokingVertexWEBGL(ext.FIRST_VERTEX_CONVENTION_WEBGL); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "provokingVertexWEBGL(ext.FIRST_VERTEX_CONVENTION_WEBGL) generates no errors"); + shouldBe("gl.getParameter(ext.PROVOKING_VERTEX_WEBGL)", "ext.FIRST_VERTEX_CONVENTION_WEBGL"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR); + ext.provokingVertexWEBGL(ext.LAST_VERTEX_CONVENTION_WEBGL); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "provokingVertexWEBGL(ext.LAST_VERTEX_CONVENTION_WEBGL) generates no errors"); + shouldBe("gl.getParameter(ext.PROVOKING_VERTEX_WEBGL)", "ext.LAST_VERTEX_CONVENTION_WEBGL"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR); + + debug(""); + debug("Check invalid provoking vertex mode"); + ext.provokingVertexWEBGL(ext.FIRST_VERTEX_CONVENTION_WEBGL); + ext.provokingVertexWEBGL(ext.PROVOKING_VERTEX_WEBGL); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "invalid provoking mode generates an error"); + shouldBe("gl.getParameter(ext.PROVOKING_VERTEX_WEBGL)", "ext.FIRST_VERTEX_CONVENTION_WEBGL"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR); + + debug(""); + debug("Check provoking vertex operation"); + + const vs = `#version 300 es + in int intAttrib; + in vec2 position; + flat out int attrib; + void main() { + gl_Position = vec4(position, 0, 1); + attrib = intAttrib; + }`; + + const fs = `#version 300 es + flat in int attrib; + out int fragColor; + void main() { + fragColor = attrib; + }`; + + const program = wtu.setupProgram(gl, [vs, fs]); + gl.useProgram(program); + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texStorage2D(gl.TEXTURE_2D, 1, gl.R32I, 16, 16); + + const fb = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fb); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); + + const vb = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vb); + const buf = new ArrayBuffer(36); + new Float32Array(buf, 0, 6).set([-1.0, -1.0, 3.0, -1.0, -1.0, 3.0]); + new Int32Array(buf, 24, 3).set([1, 2, 3]); + gl.bufferData(gl.ARRAY_BUFFER, buf, gl.STATIC_DRAW); + + const positionLocation = gl.getAttribLocation(program, "position"); + gl.enableVertexAttribArray(positionLocation); + gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); + + const intAttribLocation = gl.getAttribLocation(program, "intAttrib"); + gl.enableVertexAttribArray(intAttribLocation); + gl.vertexAttribIPointer(intAttribLocation, 1, gl.INT, 0, 24); + + const pixel = new Int32Array(4); + + ext.provokingVertexWEBGL(ext.LAST_VERTEX_CONVENTION_WEBGL); + gl.clearBufferiv(gl.COLOR, 0, new Int32Array(4)); + gl.drawArrays(gl.TRIANGLES, 0, 3); + gl.readPixels(0, 0, 1, 1, gl.RGBA_INTEGER, gl.INT, pixel); + + if (pixel[0] == 3) { + testPassed("Correct last provoking vertex"); + } else { + testFailed("Incorrect last provoking vertex"); + } + + ext.provokingVertexWEBGL(ext.FIRST_VERTEX_CONVENTION_WEBGL); + gl.clearBufferiv(gl.COLOR, 0, new Int32Array(4)); + gl.drawArrays(gl.TRIANGLES, 0, 3); + gl.readPixels(0, 0, 1, 1, gl.RGBA_INTEGER, gl.INT, pixel); + + if (pixel[0] == 1) { + testPassed("Correct first provoking vertex"); + } else { + testFailed("Incorrect first provoking vertex"); + } +} + +function runTest() { + if (!gl) { + testFailed("context does not exist"); + } else { + testPassed("context exists"); + + runTestNoExtension(); + + ext = gl.getExtension("WEBGL_provoking_vertex"); + + wtu.runExtensionSupportedTest(gl, "WEBGL_provoking_vertex", ext !== null); + + if (ext !== null) { + runTestExtension(); + } else { + testPassed("No WEBGL_provoking_vertex support -- this is legal"); + } + } +} + +runTest(); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-render-shared-exponent.html b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-render-shared-exponent.html new file mode 100644 index 0000000000..11d505fcc6 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-render-shared-exponent.html @@ -0,0 +1,251 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL WEBGL_render_shared_exponent Conformance Tests</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the WEBGL_render_shared_exponent extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext(null, null, 2); +var ext; +const color = [64.0, 32.0, 16.0, 1.0]; + +function drawTest() { + wtu.clearAndDrawUnitQuad(gl); + + wtu.checkCanvasRect(gl, 0, 0, 1, 1, color, + "reading with the RGBA format and FLOAT type", 1, + new Float32Array(4), gl.FLOAT, gl.RGBA); + + const implementationType = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_TYPE); + const implementationFormat = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_FORMAT); + if (implementationFormat == gl.RGB && implementationType == gl.UNSIGNED_INT_5_9_9_9_REV) { + // Shared exponent value may be implementation + // specific, so compare decoded values. + const value = new Uint32Array(1); + gl.readPixels(0, 0, 1, 1, gl.RGB, gl.UNSIGNED_INT_5_9_9_9_REV, value); + wtu.glErrorShouldBe(gl, gl.NO_ERROR); + + let r = (value >> 0) & 0x1FF; + let g = (value >> 9) & 0x1FF; + let b = (value >> 18) & 0x1FF; + let e = (value >> 27) & 0x01F; + debug(`Raw value: 0x${value[0].toString(16).toUpperCase()}, ` + + `Raw components: R = ${r}, G = ${g}, B = ${b}, E = ${e}`); + + e = Math.pow(2, e - 24); + r *= e; + g *= e; + b *= e; + debug(`Decoded color: (${r}, ${g}, ${b})`); + + if (r == color[0] && g == color[1] && b == color[2]) { + testPassed("reading with the exact format/type"); + } else { + testFailed("reading with the exact format/type"); + } + } +} + +function renderbufferTest(isSupported) { + debug(""); + debug(`RGB9_E5 renderbuffer: ` + + `${!isSupported ? "NOT " : ""}supported`); + + const rbo = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGB9_E5, 1, 1); + if (!isSupported) { + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "renderbuffer allocation failed"); + return; + } + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "renderbuffer allocation succeeded"); + + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo); + + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE); + + drawTest(); +} + +function textureTest(isRenderable) { + debug(""); + debug(`RGB9_E5 texture: ` + + `${!isRenderable ? "NOT " : ""}renderable`); + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB9_E5, 1, 1, 0, gl.RGB, gl.UNSIGNED_INT_5_9_9_9_REV, null); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "texture allocation succeeded"); + + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); + + if (!isRenderable) { + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT); + return; + } + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE); + + drawTest(); +} + +function formatTest(isEnabled) { + const program = wtu.setupProgram(gl, [wtu.simpleVertexShader, + wtu.simpleColorFragmentShader]); + gl.useProgram(program); + gl.uniform4fv(gl.getUniformLocation(program, "u_color"), color); + + wtu.setupUnitQuad(gl); + + renderbufferTest(isEnabled); + textureTest(isEnabled); +} + +function colorMaskTest() { + debug(""); + debug("Test color write masks with shared exponent color buffers"); + + const fs = `#version 300 es + precision highp float; + layout(location = 0) out vec4 color0; + layout(location = 1) out vec4 color1; + void main() { + color0 = vec4(1.0, 0.0, 0.0, 1.0); + color1 = vec4(0.0, 1.0, 0.0, 1.0); + }`; + const program = wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, fs]); + gl.useProgram(program); + + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + + const rb0 = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rb0); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGB9_E5, 4, 4); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rb0); + + const rb1 = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rb1); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA8, 4, 4); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.RENDERBUFFER, rb1); + + wtu.glErrorShouldBe(gl, gl.NO_ERROR); + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE); + + const clearValue = new Float32Array(4); + const dbiExt = gl.getExtension("OES_draw_buffers_indexed"); + + function expectError(enabled, effectiveMask, operation) { + if (!enabled || + effectiveMask == 0x0 /* 0000 */ || + effectiveMask == 0x8 /* 000A */ || + effectiveMask == 0x7 /* RGB0 */ || + effectiveMask == 0xF /* RGBA */ ) { + wtu.glErrorShouldBe(gl, gl.NO_ERROR, operation); + } else { + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, operation); + } + } + + function runOps(enabled, mask0) { + wtu.drawUnitQuad(gl); + expectError(enabled, mask0, "draw"); + + gl.clear(gl.COLOR_BUFFER_BIT); + expectError(enabled, mask0, "clear"); + + gl.clearBufferfv(gl.COLOR, 0, clearValue); + expectError(enabled, mask0, "clearBufferfv(RGB9_E5)"); + gl.clearBufferfv(gl.COLOR, 1, clearValue); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "clearBufferfv(RGBA8)"); + } + + for (let mask = 0; mask < 16; mask++) { + for (const enabled of [false, true]) { + debug(""); + debug(`Setting common color mask ` + + `${mask & 1 ? "R" : "0"}` + + `${mask & 2 ? "G" : "0"}` + + `${mask & 4 ? "B" : "0"}` + + `${mask & 8 ? "A" : "0"}` + + " with RGB9_E5 attachment " + + (enabled ? "enabled" : "disabled")); + gl.colorMask(mask & 1, mask & 2, mask & 4, mask & 8); + gl.drawBuffers([enabled ? gl.COLOR_ATTACHMENT0 : gl.NONE, + gl.COLOR_ATTACHMENT1]); + + runOps(enabled, mask); + + if (dbiExt) { + debug("Setting incompatible color mask on unused draw buffer") + dbiExt.colorMaskiOES(2, true, false, false, false); + runOps(enabled, mask); // common mask remains on draw buffer 0 + + debug("Setting incompatible color mask on RGBA8 draw buffer") + dbiExt.colorMaskiOES(1, true, false, false, false); + runOps(enabled, mask); // common mask remains on draw buffer 0 + + debug("Setting incompatible color mask on RGB9_E5 draw buffer") + dbiExt.colorMaskiOES(0, true, false, false, false); + runOps(enabled, 1); // overridden + + debug("Setting compatible color mask on RGB9_E5 draw buffer") + dbiExt.colorMaskiOES(0, true, true, true, false); + runOps(enabled, 7); // overridden + } + } + } +} + +function runTest() { + if (!gl) { + testFailed("context does not exist"); + return; + } + testPassed("context exists"); + + debug(""); + debug("Testing shared exponent rendering with extension disabled"); + formatTest(false); + + ext = gl.getExtension("WEBGL_render_shared_exponent"); + wtu.runExtensionSupportedTest(gl, "WEBGL_render_shared_exponent", ext !== null); + + if (ext !== null) { + debug(""); + debug("Testing shared exponent rendering with extension enabled"); + formatTest(true); + colorMaskTest(); + } else { + testPassed("No WEBGL_render_shared_exponent support -- this is legal"); + } +} + +runTest(); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-shader-pixel-local-storage.html b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-shader-pixel-local-storage.html new file mode 100644 index 0000000000..e548eea46c --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-shader-pixel-local-storage.html @@ -0,0 +1,445 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL WEBGL_shader_pixel_local_storage Conformance Tests</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/desktop-gl-constants.js"></script> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +<script src="../../js/tests/compositing-test.js"></script> +<script src="../../js/tests/invalid-vertex-attrib-test.js"></script> +</head> +<body> +<div id="description"></div> +<canvas id="canvas" width="128" height="128" style="background-color:#080"> </canvas> +<canvas id="canvas_no_alpha" width="128" height="128"> </canvas> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the WEBGL_shader_pixel_local_storage " + + "extension, if it is available."); + +const wtu = WebGLTestUtils; +const canvas = document.getElementById("canvas"); +const gl = wtu.create3DContext(canvas, {alpha: true}, 2); +const gl_no_alpha = wtu.create3DContext("canvas_no_alpha", {alpha: false}, 2); +let pls = null; + +// Outputs a fullscreen quad from a 4-vertex triangle strip. +const fullscreenQuadVertexShader = `#version 300 es +void main() { + gl_Position.x = (gl_VertexID & 1) == 0 ? -1. : 1.; + gl_Position.y = (gl_VertexID & 2) == 0 ? -1. : 1.; + gl_Position.zw = vec2(0, 1); +}`; + +function arraysEqual(a, b) { + if (typeof a !== typeof b) + return false; + if (a.length != b.length) + return false; + for (let i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) + return false; + } + return true; +} + +async function runTest() { + if (!gl) { + testFailed("WebGL2 context does not exist"); + finishTest(); + return; + } + + debug("\nCheck the behavior surrounding WEBGL_shader_pixel_local_storage being enabled."); + checkExtensionNotSupportedWhenDisabled(); + checkDependencyExtensionsEnabled(false); + debug("Enable WEBGL_shader_pixel_local_storage."); + pls = gl.getExtension("WEBGL_shader_pixel_local_storage"); + wtu.runExtensionSupportedTest(gl, "WEBGL_shader_pixel_local_storage", pls != null); + if (!pls) { + finishTest(); + return; + } + checkDependencyExtensionsEnabled(true); + + checkImplementationDependentLimits(); + checkInitialValues(); + checkWebGLNonNormativeBehavior(); + + await checkRendering(gl); + await checkRendering(gl_no_alpha); + + finishTest(); +} + +function checkExtensionNotSupportedWhenDisabled() { + debug("\nCheck that a context does not support WEBGL_shader_pixel_local_storage before it is " + + "enabled"); + shouldBeNull("gl.getParameter(0x96E0 /*MAX_PIXEL_LOCAL_STORAGE_PLANES_WEBGL*/)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + shouldBeNull( + "gl.getParameter(0x96E1 /*MAX_COLOR_ATTACHMENTS_WITH_ACTIVE_PIXEL_LOCAL_STORAGE_WEBGL*/)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + shouldBeNull( + "gl.getParameter(0x96E2 /*MAX_COMBINED_DRAW_BUFFERS_AND_PIXEL_LOCAL_STORAGE_PLANES_WEBGL*/)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + shouldBeNull( + "gl.getParameter(0x96E3 /*PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_WEBGL*/)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NONE); +} + +function checkDependencyExtensionsEnabled(enabled) { + debug("\nCheck that dependency extensions of WEBGL_shader_pixel_local_storage are " + + (enabled ? "enabled" : "disabled")); + if (wtu.getSupportedExtensionWithKnownPrefixes(gl, "OES_draw_buffers_indexed") !== undefined) { + gl.getIndexedParameter(gl.BLEND_EQUATION_RGB, 1); + wtu.glErrorShouldBe(gl, enabled ? gl.NONE : gl.INVALID_ENUM, + "OES_draw_buffers_indexed not enabled or disabled as expected"); + } + if (wtu.getSupportedExtensionWithKnownPrefixes(gl, "EXT_color_buffer_float") !== undefined) { + gl.bindRenderbuffer(gl.RENDERBUFFER, gl.createRenderbuffer()); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.R32F, 1, 1); + wtu.glErrorShouldBe(gl, enabled ? gl.NONE : gl.INVALID_ENUM, + "EXT_color_buffer_float not enabled or disabled as expected"); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + } + if (wtu.getSupportedExtensionWithKnownPrefixes(gl, "EXT_color_buffer_half_float") !== undefined) { + gl.bindRenderbuffer(gl.RENDERBUFFER, gl.createRenderbuffer()); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RG16F, 1, 1); + wtu.glErrorShouldBe(gl, enabled ? gl.NONE : gl.INVALID_ENUM, + "EXT_color_buffer_half_float not enabled or disabled as expected"); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + } +} + +function checkImplementationDependentLimits() { + debug("\nVerify conformant implementation-dependent PLS limits."); + window.MAX_PIXEL_LOCAL_STORAGE_PLANES = + gl.getParameter(pls.MAX_PIXEL_LOCAL_STORAGE_PLANES_WEBGL); + window.MAX_COLOR_ATTACHMENTS_WITH_ACTIVE_PIXEL_LOCAL_STORAGE = + gl.getParameter(pls.MAX_COLOR_ATTACHMENTS_WITH_ACTIVE_PIXEL_LOCAL_STORAGE_WEBGL); + window.MAX_COMBINED_DRAW_BUFFERS_AND_PIXEL_LOCAL_STORAGE_PLANES = + gl.getParameter(pls.MAX_COMBINED_DRAW_BUFFERS_AND_PIXEL_LOCAL_STORAGE_PLANES_WEBGL); + wtu.glErrorShouldBe(gl, gl.NONE, "Pixel local storage queries should be supported."); + + window.MAX_COLOR_ATTACHMENTS = gl.getParameter(gl.MAX_COLOR_ATTACHMENTS); + window.MAX_DRAW_BUFFERS = gl.getParameter(gl.MAX_DRAW_BUFFERS); + + // Table 6.X: Impementation Dependent Pixel Local Storage Limits. + shouldBeTrue("MAX_PIXEL_LOCAL_STORAGE_PLANES >= 4"); + shouldBeTrue("MAX_COLOR_ATTACHMENTS_WITH_ACTIVE_PIXEL_LOCAL_STORAGE >= 0"); + shouldBeTrue("MAX_COMBINED_DRAW_BUFFERS_AND_PIXEL_LOCAL_STORAGE_PLANES >= 4"); + + // Logical deductions based on 6.X. + shouldBeTrue(`MAX_COMBINED_DRAW_BUFFERS_AND_PIXEL_LOCAL_STORAGE_PLANES >= + MAX_PIXEL_LOCAL_STORAGE_PLANES`); + shouldBeTrue(`MAX_COMBINED_DRAW_BUFFERS_AND_PIXEL_LOCAL_STORAGE_PLANES >= + MAX_COLOR_ATTACHMENTS_WITH_ACTIVE_PIXEL_LOCAL_STORAGE`); + shouldBeTrue(`MAX_COLOR_ATTACHMENTS + MAX_PIXEL_LOCAL_STORAGE_PLANES >= + MAX_COMBINED_DRAW_BUFFERS_AND_PIXEL_LOCAL_STORAGE_PLANES`); + shouldBeTrue(`MAX_DRAW_BUFFERS + MAX_PIXEL_LOCAL_STORAGE_PLANES >= + MAX_COMBINED_DRAW_BUFFERS_AND_PIXEL_LOCAL_STORAGE_PLANES`); +} + +function checkInitialValues() { + debug("\nCheck that PLS state has the correct initial values."); + shouldBeTrue("gl.getParameter(pls.PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_WEBGL) == 0"); + wtu.glErrorShouldBe( + gl, gl.NONE, + "It's valid to query GL_PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_WEBGL even when fbo 0 is bound."); + + // Table 6.Y: Pixel Local Storage State + gl.bindFramebuffer(gl.FRAMEBUFFER, gl.createFramebuffer()); + shouldBeTrue("gl.getParameter(pls.PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_WEBGL) == 0"); + debug("Check the initial clear values for each plane."); + const MAX_PIXEL_LOCAL_STORAGE_PLANES = + gl.getParameter(pls.MAX_PIXEL_LOCAL_STORAGE_PLANES_WEBGL); + for (let i = 0; i < MAX_PIXEL_LOCAL_STORAGE_PLANES; ++i) + { + expectTrue(pls.getFramebufferPixelLocalStorageParameterWEBGL( + i, pls.PIXEL_LOCAL_FORMAT_WEBGL) == gl.NONE); + expectTrue(pls.getFramebufferPixelLocalStorageParameterWEBGL( + i, pls.PIXEL_LOCAL_TEXTURE_NAME_WEBGL) == null); + expectTrue(pls.getFramebufferPixelLocalStorageParameterWEBGL( + i, pls.PIXEL_LOCAL_TEXTURE_LEVEL_WEBGL) == 0); + expectTrue(pls.getFramebufferPixelLocalStorageParameterWEBGL( + i, pls.PIXEL_LOCAL_TEXTURE_LAYER_WEBGL) == 0); + expectTrue(arraysEqual( + pls.getFramebufferPixelLocalStorageParameterWEBGL( + i, pls.PIXEL_LOCAL_CLEAR_VALUE_FLOAT_WEBGL), + new Float32Array([0, 0, 0, 0]))); + expectTrue(arraysEqual( + pls.getFramebufferPixelLocalStorageParameterWEBGL( + i, pls.PIXEL_LOCAL_CLEAR_VALUE_INT_WEBGL), + new Int32Array([0, 0, 0, 0]))); + expectTrue(arraysEqual( + pls.getFramebufferPixelLocalStorageParameterWEBGL( + i, pls.PIXEL_LOCAL_CLEAR_VALUE_UNSIGNED_INT_WEBGL), + new Uint32Array([0, 0, 0, 0]))); + } + wtu.glErrorShouldBe(gl, gl.NONE); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); +} + +function checkWebGLNonNormativeBehavior() { + debug("\nCheck the WebGL-specific behavior not found in the " + + "ANGLE_shader_pixel_local_storage specification."); + gl.bindFramebuffer(gl.FRAMEBUFFER, gl.createFramebuffer()); + + debug("If 'texture' has been deleted, generates an INVALID_OPERATION error."); + wtu.glErrorShouldBe(gl, gl.NONE); + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, 1, 1); + wtu.glErrorShouldBe(gl, gl.NONE); + gl.deleteTexture(tex); + pls.framebufferTexturePixelLocalStorageWEBGL(0, tex, 0, 0); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION); + + debug("\nIf 'texture' was generated by a different WebGL2RenderingContext than this one, " + + "generates an INVALID_OPERATION error."); + const gl2 = wtu.create3DContext(null, null, 2); + const tex2 = gl2.createTexture(); + gl2.bindTexture(gl2.TEXTURE_2D, tex2); + gl2.texStorage2D(gl2.TEXTURE_2D, 1, gl2.RGBA8, 1, 1); + pls.framebufferTexturePixelLocalStorageWEBGL(0, tex2, 0, 0); + wtu.glErrorShouldBe(gl2, gl2.NONE); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION); + + debug("\nIf value has less than srcOffset + 4 elements, generates an INVALID_VALUE error."); + wtu.glErrorShouldBe(gl, gl.NONE); + pls.framebufferPixelLocalClearValuefvWEBGL(0, new Float32Array(3)); + wtu.glErrorShouldBe(gl, gl.INVALID_VALUE); + pls.framebufferPixelLocalClearValuefvWEBGL(1, [0, 0, 0]); + wtu.glErrorShouldBe(gl, gl.INVALID_VALUE); + pls.framebufferPixelLocalClearValueivWEBGL(2, new Int32Array(3)); + wtu.glErrorShouldBe(gl, gl.INVALID_VALUE); + pls.framebufferPixelLocalClearValueivWEBGL(3, [0, 0, 0]); + wtu.glErrorShouldBe(gl, gl.INVALID_VALUE); + pls.framebufferPixelLocalClearValueuivWEBGL(4, new Uint32Array(3)); + wtu.glErrorShouldBe(gl, gl.INVALID_VALUE); + pls.framebufferPixelLocalClearValueuivWEBGL(3, [0, 0, 0]); + wtu.glErrorShouldBe(gl, gl.INVALID_VALUE); + pls.framebufferPixelLocalClearValuefvWEBGL(2, new Float32Array(5), 2); + wtu.glErrorShouldBe(gl, gl.INVALID_VALUE); + pls.framebufferPixelLocalClearValuefvWEBGL(1, [0, 0, 0, 0, 0], 2); + wtu.glErrorShouldBe(gl, gl.INVALID_VALUE); + pls.framebufferPixelLocalClearValueivWEBGL(0, new Int32Array(5), 2); + wtu.glErrorShouldBe(gl, gl.INVALID_VALUE); + pls.framebufferPixelLocalClearValueivWEBGL(1, [0, 0, 0, 0, 0], 2); + wtu.glErrorShouldBe(gl, gl.INVALID_VALUE); + pls.framebufferPixelLocalClearValueuivWEBGL(2, new Uint32Array(5), 2); + wtu.glErrorShouldBe(gl, gl.INVALID_VALUE); + pls.framebufferPixelLocalClearValueuivWEBGL(3, [0, 0, 0, 0, 0], 2); + wtu.glErrorShouldBe(gl, gl.INVALID_VALUE); + + debug("\nCheck that srcOffset works properly."); + const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + pls.framebufferPixelLocalClearValuefvWEBGL(0, new Float32Array(arr), 1); + wtu.glErrorShouldBe(gl, gl.NONE); + shouldBeTrue(`arraysEqual(pls.getFramebufferPixelLocalStorageParameterWEBGL( + 0, pls.PIXEL_LOCAL_CLEAR_VALUE_FLOAT_WEBGL), + new Float32Array([1, 2, 3, 4]))`); + pls.framebufferPixelLocalClearValuefvWEBGL(1, arr, 2); + wtu.glErrorShouldBe(gl, gl.NONE); + shouldBeTrue(`arraysEqual(pls.getFramebufferPixelLocalStorageParameterWEBGL( + 1, pls.PIXEL_LOCAL_CLEAR_VALUE_FLOAT_WEBGL), + [2, 3, 4, 5])`); + pls.framebufferPixelLocalClearValueivWEBGL(2, new Int32Array(arr), 3); + wtu.glErrorShouldBe(gl, gl.NONE); + shouldBeTrue(`arraysEqual(pls.getFramebufferPixelLocalStorageParameterWEBGL( + 2, pls.PIXEL_LOCAL_CLEAR_VALUE_INT_WEBGL), + new Float32Array([3, 4, 5, 6]))`); + pls.framebufferPixelLocalClearValueivWEBGL(3, arr, 4); + wtu.glErrorShouldBe(gl, gl.NONE); + shouldBeTrue(`arraysEqual(pls.getFramebufferPixelLocalStorageParameterWEBGL( + 3, pls.PIXEL_LOCAL_CLEAR_VALUE_INT_WEBGL), + [4, 5, 6, 7])`); + pls.framebufferPixelLocalClearValueuivWEBGL(2, new Uint32Array(arr), 5); + wtu.glErrorShouldBe(gl, gl.NONE); + shouldBeTrue(`arraysEqual(pls.getFramebufferPixelLocalStorageParameterWEBGL( + 2, pls.PIXEL_LOCAL_CLEAR_VALUE_UNSIGNED_INT_WEBGL), + new Uint32Array([5, 6, 7, 8]))`); + pls.framebufferPixelLocalClearValueuivWEBGL(1, arr, 6); + wtu.glErrorShouldBe(gl, gl.NONE); + shouldBeTrue(`arraysEqual(pls.getFramebufferPixelLocalStorageParameterWEBGL( + 1, pls.PIXEL_LOCAL_CLEAR_VALUE_UNSIGNED_INT_WEBGL), + [6, 7, 8, 9])`); + wtu.glErrorShouldBe(gl, gl.NONE); + + debug("\nCheck that PIXEL_LOCAL_TEXTURE_NAME_WEBGL returns a WebGLTexture."); + shouldBeTrue(`pls.getFramebufferPixelLocalStorageParameterWEBGL( + 0, pls.PIXEL_LOCAL_TEXTURE_NAME_WEBGL) === null`); + window.validTex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, validTex); + gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, 1, 1); + wtu.glErrorShouldBe(gl, gl.NONE); + pls.framebufferTexturePixelLocalStorageWEBGL(0, validTex, 0, 0); + shouldBeTrue(`pls.getFramebufferPixelLocalStorageParameterWEBGL( + 0, pls.PIXEL_LOCAL_TEXTURE_NAME_WEBGL) === validTex`); + pls.framebufferTexturePixelLocalStorageWEBGL(0, null, 0, 0); + shouldBeTrue(`pls.getFramebufferPixelLocalStorageParameterWEBGL( + 0, pls.PIXEL_LOCAL_TEXTURE_NAME_WEBGL) === null`); + + wtu.glErrorShouldBe(gl, gl.NONE); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); +} + +async function checkRendering(localGL) { + const localCanvas = localGL.canvas; + const alpha = localGL.getContextAttributes().alpha; + debug("\nCheck very simple rendering with {alpha: " + alpha + "}"); + + const localPLS = localGL.getExtension("WEBGL_shader_pixel_local_storage"); + if (!localPLS) { + testFailed("localGL doesn't support pixel local storage."); + return; + } + + const tex = localGL.createTexture(); + localGL.bindTexture(localGL.TEXTURE_2D, tex); + localGL.texStorage2D(localGL.TEXTURE_2D, 1, localGL.RGBA8, localCanvas.width, localCanvas.height); + wtu.glErrorShouldBe(localGL, localGL.NONE); + + const plsFBO = localGL.createFramebuffer(); + localGL.bindFramebuffer(localGL.FRAMEBUFFER, plsFBO); + localPLS.framebufferTexturePixelLocalStorageWEBGL(0, tex, 0, 0); + wtu.glErrorShouldBe(localGL, localGL.NONE); + + localGL.viewport(0, 0, localCanvas.width, localCanvas.height); + + // Adds a uniform color into the existing color in pixel local storage. + const fs = `#version 300 es + #extension GL_ANGLE_shader_pixel_local_storage : require + precision lowp float; + uniform vec4 color; + layout(binding=0, rgba8) uniform lowp pixelLocalANGLE pls; + void main() { + vec4 newColor = color + pixelLocalLoadANGLE(pls); + pixelLocalStoreANGLE(pls, newColor); + }`; + + const program = wtu.setupProgram(localGL, [fullscreenQuadVertexShader, fs]); + if (!program) { + testFailed("Failed to compile program."); + return; + } + + localGL.useProgram(program); + const colorUniLocation = localGL.getUniformLocation(program, "color"); + wtu.glErrorShouldBe(localGL, localGL.NONE); + + // Disable color mask to ensure PLS and canvas manage their own color masks properly. + localGL.colorMask(false, true, false, true); + + // Set global variables for shouldBeTrue(). + window.localGL = localGL; + window.localPLS = localPLS; + + debug("\nCheck that pixel local storage works properly"); + localGL.disable(localGL.DITHER); + localPLS.beginPixelLocalStorageWEBGL([localPLS.LOAD_OP_ZERO_WEBGL]); + wtu.glErrorShouldBe(localGL, localGL.NONE); + shouldBeTrue("localGL.getParameter(localPLS.PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_WEBGL) == 1"); + + localGL.uniform4f(colorUniLocation, 0, 1, 0, 0); + localGL.drawArrays(localGL.TRIANGLE_STRIP, 0, 4); + + localPLS.pixelLocalStorageBarrierWEBGL(); + + localGL.uniform4f(colorUniLocation, 1, 0, 0, 0); + localGL.drawArrays(localGL.TRIANGLE_STRIP, 0, 4); + + localPLS.endPixelLocalStorageWEBGL([localPLS.STORE_OP_STORE_WEBGL]); + wtu.glErrorShouldBe(localGL, localGL.NONE); + shouldBeTrue("localGL.getParameter(localPLS.PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_WEBGL) == 0"); + + const readFBO = localGL.createFramebuffer(); + localGL.bindFramebuffer(localGL.READ_FRAMEBUFFER, readFBO); + localGL.framebufferTexture2D(localGL.READ_FRAMEBUFFER, localGL.COLOR_ATTACHMENT0, + localGL.TEXTURE_2D, tex, 0); + wtu.glErrorShouldBe(localGL, localGL.NONE); + wtu.checkCanvas(localGL, [255, 255, 0, 0]); + + debug("\nCheck that alpha is properly handled in the main canvas."); + localGL.bindFramebuffer(localGL.DRAW_FRAMEBUFFER, null); + localGL.blitFramebuffer(0, 0, localCanvas.width, localCanvas.height, 0, 0, localCanvas.width, + localCanvas.height, localGL.COLOR_BUFFER_BIT, localGL.NEAREST); + localGL.bindFramebuffer(localGL.FRAMEBUFFER, null); + wtu.glErrorShouldBe(localGL, localGL.NONE); + wtu.checkCanvas(localGL, [255, 255, 0, alpha ? 0 : 255]); + + localGL.bindFramebuffer(localGL.FRAMEBUFFER, plsFBO); + localPLS.beginPixelLocalStorageWEBGL([localPLS.LOAD_OP_LOAD_WEBGL]); + wtu.glErrorShouldBe(localGL, localGL.NONE); + shouldBeTrue("localGL.getParameter(localPLS.PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_WEBGL) == 1"); + + debug("\nGoing down from composite."); + + // The canvas should get cleared after compositing, even if PLS is active and color mask is + // disabled. + await new Promise(resolve => wtu.waitForComposite(resolve)); + + // Reset global variables for shouldBeTrue() after await. + window.localGL = localGL; + window.localPLS = localPLS; + + debug("\nBack from composite!"); + debug("\nPLS should still be active on plsFBO even after being interrupted for compositing."); + wtu.glErrorShouldBe(localGL, localGL.NONE); + shouldBeTrue("localGL.getParameter(localPLS.PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_WEBGL) == 1"); + + localGL.uniform4f(colorUniLocation, 0, 0, 1, 0); + localGL.drawArrays(localGL.TRIANGLE_STRIP, 0, 4); + + localPLS.endPixelLocalStorageWEBGL([localPLS.STORE_OP_STORE_WEBGL]); + wtu.glErrorShouldBe(localGL, localGL.NONE); + shouldBeTrue("localGL.getParameter(localPLS.PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_WEBGL) == 0"); + + debug("\nThe canvas should have gotten cleared while PLS was active."); + localGL.bindFramebuffer(localGL.FRAMEBUFFER, null); + wtu.checkCanvas(localGL, [0, 0, 0, alpha ? 0 : 255]); + + debug("\nThe additional PLS draw to plsFBO should have still worked after being interrupted " + + "for compositing."); + localGL.bindFramebuffer(localGL.READ_FRAMEBUFFER, readFBO); + wtu.checkCanvas(localGL, [255, 255, 255, 0]); + wtu.glErrorShouldBe(localGL, localGL.NONE); + + // Draws 'tex' to the canvas. + const fs2 = `#version 300 es + uniform lowp sampler2D tex; + out lowp vec4 fragColor; + void main() { + ivec2 pixelCoord = ivec2(floor(gl_FragCoord.xy)); + fragColor = texelFetch(tex, pixelCoord, 0); + }`; + + const program2 = wtu.setupProgram(localGL, [fullscreenQuadVertexShader, fs2]); + if (!program2) { + testFailed("Failed to compile program2."); + return; + } + + debug("\nBlue should still be disabled in the color mask. Alpha is not disabled but should be " + + "implicitly disabled since the canvas doesn't have alpha."); + localGL.useProgram(program2); + localGL.uniform1i(localGL.getUniformLocation(program2, "tex"), 0); + localGL.bindFramebuffer(localGL.FRAMEBUFFER, null); + localGL.drawArrays(localGL.TRIANGLE_STRIP, 0, 4); + wtu.checkCanvas(localGL, [0, 255, 0, alpha ? 0 : 255]); + + debug("\nThe client's color mask should have been preserved."); + shouldBeTrue(`arraysEqual(localGL.getParameter(localGL.COLOR_WRITEMASK), + [false, true, false, true])`); +} + +runTest(); +var successfullyParsed = true; +</script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-stencil-texturing.html b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-stencil-texturing.html new file mode 100644 index 0000000000..729a5bcf8a --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/extensions/webgl-stencil-texturing.html @@ -0,0 +1,279 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL WEBGL_stencil_texturing Conformance Tests</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of the WEBGL_stencil_texturing extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext(null, null, 2); +var ext; + +function runTestNoExtension() { + debug(""); + debug("Check the texture parameter without the extension"); + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + + shouldBeNull("gl.getTexParameter(gl.TEXTURE_2D, 0x90EA /* DEPTH_STENCIL_TEXTURE_MODE_WEBGL */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no more errors"); + + gl.texParameteri(gl.TEXTURE_2D, 0x90EA /* DEPTH_STENCIL_TEXTURE_MODE_WEBGL */, gl.DEPTH_COMPONENT); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown for texParameteri without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no more errors"); + + gl.texParameterf(gl.TEXTURE_2D, 0x90EA /* DEPTH_STENCIL_TEXTURE_MODE_WEBGL */, gl.DEPTH_COMPONENT); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown for texParameterf without enabling the extension"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no more errors"); + + const sampler = gl.createSampler(); + gl.samplerParameteri(sampler, 0x90EA /* DEPTH_STENCIL_TEXTURE_MODE_WEBGL */, gl.DEPTH_COMPONENT); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown for samplerParameteri"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no more errors"); + gl.samplerParameterf(sampler, 0x90EA /* DEPTH_STENCIL_TEXTURE_MODE_WEBGL */, gl.DEPTH_COMPONENT); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown for samplerParameterf"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no more errors"); +} + +function checkEnums() { + debug(""); + debug("Check enums"); + shouldBe("ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL", "0x90EA"); + shouldBe("ext.STENCIL_INDEX_WEBGL", "0x1901"); +} + +function checkQueries() { + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + + debug(""); + debug("Check default texture state"); + shouldBe('gl.getTexParameter(gl.TEXTURE_2D, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL)', 'gl.DEPTH_COMPONENT'); + debug(""); + debug("Check texture state updates using texParameteri"); + gl.texParameteri(gl.TEXTURE_2D, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL, ext.STENCIL_INDEX_WEBGL); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + shouldBe('gl.getTexParameter(gl.TEXTURE_2D, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL)', 'ext.STENCIL_INDEX_WEBGL'); + gl.texParameteri(gl.TEXTURE_2D, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL, gl.DEPTH_COMPONENT); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + shouldBe('gl.getTexParameter(gl.TEXTURE_2D, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL)', 'gl.DEPTH_COMPONENT'); + gl.texParameteri(gl.TEXTURE_2D, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL, 0); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "invalid depth stencil mode value rejected by texParameteri"); + shouldBe('gl.getTexParameter(gl.TEXTURE_2D, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL)', 'gl.DEPTH_COMPONENT'); + debug(""); + debug("Check texture state updates using texParameterf"); + gl.texParameterf(gl.TEXTURE_2D, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL, ext.STENCIL_INDEX_WEBGL); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + shouldBe('gl.getTexParameter(gl.TEXTURE_2D, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL)', 'ext.STENCIL_INDEX_WEBGL'); + gl.texParameterf(gl.TEXTURE_2D, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL, gl.DEPTH_COMPONENT); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + shouldBe('gl.getTexParameter(gl.TEXTURE_2D, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL)', 'gl.DEPTH_COMPONENT'); + gl.texParameterf(gl.TEXTURE_2D, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL, 0); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "invalid depth stencil mode value rejected by texParameterf"); + shouldBe('gl.getTexParameter(gl.TEXTURE_2D, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL)', 'gl.DEPTH_COMPONENT'); + + debug(""); + debug("Check that depth stencil texture mode is not accepted as a sampler state"); + const sampler = gl.createSampler(); + gl.samplerParameteri(sampler, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL, gl.DEPTH_COMPONENT); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown for samplerParameteri"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no more errors"); + gl.samplerParameterf(sampler, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL, gl.DEPTH_COMPONENT); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown for samplerParameterf"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no more errors"); +} + +function checkSampling() { + const formats = [ + {name: "DEPTH_COMPONENT16", internalFormat: gl.DEPTH_COMPONENT16, + format: gl.DEPTH_COMPONENT, type: gl.UNSIGNED_SHORT}, + {name: "DEPTH_COMPONENT24", internalFormat: gl.DEPTH_COMPONENT24, + format: gl.DEPTH_COMPONENT, type: gl.UNSIGNED_INT}, + {name: "DEPTH_COMPONENT32F", internalFormat: gl.DEPTH_COMPONENT32F, + format: gl.DEPTH_COMPONENT, type: gl.FLOAT}, + {name: "DEPTH24_STENCIL8", internalFormat: gl.DEPTH24_STENCIL8, + format: gl.DEPTH_STENCIL, type: gl.UNSIGNED_INT_24_8}, + {name: "DEPTH32F_STENCIL8", internalFormat: gl.DEPTH32F_STENCIL8, + format: gl.DEPTH_STENCIL, type: gl.FLOAT_32_UNSIGNED_INT_24_8_REV} + ]; + + gl.enable(gl.DEPTH_TEST); + gl.enable(gl.STENCIL_TEST); + gl.stencilFunc(gl.ALWAYS, 170, 0xFF); + gl.stencilOp(gl.REPLACE, gl.REPLACE, gl.REPLACE); + + wtu.setupUnitQuad(gl); + + const drawProgram = wtu.setupProgram(gl, [wtu.simpleVertexShader, + wtu.simpleColorFragmentShader]); + + const readDepthProgram = wtu.setupProgram(gl, [wtu.simpleTextureVertexShaderESSL300, + wtu.simpleTextureFragmentShaderESSL300]); + + const readStencilShader = `#version 300 es + precision highp float; + uniform highp usampler2D tex; + in vec2 texCoord; + out vec4 out_color; + void main() { + out_color = vec4(texture(tex, texCoord)) / 255.0; + }`; + const readStencilProgram = wtu.setupProgram(gl, [wtu.simpleTextureVertexShaderESSL300, + readStencilShader]); + + for (const format of formats) { + debug(""); + debug(`Testing depth stencil texture modes with ${format.name}`); + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + + const rbo = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA8, 1, 1); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rbo); + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texImage2D(gl.TEXTURE_2D, 0, format.internalFormat, 1, 1, 0, format.format, format.type, null); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "texture created"); + + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, tex, 0); + if (format.format == gl.DEPTH_STENCIL) { + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.TEXTURE_2D, tex, 0); + } + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE); + + gl.clear(gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); + gl.useProgram(drawProgram); + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "no errors after drawing to the depth or depth stencil texture"); + + // Detach the depth or depth stencil texture to avoid feedback loop + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, null); + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE); + + const magFilters = ['NEAREST', 'LINEAR']; + + const minFilters = [ + 'NEAREST', + 'LINEAR', + 'NEAREST_MIPMAP_NEAREST', + 'LINEAR_MIPMAP_NEAREST', + 'NEAREST_MIPMAP_LINEAR', + 'LINEAR_MIPMAP_LINEAR' + ]; + + const modes = [ + [gl.DEPTH_COMPONENT, 'DEPTH_COMPONENT'], + [ext.STENCIL_INDEX_WEBGL, 'STENCIL_INDEX_WEBGL'] + ]; + + const programs = [ + [readDepthProgram, 'depth'], + [readStencilProgram, 'stencil'] + ]; + + function validFilters(magFilter, minFilter) { + return magFilter == gl.NEAREST && + (minFilter == gl.NEAREST || minFilter == gl.NEAREST_MIPMAP_NEAREST); + } + + for (const program of programs) { + gl.useProgram(program[0]); + for (const mode of modes) { + gl.texParameteri(gl.TEXTURE_2D, ext.DEPTH_STENCIL_TEXTURE_MODE_WEBGL, mode[0]); + for (const magFilter of magFilters) { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl[magFilter]); + for (const minFilter of minFilters) { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl[minFilter]); + debug(`Program: ${program[1]}, mode: ${mode[1]}, mag: ${magFilter}, min: ${minFilter}`); + + gl.clear(gl.COLOR_BUFFER_BIT); + wtu.drawUnitQuad(gl); + + if (format.format == gl.DEPTH_COMPONENT || mode[0] == gl.DEPTH_COMPONENT) { + if (program[1] == 'depth') { + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + if (validFilters(gl[magFilter], gl[minFilter])) { + wtu.checkCanvasRect(gl, 0, 0, 1, 1, [128, 0, 0, 255], "sampling depth from complete texture", 1); + } else { + wtu.checkCanvasRect(gl, 0, 0, 1, 1, [0, 0, 0, 255], "sampling depth from incomplete texture", 1); + } + } else { + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "sampling depth using incompatible program"); + } + } else { + if (program[1] == 'stencil') { + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + if (validFilters(gl[magFilter], gl[minFilter])) { + wtu.checkCanvasRect(gl, 0, 0, 1, 1, [170, 0, 0, 1], "sampling stencil from complete texture", 1); + } else { + // Incomplete textures may produce [0, 0, 0, 1] or [0, 0, 0, 255]. + const value = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, value); + wtu.glErrorShouldBe(gl, gl.NO_ERROR); + const msg = "sampling stencil from incomplete texture"; + if (value[0] == 0 && value[1] == 0 && value[2] == 0 && (value[3] == 1 || value[3] == 255)) { + testPassed(msg); + } else { + testFailed(`${msg}: ${value}`); + } + } + } else { + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "sampling stencil using incompatible program"); + } + } + } + } + } + } + } +} + +function runTest() { + if (!gl) { + testFailed("context does not exist"); + return; + } + testPassed("context exists"); + + runTestNoExtension(); + + ext = gl.getExtension("WEBGL_stencil_texturing"); + wtu.runExtensionSupportedTest(gl, "WEBGL_stencil_texturing", ext !== null); + + if (ext !== null) { + checkEnums(); + checkQueries(); + checkSampling(); + } else { + testPassed("No WEBGL_stencil_texturing support -- this is legal"); + } +} + +runTest(); + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/glsl3/00_test_list.txt b/dom/canvas/test/webgl-conf/checkout/conformance2/glsl3/00_test_list.txt index 5a47d470f9..9e8bc87a62 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/glsl3/00_test_list.txt +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/glsl3/00_test_list.txt @@ -41,6 +41,7 @@ shader-with-invalid-characters.html shader-with-mis-matching-uniform-block.html short-circuiting-in-loop-condition.html --min-version 2.0.1 switch-case.html +--min-version 2.0.1 texture-bias.html --min-version 2.0.1 texture-offset-non-constant-offset.html texture-offset-out-of-range.html --min-version 2.0.1 texture-offset-uniform-texture-coordinate.html diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/glsl3/texture-bias.html b/dom/canvas/test/webgl-conf/checkout/conformance2/glsl3/texture-bias.html new file mode 100644 index 0000000000..0c30eb7129 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/glsl3/texture-bias.html @@ -0,0 +1,146 @@ +<!-- +Copyright (c) 2022 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>GLSL texture bias test</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +<script src="../../js/glsl-conformance-test.js"></script> +</head> +<body> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description("Texture bias should both function and respect limits."); + +function runTest(gl) { + // no if idea any drivers have a giant limit like 2^32 so just in case. + const kMaxMaxTextureSize = 256 * 1024 * 1024; + const maxTextureSize = Math.min(kMaxMaxTextureSize, gl.getParameter(gl.MAX_TEXTURE_SIZE)); + const maxLODs = (Math.log2(maxTextureSize) | 0) + 1; + const maxTextureLODBias = gl.getParameter(gl.MAX_TEXTURE_LOD_BIAS); + + debug(`maxTextureSize: ${maxTextureSize}`); + debug(`maxLODs: ${maxLODs}`); + debug(`maxTextureLODBias: ${maxTextureLODBias}`); + + const vs = `#version 300 es + uniform float uvMult; + out vec2 v_uv; + void main() { + vec2 xy = vec2( + gl_VertexID % 2, + (gl_VertexID / 2 + gl_VertexID / 3) % 2); + + gl_Position = vec4(xy * 2. - 1.0, 0, 1); + v_uv = xy * uvMult; + } + `; + const fs = `#version 300 es + precision highp float; + uniform sampler2D tex; + uniform float biasMult; + in vec2 v_uv; + out vec4 fragColor; + void main() { + vec4 texColor = texture(tex, v_uv, (gl_FragCoord.x - 0.5) * biasMult); // the color we care about + vec4 texelColor = texelFetch(tex, ivec2(0), int(gl_FragCoord)); // just a sanity check + vec4 coordColor = vec4((100.0 + gl_FragCoord.x - 0.5) / 255.0); // another sanity check + fragColor = mix(texColor, coordColor, step(1.0, gl_FragCoord.y)); // line < 1 = texColor, line >= 1 = coordColor + fragColor = mix(fragColor, texelColor, step(2.0, gl_FragCoord.y)); // line < 2 = fragColor, line >= 2 = texelColor + } + `; + const program = wtu.setupProgram(gl, [vs, fs]); + const uvMultLoc = gl.getUniformLocation(program, 'uvMult'); + const biasLoc = gl.getUniformLocation(program, 'biasMult'); + + gl.canvas.width = maxLODs; + gl.canvas.height = 3; + gl.viewport(0, 0, maxLODs, 3); + + // create a texture where each mip is a different color (1, 2, 3, ...) + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST_MIPMAP_NEAREST); + gl.texStorage2D(gl.TEXTURE_2D, maxLODs, gl.RGBA8, maxTextureSize, 1); + { + let level = 0; + for (let width = maxTextureSize; width > 0; width = width / 2 | 0) { + const pixels = new Uint8Array(width * 1 * 4); + pixels.fill(level + 1); + debug(`fill mip level: ${level}, width: ${width}`); + gl.texSubImage2D(gl.TEXTURE_2D, level, 0, 0, width, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + wtu.glErrorShouldBe(gl, gl.NO_ERROR); + ++level; + } + } + + // Draw each mip. Result should be [mip0, mip1, mip2, ...] + debug(""); + debug("check positive bias"); + // set the UVs so we'd get mip level 0 for every pixel + gl.uniform1f(uvMultLoc, maxLODs / maxTextureSize); + gl.uniform1f(biasLoc, 1); + gl.drawArrays(gl.TRIANGLES, 0, 6); + + wtu.glErrorShouldBe(gl, gl.NO_ERROR); + + const clampPlusMinus = (v, limit) => Math.min(limit, Math.max(-limit, v)); + + const checkResults = (gl, biasMult) => { + const base = biasMult > 0 ? 1 : maxLODs; + for (let i = 0; i < maxLODs; ++i) { + { + const expected = new Array(4).fill(clampPlusMinus(i * biasMult, maxTextureLODBias) + base); + wtu.checkCanvasRect(gl, i, 0, 1, 1, expected, `should be: ${expected}`); + } + { + const expected = new Array(4).fill(100 + i); + wtu.checkCanvasRect(gl, i, 1, 1, 1, expected, `should be: ${expected}`); + } + { + const expected = new Array(4).fill(i + 1); + wtu.checkCanvasRect(gl, i, 2, 1, 1, expected, `should be: ${expected}`); + } + } + } + + checkResults(gl, 1); + + // Draw each mip. Result should be [mipMax, mipMax - 1, mipMax - 2, ...] + debug(""); + debug("check negative bias"); + // set the UVs so we'd get highest mip level (the 1x1 level mip) for every pixel + gl.uniform1f(uvMultLoc, maxLODs); + gl.uniform1f(biasLoc, -1); + gl.drawArrays(gl.TRIANGLES, 0, 6); + + checkResults(gl, -1); + + finishTest(); +} + +const wtu = WebGLTestUtils; + +const gl = wtu.create3DContext(undefined, undefined, 2); + +var successfullyParsed = true; + +if (!gl) { + testFailed("Unable to initialize WebGL 2.0 context."); +} else { + runTest(gl); +} + +</script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/query/00_test_list.txt b/dom/canvas/test/webgl-conf/checkout/conformance2/query/00_test_list.txt index c40921bf88..a2e21b5ad7 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/query/00_test_list.txt +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/query/00_test_list.txt @@ -1,2 +1,3 @@ occlusion-query.html +--min-version 2.0.1 occlusion-query-scissor.html query.html diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/query/occlusion-query-scissor.html b/dom/canvas/test/webgl-conf/checkout/conformance2/query/occlusion-query-scissor.html new file mode 100644 index 0000000000..dec88e56d3 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/query/occlusion-query-scissor.html @@ -0,0 +1,117 @@ +<!-- +Copyright (c) 2022 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>WebGL Occlusion Query w/Scissor Conformance Tests</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"></script> +</head> +<body> +<div id="description"></div> +<canvas id="canvas" style="width: 50px; height: 50px;"> </canvas> +<div id="console"></div> +<script> +"use strict"; +description("This test verifies the functionality of occlusion query objects with the scissor test."); +debug('Regression test for <a href="http://anglebug.com/7157">http://anglebug.com/7157</a>'); + +debug(""); + +const wtu = WebGLTestUtils; + +const wait = () => new Promise(resolve => setTimeout(resolve)); + +async function runOcclusionQueryTest(gl) { + const kSize = 4; + const colors = [ + [0, 0, 0, 255], + [255, 0, 0, 255], + [0, 255, 0, 255], + [255, 255, 0, 255], + ]; + const framebuffers = colors.map((color, index) => { + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, kSize, kSize, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + + const fb = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fb); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); + + gl.clearColor(index & 1, (index >> 1) & 1, (index >> 2) & 1, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + + wtu.checkCanvasRect(gl, 0, 0, kSize, kSize, color); + + return fb; + }); + + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "setup should succeed"); + + gl.viewport(0, 0, kSize, kSize); + + const program = wtu.setupSimpleColorProgram(gl, 0); + gl.uniform4f(gl.getUniformLocation(program, "u_color"), 0, 1, 0, 1); + wtu.setupUnitQuad(gl, 0); + + const query = gl.createQuery();; + + for (let i = 0; i < 256; ++i) + { + gl.beginQuery(gl.ANY_SAMPLES_PASSED, query); + let drawn = false; + + framebuffers.forEach((fb, index) => { + gl.bindFramebuffer(gl.FRAMEBUFFER, fb); + const scissor = (i >> 4 >> index) & 1; + if (i & (1 << index)) + { + if (scissor) + { + gl.enable(gl.SCISSOR_TEST); + gl.scissor(0, 0, 0, 0); + } + wtu.drawUnitQuad(gl); + drawn ||= !scissor; + if (scissor) + { + gl.disable(gl.SCISSOR_TEST); + gl.scissor(0, 0, kSize, kSize); + } + } + }); + + gl.endQuery(gl.ANY_SAMPLES_PASSED); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should draw"); + + do { + await wait(); + } while (!gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE)); + + const result = gl.getQueryParameter(query, gl.QUERY_RESULT); + + const expected = drawn ? 1 : 0; + assertMsg(result === expected, `pass ${i}, result: ${result} === expected: ${expected}`); + } + finishTest(); +} + +const canvas = document.getElementById("canvas"); +const gl = wtu.create3DContext(canvas, null, 2); + +if (!gl) { + testFailed("WebGL context does not exist"); +} else { + testPassed("WebGL context exists"); + runOcclusionQueryTest(gl); +} +</script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/renderbuffers/framebuffer-object-attachment.html b/dom/canvas/test/webgl-conf/checkout/conformance2/renderbuffers/framebuffer-object-attachment.html index 754ff2cc73..bc4d623446 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/renderbuffers/framebuffer-object-attachment.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/renderbuffers/framebuffer-object-attachment.html @@ -111,9 +111,10 @@ function testFramebufferWebGL1RequiredCombinations() { gl.deleteFramebuffer(fbo); } -function testDepthStencilAttachmentBehaviors() { +function testDepthStencilAttachmentBehaviors(testOrphanedRenderbuffers) { + let suffix = testOrphanedRenderbuffers ? " with deleted renderbuffer" : ""; debug(""); - debug("Checking ES3 DEPTH_STENCIL_ATTACHMENT behaviors are implemented for WebGL 2"); + debug("Checking ES3 DEPTH_STENCIL_ATTACHMENT behaviors are implemented for WebGL 2" + suffix); // DEPTH_STENCIL_ATTACHMENT is treated as an independent attachment point in WebGL 1; // however, in WebGL 2, it is treated as an alias for DEPTH_ATTACHMENT + STENCIL_ATTACHMENT. var size = 16; @@ -127,24 +128,45 @@ function testDepthStencilAttachmentBehaviors() { gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA8, size, size); checkFramebuffer([gl.FRAMEBUFFER_COMPLETE]); - var depthBuffer = gl.createRenderbuffer(); - gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer); - gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, size, size); + function createDepthBuffer() { + let buffer = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, buffer); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, size, size); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + return buffer; + } + + function createStencilBuffer() { + let buffer = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, buffer); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.STENCIL_INDEX8, size, size); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + return buffer; + } - var stencilBuffer = gl.createRenderbuffer(); - gl.bindRenderbuffer(gl.RENDERBUFFER, stencilBuffer); - gl.renderbufferStorage(gl.RENDERBUFFER, gl.STENCIL_INDEX8, size, size); + function createDepthStencilBuffer() { + let buffer = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, buffer); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, size, size); + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + return buffer; + } - var depthStencilBuffer = gl.createRenderbuffer(); - gl.bindRenderbuffer(gl.RENDERBUFFER, depthStencilBuffer); - gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, size, size); + function orphan(renderbuffer) { + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.deleteRenderbuffer(renderbuffer); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + } wtu.glErrorShouldBe(gl, gl.NO_ERROR); debug(""); - debug("color + depth"); + debug("color + depth" + suffix); + var depthBuffer = createDepthBuffer(); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer); + if (testOrphanedRenderbuffers) + orphan(depthBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", null); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", null); @@ -153,9 +175,12 @@ function testDepthStencilAttachmentBehaviors() { checkBufferBits(gl.DEPTH_ATTACHMENT); debug(""); - debug("color + depth + stencil: depth != stencil"); + debug("color + depth + stencil: depth != stencil" + suffix); + var stencilBuffer = createStencilBuffer(); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, stencilBuffer); + if (testOrphanedRenderbuffers) + orphan(stencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", stencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", null); @@ -170,9 +195,12 @@ function testDepthStencilAttachmentBehaviors() { wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION); debug(""); - debug("color + depth: DEPTH_STENCIL for DEPTH_ATTACHMENT"); + debug("color + depth: DEPTH_STENCIL for DEPTH_ATTACHMENT" + suffix); + var depthStencilBuffer = createDepthStencilBuffer(); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthStencilBuffer); + if (testOrphanedRenderbuffers) + orphan(depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", null); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", null); @@ -181,9 +209,16 @@ function testDepthStencilAttachmentBehaviors() { checkBufferBits(gl.DEPTH_ATTACHMENT); debug(""); - debug("color + depth + stencil: DEPTH_STENCIL for DEPTH_ATTACHMENT and STENCIL_ATTACHMENT"); + debug("color + depth + stencil: DEPTH_STENCIL for DEPTH_ATTACHMENT and STENCIL_ATTACHMENT" + suffix); + if (testOrphanedRenderbuffers) { + depthStencilBuffer = createDepthStencilBuffer(); + gl.framebufferRenderbuffer( + gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthStencilBuffer); + } gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.RENDERBUFFER, depthStencilBuffer); + if (testOrphanedRenderbuffers) + orphan(depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); @@ -192,7 +227,7 @@ function testDepthStencilAttachmentBehaviors() { checkBufferBits(gl.DEPTH_STENCIL_ATTACHMENT); debug(""); - debug("color + depth_stencil"); + debug("color + depth_stencil" + suffix); var texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH24_STENCIL8, size, size, 0, gl.DEPTH_STENCIL, gl.UNSIGNED_INT_24_8, null); @@ -209,8 +244,12 @@ function testDepthStencilAttachmentBehaviors() { shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", null); wtu.glErrorShouldBe(gl, gl.NO_ERROR); + if (testOrphanedRenderbuffers) + depthStencilBuffer = createDepthStencilBuffer(); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, depthStencilBuffer); + if (testOrphanedRenderbuffers) + orphan(depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); @@ -219,16 +258,24 @@ function testDepthStencilAttachmentBehaviors() { checkBufferBits(gl.DEPTH_STENCIL_ATTACHMENT); debug(""); - debug("DEPTH_STENCIL_ATTACHMENT overwrites DEPTH_ATTACHMENT/STENCIL_ATTACHMENT") + debug("DEPTH_STENCIL_ATTACHMENT overwrites DEPTH_ATTACHMENT/STENCIL_ATTACHMENT" + suffix); + if (testOrphanedRenderbuffers) + depthBuffer = createDepthBuffer(); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer); + if (testOrphanedRenderbuffers) + orphan(depthBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", null); wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION); + if (testOrphanedRenderbuffers) + depthStencilBuffer = createDepthStencilBuffer(); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, depthStencilBuffer); + if (testOrphanedRenderbuffers) + orphan(depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); @@ -244,9 +291,13 @@ function testDepthStencilAttachmentBehaviors() { checkBufferBits(); debug(""); - debug("STENCIL_ATTACHMENT overwrites stencil set by DEPTH_STENCIL_ATTACHMENT") + debug("STENCIL_ATTACHMENT overwrites stencil set by DEPTH_STENCIL_ATTACHMENT" + suffix); + if (testOrphanedRenderbuffers) + depthStencilBuffer = createDepthStencilBuffer(); gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, depthStencilBuffer); + if (testOrphanedRenderbuffers) + orphan(depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); @@ -260,6 +311,28 @@ function testDepthStencilAttachmentBehaviors() { wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION); checkFramebuffer([gl.FRAMEBUFFER_COMPLETE]); checkBufferBits(gl.DEPTH_ATTACHMENT); + + debug(""); + debug("DEPTH_ATTACHMENT overwrites depth set by DEPTH_STENCIL_ATTACHMENT" + suffix); + if (testOrphanedRenderbuffers) + depthStencilBuffer = createDepthStencilBuffer(); + gl.framebufferRenderbuffer( + gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, depthStencilBuffer); + if (testOrphanedRenderbuffers) + orphan(depthStencilBuffer); + shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); + shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); + shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); + wtu.glErrorShouldBe(gl, gl.NO_ERROR); + + gl.framebufferRenderbuffer( + gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, null); + shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", null); + shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", depthStencilBuffer); + shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", null); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION); + checkFramebuffer([gl.FRAMEBUFFER_COMPLETE]); + checkBufferBits(gl.STENCIL_ATTACHMENT); } function testFramebufferIncompleteAttachment() { @@ -469,7 +542,8 @@ description("Test framebuffer object attachment behaviors"); shouldBeNonNull("gl = wtu.create3DContext(undefined, undefined, 2)"); testFramebufferWebGL1RequiredCombinations(); -testDepthStencilAttachmentBehaviors(); +testDepthStencilAttachmentBehaviors(false); +testDepthStencilAttachmentBehaviors(true); testFramebufferIncompleteAttachment(); testFramebufferIncompleteMissingAttachment(); testFramebufferWithImagesOfDifferentSizes(); diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/renderbuffers/framebuffer-texture-layer.html b/dom/canvas/test/webgl-conf/checkout/conformance2/renderbuffers/framebuffer-texture-layer.html index 0e435d6a2e..13e5d790e8 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/renderbuffers/framebuffer-texture-layer.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/renderbuffers/framebuffer-texture-layer.html @@ -121,6 +121,37 @@ function testFramebufferTextureLayer() { "attaching a depth_stencil texture to a framebuffer should succeed."); checkFramebuffer([gl.FRAMEBUFFER_COMPLETE]); + var texDepthStencil = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D_ARRAY, texDepthStencil); + + var texDepthStencilMany = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D_ARRAY, texDepthStencilMany); + var fbDepthStencilMany = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbDepthStencilMany); + gl.texImage3D(gl.TEXTURE_2D_ARRAY, + 0, // level + gl.DEPTH24_STENCIL8, // internalFormat + 1, // width + 1, // height + 2, // depth + 0, // border + gl.DEPTH_STENCIL, // format + gl.UNSIGNED_INT_24_8, // type + new Uint32Array([0, 1])); // data + + gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, texDepthStencilMany, 0, 0); + gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, texDepthStencilMany, 0, 0); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, + "attaching a depth_stencil 2d array texture level 0 to a framebuffer should succeed."); + checkFramebuffer([gl.FRAMEBUFFER_COMPLETE]); + shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", texDepthStencilMany); + gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT, texDepthStencilMany, 0, 1); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, + "attaching a 2d array texture level 0 to depth and layer 1 to stencil attachment of a framebuffer should succeed."); + // "Depth and stencil attachments, if present, are the same image." If not, then "FRAMEBUFFER_UNSUPPORTED". + checkFramebuffer([gl.FRAMEBUFFER_UNSUPPORTED]); + shouldEvaluateTo("gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)", texDepthStencilMany); + // Clean up gl.deleteTexture(tex3d); gl.deleteTexture(texDepthStencil); diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/rendering/00_test_list.txt b/dom/canvas/test/webgl-conf/checkout/conformance2/rendering/00_test_list.txt index 92ce232ee2..c4a1bbe9e8 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/rendering/00_test_list.txt +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/rendering/00_test_list.txt @@ -11,7 +11,6 @@ blitframebuffer-size-overflow.html --min-version 2.0.1 blitframebuffer-stencil-only.html blitframebuffer-test.html --min-version 2.0.1 blitframebuffer-unaffected-by-colormask.html ---min-version 2.0.1 builtin-vert-attribs.html canvas-resizing-with-pbo-bound.html --min-version 2.0.1 clearbuffer-sub-source.html --min-version 2.0.1 clearbufferfv-with-alpha-false.html diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/rendering/builtin-vert-attribs.html b/dom/canvas/test/webgl-conf/checkout/conformance2/rendering/builtin-vert-attribs.html deleted file mode 100644 index cc64c9034b..0000000000 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/rendering/builtin-vert-attribs.html +++ /dev/null @@ -1,408 +0,0 @@ -<!-- -Copyright (c) 2022 The Khronos Group Inc. -Use of this source code is governed by an MIT-style license that can be -found in the LICENSE.txt file. ---> - -<!DOCTYPE html> -<html> -<head> -<meta charset=utf-8> -<link rel=stylesheet href="../../resources/js-test-style.css"/> -<script src="../../js/js-test-pre.js"></script> -<script src="../../js/webgl-test-utils.js"></script> -</head> -<body> -<canvas id=e_canvas width=1 height=1 style="width: 100px; height: 100px;"></canvas> -<div id=description></div> -<div id=console></div> -<script> -"use strict"; -description('gl_VertexID and gl_InstanceID should behave as per spec.'); - -// - -/* -So what are gl_VertexID and gl_InstanceID supposed to do? -In ES 3.0 and GL 4.1 (Core), this is all we get: - -# ES 3.0 - -> (p78) gl_VertexID holds the integer index i implicitly passed by DrawArrays or -> one of the other drawing commands defined in section 2.9.3. The value of -> gl_VertexID is defined if and only if all enabled vertex arrays have non-zero buffer -> object bindings. -> gl_InstanceID holds the integer instance number of the current primitive in -> an instanced draw call (see section 2.9.3). - - -# GL 4.1 (Core) - -> (p102) gl_VertexID holds the integer index i implicitly passed by DrawArrays or -> one of the other drawing commands defined in section 2.8.3. -> gl_InstanceID holds the integer index of the current primitive in an -> instanced draw call (see section 2.8.3). - - -# ES 3.1 - -ES 3.1 retains the wording from ES 3.0, but adds the following clarifications: - -gl_VertexID: -> (p252) The index of any element transferred to the GL by DrawArraysOneInstance -> is referred to as its vertex ID, and may be read by a vertex shader as gl_VertexID. -> The vertex ID of the ith element transferred is first + i. - -> (p254) The index of any element transferred to the GL by -> DrawElementsOneInstance is referred to as its vertex ID, and may be read by a vertex shader as -> gl_VertexID. If no element array buffer is bound, the vertex ID of the ith element -> transferred is indices[i] + basevertex. Otherwise, the vertex ID of the ith -> element transferred is the sum of basevertex and the value stored in the currently -> bound element array buffer at offset indices + i. - -gl_InstanceID -> (p255) If an enabled vertex attribute array is instanced (it has a non-zero divisor as -> specified by VertexAttribDivisor), the element index that is transferred to the GL, -> for all vertices, is given by -> `floor(instance / divisor) + baseinstance` - - -# Errata - -Drivers generally do implement the ES 3.1 behavior. -A notable exception is Mac's legacy GL (4.1) driver which has two bugs here. -(Both ANGLE-on-Metal and the system M1+ GL driver seem correct though) - -## gl_InstanceID random for DrawArrays calls -Use ERRATA.IGNORE_GL_INSTANCE_ID to cause these tests to pass. - -## Adds `first` to user-attrib instanced fetch ids in DrawArrays calls. -Use ERRATA.FIRST_ADDS_TO_INSTANCE to cause these tests to pass. -*/ - -const wtu = WebGLTestUtils; -const gl = wtu.create3DContext('e_canvas'); - -const ERRATA = {}; -//ERRATA.IGNORE_GL_INSTANCE_ID = true; // Chrome on ANGLE-on-Mac-GL needs this. -//ERRATA.FIRST_ADDS_TO_INSTANCE = true; // Firefox with MOZ_WEBGL_WORKAROUND_FIRST_AFFECTS_INSTANCE_ID=0 would need this. - -debug(`ERRATA: ${JSON.stringify(ERRATA)}`); - -function make_vs_point(vid, iid) { - return `\ - #version 300 es - - ${vid.name == 'gl_VertexID' ? '// ' : ''}layout(location=${vid.loc}) in highp int ${vid.name}; - ${iid.name == 'gl_InstanceID' ? '// ' :''}layout(location=${iid.loc}) in highp int ${iid.name}; - out vec4 v_color; - - void main() { - gl_PointSize = 1.0; - gl_Position = vec4(0.0, 0.0, 0.0, 1.0); - v_color = vec4(1.0, float(${vid.name}) / 255.0, float(${iid.name}) / 255.0, 1.0); -#if ${(iid.name == 'gl_InstanceID' && ERRATA.IGNORE_GL_INSTANCE_ID)|0} - v_color.b = 0.0; -#endif - }`; -} - -function make_vs_tri(vid, iid) { - return `\ - #version 300 es - - ${vid.name == 'gl_VertexID' ? '// ' : ''}layout(location=${vid.loc}) in highp int ${vid.name}; - ${iid.name == 'gl_InstanceID' ? '// ' :''}layout(location=${iid.loc}) in highp int ${iid.name}; - out vec4 v_color; - - void main() { - int prim_vert_id = ${vid.name} % 3; - int flat_vert_id = ${vid.name} - prim_vert_id + 2; - gl_Position = vec4(0.0, 0.0, 0.0, 1.0); - gl_Position.x = (prim_vert_id == 1) ? 2.0 : -1.0; - gl_Position.y = (prim_vert_id == 2) ? 2.0 : -1.0; - v_color = vec4(1.0, float(flat_vert_id) / 255.0, float(${iid.name}) / 255.0, 1.0); -#if ${(iid.name == 'gl_InstanceID' && ERRATA.IGNORE_GL_INSTANCE_ID)|0} - v_color.b = 0.0; -#endif - }`; -} - -const FS = `\ - #version 300 es - precision mediump float; - - in vec4 v_color; - out vec4 o_color; - - void main() { - o_color = v_color; - } -`; - - -function crossCombine(...args) { - function crossCombine2(listA, listB) { - const listC = []; - for (const a of listA) { - for (const b of listB) { - const c = Object.assign({}, a, b); - listC.push(c); - } - } - return listC; - } - - let res = [{}]; - while (args.length) { - const next = args.shift(); - next[0].defined; - res = crossCombine2(res, next); - } - return res; -} - -/// makeCombiner('foo', [5, 3]) -> [{foo: 5}, {foo: 3}] -function makeCombiner(key, vals) { - const ret = []; - for (const val of vals) { - const cur = {}; - cur[key] = val; - ret.push(cur); - } - return ret; -} - -debug('Draw a point with a shader that takes no attributes and verify it fills the whole canvas.'); - - -let TESTS = [ - makeCombiner('vid', [ - {name: 'a_VertexID', loc:0}, - {name: 'a_VertexID', loc:2}, // Test 2, so that we're not only testing 0. - {name: 'gl_VertexID', loc:-1}, - {name: 'gl_VertexID', loc:0}, // Enable a vertex array, despite not using it. - {name: 'gl_VertexID', loc:2}, // Enable a vertex array, despite not using it. - ]), - makeCombiner('iid', [ - {name: 'a_InstanceID', loc:1}, - {name: 'gl_InstanceID', loc:-1}, - {name: 'gl_InstanceID', loc:1}, // Enable a vertex array, despite not using it. - ]), - makeCombiner('separate_vbufs', [true, false]), -]; -//console.log('a', {TESTS}); -TESTS = crossCombine(...TESTS); -//console.log('b', {TESTS}); - - -let vdata = new Int32Array(1000); -vdata = vdata.map((v,i) => i); -const vbuf = gl.createBuffer(); -gl.bindBuffer(gl.ARRAY_BUFFER, vbuf); -gl.bufferData(gl.ARRAY_BUFFER, vdata, gl.STATIC_DRAW); - - -const vbuf2 = gl.createBuffer(); -gl.bindBuffer(gl.ARRAY_BUFFER, vbuf2); -gl.bufferData(gl.ARRAY_BUFFER, vdata, gl.STATIC_DRAW); - - -let index_data = new Uint32Array(1000); -index_data = index_data.map((x,i) => 10+i); -const index_buffer = gl.createBuffer(); -gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index_buffer); -gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, index_data, gl.STATIC_DRAW); - - -gl.disable(gl.DEPTH_TEST); - -(async () => { - for (const desc of TESTS) { - await wtu.dispatchPromise(); // Yield, for responsiveness. - debug(''); - debug('---------------------'); - debug(`desc: ${JSON.stringify(desc)}`); - - let fn = (vs) => { - //console.log({vs}); - const prog = wtu.setupProgram(gl, [vs, FS]); - - { - const WEBGL_debug_shaders = gl.getExtension('WEBGL_debug_shaders'); - let i = -1; - for (const s of gl.getAttachedShaders(prog)) { - i += 1; - debug(''); - debug(`shader[${i}] getShaderSource() -> `); - debug(gl.getShaderSource(s)); - if (WEBGL_debug_shaders) { - debug(`shader[${i}] getTranslatedShaderSource() -> `); - debug(WEBGL_debug_shaders.getTranslatedShaderSource(s)); - } - } - } - return prog; - }; - const point_prog = fn(make_vs_point(desc.vid, desc.iid)); - const tri_prog = fn(make_vs_tri(desc.vid, desc.iid)); - - // - - - gl.bindBuffer(gl.ARRAY_BUFFER, null); - for (let i = 0; i <= 2; i++) { - gl.disableVertexAttribArray(i); - gl.vertexAttribPointer(i, 4, gl.FLOAT, false, 0, 0); - gl.vertexAttribDivisor(i, 0); - } - - gl.bindBuffer(gl.ARRAY_BUFFER, vbuf); - let loc = desc.vid.loc; - if (loc != -1) { - gl.enableVertexAttribArray(loc); - gl.vertexAttribIPointer(loc, 1, gl.INT, 0, 0); - }; - - if (desc.separate_vbufs) { - gl.bindBuffer(gl.ARRAY_BUFFER, vbuf2); - } - loc = desc.iid.loc; - if (loc != -1) { - gl.enableVertexAttribArray(loc); - gl.vertexAttribIPointer(loc, 1, gl.INT, 0, 0); - gl.vertexAttribDivisor(loc, 1); - }; - - { - const err = gl.getError(); - if (err) throw err; // Broken init. - } - - // - - - fn = (eval_str, expected_arr) => { - if (ERRATA.IGNORE_GL_INSTANCE_ID) { - if (desc.iid.name == 'gl_InstanceID') { - expected_arr = expected_arr.map(x => x); - expected_arr[2] = 0; - } - } - - debug(''); - //debug(`${eval_str} -> [${expected_arr.join(', ')}]`); - eval(eval_str); - - const err = gl.getError(); - if (err) throw err; // Broken subtest. - - wtu.checkCanvas(gl, expected_arr, eval_str); - } - - gl.useProgram(point_prog); - - gl.clear(gl.COLOR_BUFFER_BIT); - fn(`gl.drawArrays(gl.POINTS, 0, 0)`, [0, 0, 0, 0]); - fn(`gl.drawArrays(gl.POINTS, 0, 1)`, [255, 0, 0, 255]); - fn(`gl.drawArrays(gl.POINTS, 0, 2)`, [255, 1, 0, 255]); - if (ERRATA.FIRST_ADDS_TO_INSTANCE) { - fn(`gl.drawArrays(gl.POINTS, 100, 2)`, [255, 100+2-1, 100, 255]); - } else { - fn(`gl.drawArrays(gl.POINTS, 100, 2)`, [255, 100+2-1, 0, 255]); - } - fn(`gl.drawArrays(gl.POINTS, 0, 255)`, [255, 254, 0, 255]); - fn(`gl.drawArrays(gl.POINTS, 0, 256)`, [255, 255, 0, 255]); - - gl.clear(gl.COLOR_BUFFER_BIT); - fn(`gl.drawArraysInstanced(gl.POINTS, 0, 0, 1)`, [0, 0, 0, 0]); - gl.clear(gl.COLOR_BUFFER_BIT); - fn(`gl.drawArraysInstanced(gl.POINTS, 0, 1, 0)`, [0, 0, 0, 0]); - - fn(`gl.drawArraysInstanced(gl.POINTS, 0, 1, 1)`, [255, 0, 0, 255]); - fn(`gl.drawArraysInstanced(gl.POINTS, 0, 2, 1)`, [255, 1, 0, 255]); - fn(`gl.drawArraysInstanced(gl.POINTS, 0, 1, 2)`, [255, 0, 1, 255]); - fn(`gl.drawArraysInstanced(gl.POINTS, 0, 2, 2)`, [255, 1, 1, 255]); - if (ERRATA.FIRST_ADDS_TO_INSTANCE) { - fn(`gl.drawArraysInstanced(gl.POINTS, 100, 2, 2)`, [255, 100+2-1, 101, 255]); - } else { - fn(`gl.drawArraysInstanced(gl.POINTS, 100, 2, 2)`, [255, 100+2-1, 1, 255]); - } - fn(`gl.drawArraysInstanced(gl.POINTS, 0, 255, 255)`, [255, 254, 254, 255]); - - // - - - gl.clear(gl.COLOR_BUFFER_BIT); - fn(`gl.drawElements(gl.POINTS, 0, gl.UNSIGNED_INT, 4*0)`, [0, 0, 0, 0]); - fn(`gl.drawElements(gl.POINTS, 1, gl.UNSIGNED_INT, 4*0)`, [255, 10+0, 0, 255]); - fn(`gl.drawElements(gl.POINTS, 2, gl.UNSIGNED_INT, 4*0)`, [255, 10+1, 0, 255]); - fn(`gl.drawElements(gl.POINTS, 2, gl.UNSIGNED_INT, 4*100)`, [255, 100+10+1, 0, 255]); - fn(`gl.drawElements(gl.POINTS, 245, gl.UNSIGNED_INT, 4*0)`, [255, 10+244, 0, 255]); - fn(`gl.drawElements(gl.POINTS, 246, gl.UNSIGNED_INT, 4*0)`, [255, 10+245, 0, 255]); - - gl.clear(gl.COLOR_BUFFER_BIT); - fn(`gl.drawElementsInstanced(gl.POINTS, 0, gl.UNSIGNED_INT, 4*0, 1)`, [0, 0, 0, 0]); - gl.clear(gl.COLOR_BUFFER_BIT); - fn(`gl.drawElementsInstanced(gl.POINTS, 1, gl.UNSIGNED_INT, 4*0, 0)`, [0, 0, 0, 0]); - - fn(`gl.drawElementsInstanced(gl.POINTS, 1, gl.UNSIGNED_INT, 4*0, 1)`, [255, 10+0, 0, 255]); - fn(`gl.drawElementsInstanced(gl.POINTS, 2, gl.UNSIGNED_INT, 4*0, 1)`, [255, 10+1, 0, 255]); - fn(`gl.drawElementsInstanced(gl.POINTS, 1, gl.UNSIGNED_INT, 4*0, 2)`, [255, 10+0, 1, 255]); - fn(`gl.drawElementsInstanced(gl.POINTS, 2, gl.UNSIGNED_INT, 4*0, 2)`, [255, 10+1, 1, 255]); - fn(`gl.drawElementsInstanced(gl.POINTS, 2, gl.UNSIGNED_INT, 4*100, 2)`, [255, 100+10+1, 1, 255]); - fn(`gl.drawElementsInstanced(gl.POINTS, 245, gl.UNSIGNED_INT, 4*0, 255)`, [255, 10+244, 254, 255]); - - // - - - gl.useProgram(tri_prog); - - gl.clear(gl.COLOR_BUFFER_BIT); - fn(`gl.drawArrays(gl.TRIANGLES, 0, 0*3)`, [0, 0, 0, 0]); - fn(`gl.drawArrays(gl.TRIANGLES, 0, 1*3)`, [255, 1*3-1, 0, 255]); - fn(`gl.drawArrays(gl.TRIANGLES, 0, 2*3)`, [255, 2*3-1, 0, 255]); - if (ERRATA.FIRST_ADDS_TO_INSTANCE) { - fn(`gl.drawArrays(gl.TRIANGLES, 90, 2*3)`, [255, 90+2*3-1, 90, 255]); - } else { - fn(`gl.drawArrays(gl.TRIANGLES, 90, 2*3)`, [255, 90+2*3-1, 0, 255]); - } - - gl.clear(gl.COLOR_BUFFER_BIT); - fn(`gl.drawArraysInstanced(gl.TRIANGLES, 0, 0, 1)`, [0, 0, 0, 0]); - gl.clear(gl.COLOR_BUFFER_BIT); - fn(`gl.drawArraysInstanced(gl.TRIANGLES, 0, 1*3, 0)`, [0, 0, 0, 0]); - - fn(`gl.drawArraysInstanced(gl.TRIANGLES, 0, 1*3, 1)`, [255, 1*3-1, 0, 255]); - fn(`gl.drawArraysInstanced(gl.TRIANGLES, 0, 2*3, 1)`, [255, 2*3-1, 0, 255]); - fn(`gl.drawArraysInstanced(gl.TRIANGLES, 0, 1*3, 2)`, [255, 1*3-1, 1, 255]); - fn(`gl.drawArraysInstanced(gl.TRIANGLES, 0, 2*3, 2)`, [255, 2*3-1, 1, 255]); - if (ERRATA.FIRST_ADDS_TO_INSTANCE) { - fn(`gl.drawArraysInstanced(gl.TRIANGLES, 90, 2*3, 2)`, [255, 90+2*3-1, 91, 255]); - } else { - fn(`gl.drawArraysInstanced(gl.TRIANGLES, 90, 2*3, 2)`, [255, 90+2*3-1, 1, 255]); - } - - // - - - gl.clear(gl.COLOR_BUFFER_BIT); - fn(`gl.drawElements(gl.TRIANGLES, 0*3, gl.UNSIGNED_INT, 4*0)`, [0, 0, 0, 0]); - fn(`gl.drawElements(gl.TRIANGLES, 1*3, gl.UNSIGNED_INT, 4*0)`, [255, 10+1*3-1, 0, 255]); - fn(`gl.drawElements(gl.TRIANGLES, 2*3, gl.UNSIGNED_INT, 4*0)`, [255, 10+2*3-1, 0, 255]); - fn(`gl.drawElements(gl.TRIANGLES, 2*3, gl.UNSIGNED_INT, 4*100)`, [255, 100+10+2*3-1, 0, 255]); - - gl.clear(gl.COLOR_BUFFER_BIT); - fn(`gl.drawElementsInstanced(gl.TRIANGLES, 0*3, gl.UNSIGNED_INT, 4*0, 1)`, [0, 0, 0, 0]); - gl.clear(gl.COLOR_BUFFER_BIT); - fn(`gl.drawElementsInstanced(gl.TRIANGLES, 1*3, gl.UNSIGNED_INT, 4*0, 0)`, [0, 0, 0, 0]); - - fn(`gl.drawElementsInstanced(gl.TRIANGLES, 1*3, gl.UNSIGNED_INT, 4*0, 1)`, [255, 10+1*3-1, 0, 255]); - fn(`gl.drawElementsInstanced(gl.TRIANGLES, 2*3, gl.UNSIGNED_INT, 4*0, 1)`, [255, 10+2*3-1, 0, 255]); - fn(`gl.drawElementsInstanced(gl.TRIANGLES, 1*3, gl.UNSIGNED_INT, 4*0, 2)`, [255, 10+1*3-1, 1, 255]); - fn(`gl.drawElementsInstanced(gl.TRIANGLES, 2*3, gl.UNSIGNED_INT, 4*0, 2)`, [255, 10+2*3-1, 1, 255]); - fn(`gl.drawElementsInstanced(gl.TRIANGLES, 2*3, gl.UNSIGNED_INT, 4*100, 2)`, [255, 100+10+2*3-1, 1, 255]); - } - - finishTest(); -})(); - -var successfullyParsed = true; -</script> -</body> -</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/00_test_list.txt b/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/00_test_list.txt index 17f8312e58..3639c10547 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/00_test_list.txt +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/00_test_list.txt @@ -20,6 +20,7 @@ mipmap-fbo.html --min-version 2.0.1 origin-clean-conformance-offscreencanvas.html tex-3d-size-limit.html --min-version 2.0.1 tex-base-level-bug.html +--min-version 2.0.1 tex-image-10bpc.html tex-image-and-sub-image-with-array-buffer-view-sub-source.html tex-image-with-bad-args.html tex-image-with-bad-args-from-dom-elements.html diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/tex-image-10bpc.html b/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/tex-image-10bpc.html new file mode 100644 index 0000000000..973a0ec0f5 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/tex-image-10bpc.html @@ -0,0 +1,66 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> + +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8" /> +<title>Ensure 10bpc image is not crushed to 8bpc in texImage2D</title> +<link rel="stylesheet" href="../../../resources/js-test-style.css" /> +<script src="../../../js/js-test-pre.js"></script> +<script src="../../../js/webgl-test-utils.js"></script> +</head> +<body> +<canvas id="example" width="24" height="24"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description(document.title); +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext("example", undefined, 2); +var uniquePixels; + +// This is an 8x1, 10-bit-per-channel PNG (encoded as 16bpc). +// The first pixel is black, and each next pixel is one brighter; approximately: +// (0/1023,0,0), (1/1023,0,0), (2/1023,0,0), ..., (7/1023,0,0) +const imgW = 8, imgH = 1; +const imgURL = "../../../resources/red-gradient-8x1-10bit-untagged.png"; + +const img = document.createElement("img"); +img.onload = () => { + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + const level = 0; + const internalformat = gl.RGB10_A2; + const border = 0; + const format = gl.RGBA; + const type = gl.UNSIGNED_INT_2_10_10_10_REV; + gl.texImage2D(gl.TEXTURE_2D, level, internalformat, imgW, imgH, border, format, type, img); + + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); + + shouldBe("gl.checkFramebufferStatus(gl.FRAMEBUFFER)", "gl.FRAMEBUFFER_COMPLETE"); + + const pixels = new Uint32Array(imgW * imgH); + gl.readPixels(0, 0, imgW, imgH, format, type, pixels); + uniquePixels = new Set(pixels); + // If the image was crushed to 8bpc, there will be 2-3 distinct values: + // (0/255,0,0), (1/255,0,0), and maybe (2/255,0,0) (depending on truncation vs rounding). + // If it wasn't, there should be 7-8. + // At time of writing, on Mac M1, Chrome gets 2 if it's crushed, and 7 if it's not. + shouldBeGreaterThanOrEqual("uniquePixels.size", "7", "there should be at least 7 distinct color values"); + + finishTest(); +}; +img.src = imgURL; + +var successfullyParsed = true; +</script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/tex-image-with-bad-args-from-dom-elements.html b/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/tex-image-with-bad-args-from-dom-elements.html index f1616e81d7..34ece05699 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/tex-image-with-bad-args-from-dom-elements.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/tex-image-with-bad-args-from-dom-elements.html @@ -106,7 +106,6 @@ var tests = [ { type: "video", src: "../../../resources/red-green.mp4", videoType: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', run: testVideo }, { type: "video", src: "../../../resources/red-green.bt601.vp9.webm", videoType: 'video/webm; codecs="vp9"', run: testVideo }, { type: "video", src: "../../../resources/red-green.webmvp8.webm", videoType: 'video/webm; codecs="vp8, vorbis"', run: testVideo }, - { type: "video", src: "../../../resources/red-green.theora.ogv", videoType: 'video/ogg; codecs="theora, vorbis"', run: testVideo }, ]; var testIndex = 0; diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/tex-new-formats.html b/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/tex-new-formats.html index df10edb4d3..760bc6bd19 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/tex-new-formats.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/textures/misc/tex-new-formats.html @@ -37,6 +37,7 @@ if (!gl) { testPassed("WebGL context exists"); runTexFormatsTest(); + runDepthStencilFormatTest(); wtu.glErrorShouldBe(gl, gl.NO_ERROR, "there should be no errors"); } @@ -556,6 +557,35 @@ function runTexFormatsTest() }); } +function runDepthStencilFormatTest() { + debug(""); + debug("Testing FLOAT_32_UNSIGNED_INT_24_8_REV with data"); + const fb = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fb); + + const tex2D = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex2D); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH32F_STENCIL8, 1, 1, 0, gl.DEPTH_STENCIL, gl.FLOAT_32_UNSIGNED_INT_24_8_REV, new Uint8Array(8)); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.TEXTURE_2D, tex2D, 0); + if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE) { + testFailed("2D texture with invalid type was created"); + } else { + testPassed("2D texture with invalid type was not created") + } + + const tex3D = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D_ARRAY, tex3D); + gl.texImage3D(gl.TEXTURE_2D_ARRAY, 0, gl.DEPTH32F_STENCIL8, 1, 1, 1, 0, gl.DEPTH_STENCIL, gl.FLOAT_32_UNSIGNED_INT_24_8_REV, new Uint8Array(8)); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION); + gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, tex3D, 0, 0); + if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE) { + testFailed("2D array texture with invalid type was created"); + } else { + testPassed("2D array texture with invalid type was not created") + } +} + debug(""); var successfullyParsed = true; </script> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/transform_feedback/simultaneous_binding.html b/dom/canvas/test/webgl-conf/checkout/conformance2/transform_feedback/simultaneous_binding.html index 228b4ab5cf..11d1eaa829 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/transform_feedback/simultaneous_binding.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/transform_feedback/simultaneous_binding.html @@ -304,6 +304,17 @@ for (let genericBindPointValue of genericBindPointValues) { gl.copyBufferSubData(gl.COPY_READ_BUFFER, gl.COPY_WRITE_BUFFER, 0, 0, 1); wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "copyBufferSubData with double bound buffer"); + debug("<hr/>Test that rejected operations do not change the bound buffer size"); + + gl.bindBuffer(gl.ARRAY_BUFFER, tfBuffer); + gl.bufferData(gl.ARRAY_BUFFER, 8, gl.STATIC_DRAW); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "bufferData with double bound buffer"); + + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Uint8Array(16)); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "bufferSubData should succeed"); + gl.bindBuffer(gl.ARRAY_BUFFER, null); + debug("<hr/>Test bufferData family with tf object unbound"); gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null); diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/transform_feedback/transform_feedback.html b/dom/canvas/test/webgl-conf/checkout/conformance2/transform_feedback/transform_feedback.html index 20256c6ace..16855453f0 100644 --- a/dom/canvas/test/webgl-conf/checkout/conformance2/transform_feedback/transform_feedback.html +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/transform_feedback/transform_feedback.html @@ -64,6 +64,8 @@ if (!gl) { runUnboundDeleteTest(); runBoundDeleteTest(); runOneOutputFeedbackTest(); + runUnchangedBufferBindingsTest(); + runNoOutputsTest(); // Must be the last test, since it's asynchronous and calls finishTest(). runTwoOutputFeedbackTest(); } @@ -638,6 +640,70 @@ function runContextLostOneOutputFeedbackTest() { finishTest(); } +function runUnchangedBufferBindingsTest() { + debug(""); + debug("Testing that buffer bindings cannot be changed while transform feedback is active"); + + program = wtu.setupTransformFeedbackProgram( + gl, [wtu.simpleVertexShader, wtu.simpleColorFragmentShader], ['gl_Position'], gl.INTERLEAVED_ATTRIBS); + + tf = gl.createTransformFeedback(); + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf); + + buf = gl.createBuffer(); + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf); + + gl.beginTransformFeedback(gl.POINTS); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Transform feedback is active"); + + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, gl.createBuffer()); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Cannot change the bound buffer while transform feedback is active"); + shouldBe("gl.getIndexedParameter(gl.TRANSFORM_FEEDBACK_BUFFER_BINDING, 0)", "buf"); + + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Cannot rebind the same buffer while transform feedback is active"); + + gl.bindBufferRange(gl.TRANSFORM_FEEDBACK_BUFFER, 0, gl.createBuffer(), 0, 64); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Cannot change the bound buffer while transform feedback is active"); + shouldBe("gl.getIndexedParameter(gl.TRANSFORM_FEEDBACK_BUFFER_BINDING, 0)", "buf"); + + gl.bindBufferRange(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf, 0, 64); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Cannot rebind the same buffer while transform feedback is active"); + + gl.endTransformFeedback(); + gl.deleteTransformFeedback(tf); + gl.deleteBuffer(buf); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "No extra errors after the test"); +} + +function runNoOutputsTest() { + debug(""); + debug("Testing transform feedback with no varyings to record"); + + tf = gl.createTransformFeedback(); + gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf); + + buf = gl.createBuffer(); + gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf); + + for (const mode of ['SEPARATE_ATTRIBS', 'INTERLEAVED_ATTRIBS']) { + program = wtu.setupTransformFeedbackProgram( + gl, [wtu.simpleVertexShader, wtu.simpleColorFragmentShader], [], gl[mode]); + + debug(`Testing with ${mode}`); + gl.beginTransformFeedback(gl.POINTS); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "beginTransformFeedback: No varyings to record"); + gl.useProgram(null); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "useProgram: Transform feedback is not active"); + gl.endTransformFeedback(); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "endTransformFeedback: Transform feedback is not active"); + } + + gl.deleteTransformFeedback(tf); + gl.deleteBuffer(buf); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "No extra errors after the test"); +} + debug(""); </script> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/00_test_list.txt b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/00_test_list.txt new file mode 100644 index 0000000000..d188fc30a6 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/00_test_list.txt @@ -0,0 +1,12 @@ +--min-version 2.0.1 readpixels-16gb-wasm-memory.html +--min-version 2.0.1 readpixels-4gb-wasm-memory.html +--min-version 2.0.1 teximage2d-16gb-wasm-memory.html +--min-version 2.0.1 teximage2d-4gb-wasm-memory.html +--min-version 2.0.1 texsubimage2d-16gb-wasm-memory.html +--min-version 2.0.1 texsubimage2d-4gb-wasm-memory.html +--min-version 2.0.1 bufferdata-16gb-wasm-memory.html +--min-version 2.0.1 bufferdata-4gb-wasm-memory.html +--min-version 2.0.1 buffersubdata-16gb-wasm-memory.html +--min-version 2.0.1 buffersubdata-4gb-wasm-memory.html +--min-version 2.0.1 getbuffersubdata-16gb-wasm-memory.html +--min-version 2.0.1 getbuffersubdata-4gb-wasm-memory.html
\ No newline at end of file diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/bufferdata-16gb-wasm-memory.html b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/bufferdata-16gb-wasm-memory.html new file mode 100644 index 0000000000..0dd21bf64f --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/bufferdata-16gb-wasm-memory.html @@ -0,0 +1,51 @@ +<!-- +Copyright (c) 2024 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>bufferData test to Wasm Memory 16GB in size.</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"> </script> +</head> +<body> +<canvas id="canvas" width="2" height="2" style="width: 40px; height: 40px;"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description(document.title); +debug("Tests that bufferData can be called on WebAssembly Memory of 16GB in size."); +debug(""); +let wtu = WebGLTestUtils; +let gl = wtu.create3DContext("canvas", undefined, 2); + +const PAGE = 65536; +const SIZE = 16 * 1024 * 1024 * 1024; +let view = new Uint8Array(new WebAssembly.Memory({ index: 'i64', initial: SIZE / PAGE }).buffer); +let expectedData = new Uint8Array([1, 2, 3, 4]); +const length = expectedData.length; +const offset = SIZE - length; +view.set(expectedData, offset); + +let buf = gl.createBuffer(); +gl.bindBuffer(gl.ARRAY_BUFFER, buf); +gl.bufferData(gl.ARRAY_BUFFER, view, gl.STATIC_DRAW, offset, length); +wtu.glErrorShouldBe(gl, gl.NO_ERROR); + +let actualData = new Uint8Array(length); +gl.getBufferSubData(gl.ARRAY_BUFFER, 0, actualData); +wtu.glErrorShouldBe(gl, gl.NO_ERROR); +for (let i = 0; i < length; i++) { + shouldBe(`actualData[${i}]`, `expectedData[${i}]`); +} + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/bufferdata-4gb-wasm-memory.html b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/bufferdata-4gb-wasm-memory.html new file mode 100644 index 0000000000..2296c3a5ea --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/bufferdata-4gb-wasm-memory.html @@ -0,0 +1,51 @@ +<!-- +Copyright (c) 2024 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>bufferData test to Wasm Memory 4GB in size.</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"> </script> +</head> +<body> +<canvas id="canvas" width="2" height="2" style="width: 40px; height: 40px;"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description(document.title); +debug("Tests that bufferData can be called on WebAssembly Memory of 4GB in size."); +debug(""); +let wtu = WebGLTestUtils; +let gl = wtu.create3DContext("canvas", undefined, 2); + +const PAGE = 65536; +const SIZE = 4 * 1024 * 1024 * 1024 - PAGE; +let view = new Uint8Array(new WebAssembly.Memory({ initial: SIZE / PAGE }).buffer); +let expectedData = new Uint8Array([1, 2, 3, 4]); +const length = expectedData.length; +const offset = SIZE - length; +view.set(expectedData, offset); + +let buf = gl.createBuffer(); +gl.bindBuffer(gl.ARRAY_BUFFER, buf); +gl.bufferData(gl.ARRAY_BUFFER, view, gl.STATIC_DRAW, offset, length); +wtu.glErrorShouldBe(gl, gl.NO_ERROR); + +let actualData = new Uint8Array(length); +gl.getBufferSubData(gl.ARRAY_BUFFER, 0, actualData); +wtu.glErrorShouldBe(gl, gl.NO_ERROR); +for (let i = 0; i < length; i++) { + shouldBe(`actualData[${i}]`, `expectedData[${i}]`); +} + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/buffersubdata-16gb-wasm-memory.html b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/buffersubdata-16gb-wasm-memory.html new file mode 100644 index 0000000000..08d6d1df50 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/buffersubdata-16gb-wasm-memory.html @@ -0,0 +1,52 @@ +<!-- +Copyright (c) 2024 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>bufferSubData test to Wasm Memory 16GB in size.</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"> </script> +</head> +<body> +<canvas id="canvas" width="2" height="2" style="width: 40px; height: 40px;"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description(document.title); +debug("Tests that bufferSubData can be called on WebAssembly Memory of 16GB in size."); +debug(""); +let wtu = WebGLTestUtils; +let gl = wtu.create3DContext("canvas", undefined, 2); + +const PAGE = 65536; +const SIZE = 16 * 1024 * 1024 * 1024; +let view = new Uint8Array(new WebAssembly.Memory({ index: 'i64', initial: SIZE / PAGE }).buffer); +let expectedData = new Uint8Array([1, 2]); +const length = expectedData.length; +let srcOffset = SIZE - length; +view.set(expectedData, srcOffset); +const dstByteOffset = 4; + +let buf = gl.createBuffer(); +gl.bindBuffer(gl.ARRAY_BUFFER, buf); +gl.bufferData(gl.ARRAY_BUFFER, 8, gl.STATIC_DRAW); +gl.bufferSubData(gl.ARRAY_BUFFER, dstByteOffset, view, srcOffset, length); +wtu.glErrorShouldBe(gl, gl.NO_ERROR); + +let actualData = new Uint8Array(length); +gl.getBufferSubData(gl.ARRAY_BUFFER, dstByteOffset, actualData); +for (let i = 0; i < length; i++) { + shouldBe(`actualData[${i}]`, `expectedData[${i}]`); +} + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/buffersubdata-4gb-wasm-memory.html b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/buffersubdata-4gb-wasm-memory.html new file mode 100644 index 0000000000..2834a6901b --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/buffersubdata-4gb-wasm-memory.html @@ -0,0 +1,52 @@ +<!-- +Copyright (c) 2024 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>bufferSubData test to Wasm Memory 4GB in size.</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"> </script> +</head> +<body> +<canvas id="canvas" width="2" height="2" style="width: 40px; height: 40px;"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description(document.title); +debug("Tests that bufferSubData can be called on WebAssembly Memory of 4GB in size."); +debug(""); +let wtu = WebGLTestUtils; +let gl = wtu.create3DContext("canvas", undefined, 2); + +const PAGE = 65536; +const SIZE = 4 * 1024 * 1024 * 1024 - PAGE; +let view = new Uint8Array(new WebAssembly.Memory({ initial: SIZE / PAGE }).buffer); +let expectedData = new Uint8Array([1, 2]); +const length = expectedData.length; +let srcOffset = SIZE - length; +view.set(expectedData, srcOffset); +const dstByteOffset = 4; + +let buf = gl.createBuffer(); +gl.bindBuffer(gl.ARRAY_BUFFER, buf); +gl.bufferData(gl.ARRAY_BUFFER, 8, gl.STATIC_DRAW); +gl.bufferSubData(gl.ARRAY_BUFFER, dstByteOffset, view, srcOffset, length); +wtu.glErrorShouldBe(gl, gl.NO_ERROR); + +let actualData = new Uint8Array(length); +gl.getBufferSubData(gl.ARRAY_BUFFER, dstByteOffset, actualData); +for (let i = 0; i < length; i++) { + shouldBe(`actualData[${i}]`, `expectedData[${i}]`); +} + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/getbuffersubdata-16gb-wasm-memory.html b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/getbuffersubdata-16gb-wasm-memory.html new file mode 100644 index 0000000000..8390957b02 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/getbuffersubdata-16gb-wasm-memory.html @@ -0,0 +1,48 @@ +<!-- +Copyright (c) 2024 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>getBufferSubData test to Wasm Memory 16GB in size.</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"> </script> +</head> +<body> +<canvas id="canvas" width="2" height="2" style="width: 40px; height: 40px;"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description(document.title); +debug("Tests that getBufferSubData can be called on WebAssembly Memory of 16GB in size."); +debug(""); +let wtu = WebGLTestUtils; +let gl = wtu.create3DContext("canvas", undefined, 2); + +const PAGE = 65536; +const SIZE = 16 * 1024 * 1024 * 1024; +let view = new Uint8Array(new WebAssembly.Memory({ index: 'i64', initial: SIZE / PAGE }).buffer); +let expectedData = new Uint8Array([1, 2, 3, 4]); + +let buf = gl.createBuffer(); +gl.bindBuffer(gl.ARRAY_BUFFER, buf); +gl.bufferData(gl.ARRAY_BUFFER, expectedData, gl.STATIC_DRAW); + +const length = expectedData.length; +const offset = SIZE - length; +gl.getBufferSubData(gl.ARRAY_BUFFER, 0, view, offset, length); +wtu.glErrorShouldBe(gl, gl.NO_ERROR); +for (let i = 0; i < length; i++) { + shouldBe(`view[${i + offset}]`, `expectedData[${i}]`); +} + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/getbuffersubdata-4gb-wasm-memory.html b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/getbuffersubdata-4gb-wasm-memory.html new file mode 100644 index 0000000000..09a336b753 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/getbuffersubdata-4gb-wasm-memory.html @@ -0,0 +1,48 @@ +<!-- +Copyright (c) 2024 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>getBufferSubData test to Wasm Memory 4GB in size.</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"> </script> +</head> +<body> +<canvas id="canvas" width="2" height="2" style="width: 40px; height: 40px;"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description(document.title); +debug("Tests that getBufferSubData can be called on WebAssembly Memory of 4GB in size."); +debug(""); +let wtu = WebGLTestUtils; +let gl = wtu.create3DContext("canvas", undefined, 2); + +const PAGE = 65536; +const SIZE = 4 * 1024 * 1024 * 1024 - PAGE; +let view = new Uint8Array(new WebAssembly.Memory({ initial: SIZE / PAGE }).buffer); +let expectedData = new Uint8Array([1, 2, 3, 4]); + +let buf = gl.createBuffer(); +gl.bindBuffer(gl.ARRAY_BUFFER, buf); +gl.bufferData(gl.ARRAY_BUFFER, expectedData, gl.STATIC_DRAW); + +const length = expectedData.length; +const offset = SIZE - length; +gl.getBufferSubData(gl.ARRAY_BUFFER, 0, view, offset, length); +wtu.glErrorShouldBe(gl, gl.NO_ERROR); +for (let i = 0; i < length; i++) { + shouldBe(`view[${i + offset}]`, `expectedData[${i}]`); +} + +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/readpixels-16gb-wasm-memory.html b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/readpixels-16gb-wasm-memory.html new file mode 100644 index 0000000000..af74678dda --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/readpixels-16gb-wasm-memory.html @@ -0,0 +1,51 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>gl.readPixels() test to Wasm Memory 16GB in size.</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"> </script> +</head> +<body> +<canvas id="canvas" width="2" height="2" style="width: 40px; height: 40px;"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description(document.title); +debug(""); +debug("Tests that gl.readPixels() can be called on WebAssembly Memory of 16GB in size."); +debug(""); +let wtu = WebGLTestUtils; +let gl = wtu.create3DContext("canvas", undefined, 2); + +const PAGE = 65536; +const SIZE = 16*1024*1024*1024; +let view = new Uint8Array(new WebAssembly.Memory({ index: 'i64', initial: SIZE/PAGE }).buffer); + +// Clear the canvas to a specific color +const expectedColor = [42, 84, 128, 255]; +gl.clearColor(expectedColor[0]/255.0, expectedColor[1]/255.0, expectedColor[2]/255.0, expectedColor[3]/255.0); +gl.clear(gl.COLOR_BUFFER_BIT); + +// Test that gl.readPixels() can be called with a high offset to Memory +const offset = SIZE - 4; +view.set([0,0,0,0], offset); // For good measure, clear data at offset before reading +gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, view, offset); +wtu.glErrorShouldBe(gl, gl.NO_ERROR); +let obtainedColor = view.subarray(offset, offset+4); +shouldBe('obtainedColor[0]', 'expectedColor[0]'); +shouldBe('obtainedColor[1]', 'expectedColor[1]'); +shouldBe('obtainedColor[2]', 'expectedColor[2]'); +shouldBe('obtainedColor[3]', 'expectedColor[3]'); +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/readpixels-4gb-wasm-memory.html b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/readpixels-4gb-wasm-memory.html new file mode 100644 index 0000000000..f97a3ccba0 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/readpixels-4gb-wasm-memory.html @@ -0,0 +1,50 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>gl.readPixels() test to Wasm Memory 4GB in size.</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"> </script> +</head> +<body> +<canvas id="canvas" width="2" height="2" style="width: 40px; height: 40px;"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description(document.title); +debug("Tests that gl.readPixels() can be called on WebAssembly Memory of 4GB in size."); +debug(""); +let wtu = WebGLTestUtils; +let gl = wtu.create3DContext("canvas", undefined, 2); + +const PAGE = 65536; +const SIZE = 4*1024*1024*1024 - PAGE; // when uint32_t size is max, we can only reach one page short of full 4GB +let view = new Uint8Array(new WebAssembly.Memory({ initial: SIZE/PAGE }).buffer); + +// Clear the canvas to a specific color +const expectedColor = [42, 84, 128, 255]; +gl.clearColor(expectedColor[0]/255.0, expectedColor[1]/255.0, expectedColor[2]/255.0, expectedColor[3]/255.0); +gl.clear(gl.COLOR_BUFFER_BIT); + +// Test that gl.readPixels() can be called with a high offset to Memory +const offset = SIZE - 4; +view.set([0,0,0,0], offset); // For good measure, clear data at offset before reading +gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, view, offset); +wtu.glErrorShouldBe(gl, gl.NO_ERROR); +let obtainedColor = view.subarray(offset, offset+4); +shouldBe('obtainedColor[0]', 'expectedColor[0]'); +shouldBe('obtainedColor[1]', 'expectedColor[1]'); +shouldBe('obtainedColor[2]', 'expectedColor[2]'); +shouldBe('obtainedColor[3]', 'expectedColor[3]'); +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/teximage2d-16gb-wasm-memory.html b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/teximage2d-16gb-wasm-memory.html new file mode 100644 index 0000000000..8ce5111953 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/teximage2d-16gb-wasm-memory.html @@ -0,0 +1,85 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>gl.texImage2D() test to Wasm Memory 16GB in size.</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"> </script> +</head> +<body> +<canvas id="canvas" width="2" height="2" style="width: 40px; height: 40px;"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description(document.title); +debug("Tests that gl.texImage2D() can be called on WebAssembly Memory 16GB in size."); +debug(""); +let wtu = WebGLTestUtils; +let gl = wtu.create3DContext("canvas", undefined, 2); + +const PAGE = 65536; +const SIZE = 16*1024*1024*1024; +let view = new Uint8Array(new WebAssembly.Memory({ index: 'i64', initial: SIZE/PAGE }).buffer); + +function compileShader(type, src) { + let shader = gl.createShader(type); + gl.shaderSource(shader, src); + gl.compileShader(shader); + let log = gl.getShaderInfoLog(shader); + if (log) debug(log); + return shader; +} + +function createProgram(vs, fs) { + let program = gl.createProgram(); + gl.attachShader(program, vs); + gl.attachShader(program, fs); + gl.bindAttribLocation(program, 0, 'pos'); + gl.linkProgram(program); + gl.useProgram(program); + return program; +} + +let program = createProgram( + compileShader(gl.VERTEX_SHADER, ` + varying vec2 uv; + attribute vec2 pos; + void main() { uv = pos; gl_Position = vec4(pos*2.0-vec2(1.0,1.0),0,1); }`), + compileShader(gl.FRAGMENT_SHADER, ` + precision lowp float; + uniform sampler2D tex; + varying vec2 uv; + void main() { gl_FragColor = texture2D(tex,uv); }`)); + +gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); +gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), gl.STATIC_DRAW); +gl.vertexAttribPointer(0, 2, gl.FLOAT, gl.FALSE, 0, 0); +gl.enableVertexAttribArray(0); + +let texture = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_2D, texture); + +// Test uploading an image +const expectedColor = [42, 84, 128, 255]; +const offset = SIZE - 4; +view.set(expectedColor, offset); +gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, view, offset); +wtu.glErrorShouldBe(gl, gl.NO_ERROR); + +// Test rendering with that image +gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + +// Verify that we rendered what we expected +wtu.checkCanvasRect(gl, 0, 0, 1, 1, expectedColor, "texImage2D produced expected color"); +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/teximage2d-4gb-wasm-memory.html b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/teximage2d-4gb-wasm-memory.html new file mode 100644 index 0000000000..5d6897347d --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/teximage2d-4gb-wasm-memory.html @@ -0,0 +1,85 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>gl.texImage2D() test to Wasm Memory 4GB in size.</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"> </script> +</head> +<body> +<canvas id="canvas" width="2" height="2" style="width: 40px; height: 40px;"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description(document.title); +debug("Tests that gl.texImage2D() can be called on WebAssembly Memory 4GB in size."); +debug(""); +let wtu = WebGLTestUtils; +let gl = wtu.create3DContext("canvas", undefined, 2); + +const PAGE = 65536; +const SIZE = 4*1024*1024*1024 - PAGE; // when uint32_t size is max, we can only reach one page short of full 4GB +let view = new Uint8Array(new WebAssembly.Memory({ initial: SIZE/PAGE }).buffer); + +function compileShader(type, src) { + let shader = gl.createShader(type); + gl.shaderSource(shader, src); + gl.compileShader(shader); + let log = gl.getShaderInfoLog(shader); + if (log) debug(log); + return shader; +} + +function createProgram(vs, fs) { + let program = gl.createProgram(); + gl.attachShader(program, vs); + gl.attachShader(program, fs); + gl.bindAttribLocation(program, 0, 'pos'); + gl.linkProgram(program); + gl.useProgram(program); + return program; +} + +let program = createProgram( + compileShader(gl.VERTEX_SHADER, ` + varying vec2 uv; + attribute vec2 pos; + void main() { uv = pos; gl_Position = vec4(pos*2.0-vec2(1.0,1.0),0,1); }`), + compileShader(gl.FRAGMENT_SHADER, ` + precision lowp float; + uniform sampler2D tex; + varying vec2 uv; + void main() { gl_FragColor = texture2D(tex,uv); }`)); + +gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); +gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), gl.STATIC_DRAW); +gl.vertexAttribPointer(0, 2, gl.FLOAT, gl.FALSE, 0, 0); +gl.enableVertexAttribArray(0); + +let texture = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_2D, texture); + +// Test uploading an image +const expectedColor = [42, 84, 128, 255]; +const offset = SIZE - 4; +view.set(expectedColor, offset); +gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, view, offset); +wtu.glErrorShouldBe(gl, gl.NO_ERROR); + +// Test rendering with that image +gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + +// Verify that we rendered what we expected +wtu.checkCanvasRect(gl, 0, 0, 1, 1, expectedColor, "texImage2D produced expected color"); +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/texsubimage2d-16gb-wasm-memory.html b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/texsubimage2d-16gb-wasm-memory.html new file mode 100644 index 0000000000..328d42ec49 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/texsubimage2d-16gb-wasm-memory.html @@ -0,0 +1,86 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>gl.texSubImage2D() test to Wasm Memory 16GB in size.</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"> </script> +</head> +<body> +<canvas id="canvas" width="2" height="2" style="width: 40px; height: 40px;"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description(document.title); +debug("Tests that gl.texSubImage2D() can be called on WebAssembly Memory 16GB in size."); +debug(""); +let wtu = WebGLTestUtils; +let gl = wtu.create3DContext("canvas", undefined, 2); + +const PAGE = 65536; +const SIZE = 16*1024*1024*1024; +let view = new Uint8Array(new WebAssembly.Memory({ index: 'i64', initial: SIZE/PAGE }).buffer); + +function compileShader(type, src) { + let shader = gl.createShader(type); + gl.shaderSource(shader, src); + gl.compileShader(shader); + let log = gl.getShaderInfoLog(shader); + if (log) debug(log); + return shader; +} + +function createProgram(vs, fs) { + let program = gl.createProgram(); + gl.attachShader(program, vs); + gl.attachShader(program, fs); + gl.bindAttribLocation(program, 0, 'pos'); + gl.linkProgram(program); + gl.useProgram(program); + return program; +} + +let program = createProgram( + compileShader(gl.VERTEX_SHADER, ` + varying vec2 uv; + attribute vec2 pos; + void main() { uv = pos; gl_Position = vec4(pos*2.0-vec2(1.0,1.0),0,1); }`), + compileShader(gl.FRAGMENT_SHADER, ` + precision lowp float; + uniform sampler2D tex; + varying vec2 uv; + void main() { gl_FragColor = texture2D(tex,uv); }`)); + +gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); +gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), gl.STATIC_DRAW); +gl.vertexAttribPointer(0, 2, gl.FLOAT, gl.FALSE, 0, 0); +gl.enableVertexAttribArray(0); + +let texture = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_2D, texture); + +// Test uploading an image +const expectedColor = [42, 84, 128, 255]; +const offset = SIZE - 4; +view.set(expectedColor, offset); +gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, 1, 1); +gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, view, offset); +wtu.glErrorShouldBe(gl, gl.NO_ERROR); + +// Test rendering with that image +gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + +// Verify that we rendered what we expected +wtu.checkCanvasRect(gl, 0, 0, 1, 1, expectedColor, "texSubImage2D produced expected color"); +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/texsubimage2d-4gb-wasm-memory.html b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/texsubimage2d-4gb-wasm-memory.html new file mode 100644 index 0000000000..d7756629a0 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/conformance2/wasm/texsubimage2d-4gb-wasm-memory.html @@ -0,0 +1,86 @@ +<!-- +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +--> +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>gl.texSubImage2D() test to Wasm Memory 4GB in size.</title> +<link rel="stylesheet" href="../../resources/js-test-style.css"/> +<script src="../../js/js-test-pre.js"></script> +<script src="../../js/webgl-test-utils.js"> </script> +</head> +<body> +<canvas id="canvas" width="2" height="2" style="width: 40px; height: 40px;"></canvas> +<div id="description"></div> +<div id="console"></div> +<script> +"use strict"; +description(document.title); +debug("Tests that gl.texSubImage2D() can be called on WebAssembly Memory 4GB in size."); +debug(""); +let wtu = WebGLTestUtils; +let gl = wtu.create3DContext("canvas", undefined, 2); + +const PAGE = 65536; +const SIZE = 4*1024*1024*1024 - PAGE; // when uint32_t size is max, we can only reach one page short of full 4GB +let view = new Uint8Array(new WebAssembly.Memory({ initial: SIZE/PAGE }).buffer); + +function compileShader(type, src) { + let shader = gl.createShader(type); + gl.shaderSource(shader, src); + gl.compileShader(shader); + let log = gl.getShaderInfoLog(shader); + if (log) debug(log); + return shader; +} + +function createProgram(vs, fs) { + let program = gl.createProgram(); + gl.attachShader(program, vs); + gl.attachShader(program, fs); + gl.bindAttribLocation(program, 0, 'pos'); + gl.linkProgram(program); + gl.useProgram(program); + return program; +} + +let program = createProgram( + compileShader(gl.VERTEX_SHADER, ` + varying vec2 uv; + attribute vec2 pos; + void main() { uv = pos; gl_Position = vec4(pos*2.0-vec2(1.0,1.0),0,1); }`), + compileShader(gl.FRAGMENT_SHADER, ` + precision lowp float; + uniform sampler2D tex; + varying vec2 uv; + void main() { gl_FragColor = texture2D(tex,uv); }`)); + +gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); +gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), gl.STATIC_DRAW); +gl.vertexAttribPointer(0, 2, gl.FLOAT, gl.FALSE, 0, 0); +gl.enableVertexAttribArray(0); + +let texture = gl.createTexture(); +gl.bindTexture(gl.TEXTURE_2D, texture); + +// Test uploading an image +const expectedColor = [42, 84, 128, 255]; +const offset = SIZE - 4; +view.set(expectedColor, offset); +gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, 1, 1); +gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, view, offset); +wtu.glErrorShouldBe(gl, gl.NO_ERROR); + +// Test rendering with that image +gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + +// Verify that we rendered what we expected +wtu.checkCanvasRect(gl, 0, 0, 1, 1, expectedColor, "texSubImage2D produced expected color"); +var successfullyParsed = true; +</script> +<script src="../../js/js-test-post.js"></script> +</body> +</html> diff --git a/dom/canvas/test/webgl-conf/checkout/deqp/framework/common/tcuTestCase.js b/dom/canvas/test/webgl-conf/checkout/deqp/framework/common/tcuTestCase.js index ec08eea5ca..55913f4366 100644 --- a/dom/canvas/test/webgl-conf/checkout/deqp/framework/common/tcuTestCase.js +++ b/dom/canvas/test/webgl-conf/checkout/deqp/framework/common/tcuTestCase.js @@ -453,7 +453,32 @@ goog.scope(function() { if (inited) { // Run the test, save the result. + + const debug = tcuTestCase._debug = tcuTestCase._debug || (() => { + function LapStopwatch() { + this.lap = function() { + const now = performance.now(); + const ret = now - this.last; + this.last = now; + return ret; + }; + this.lap(); + } + return { + stopwatch: new LapStopwatch(), + testDoneCount: 0, + }; + })(); + const overheadDur = debug.stopwatch.lap(); + tcuTestCase.lastResult = state.currentTest.iterate(); + + const testDur = debug.stopwatch.lap(); + debug.testDoneCount += 1; + console.log( + `[test ${debug.testDoneCount}] Ran in ${testDur}ms`, + `(+ ${overheadDur}ms overhead)`, + ); } else { // Skip uninitialized test. tcuTestCase.lastResult = tcuTestCase.IterateResult.STOP; @@ -484,8 +509,8 @@ goog.scope(function() { } tcuTestCase.runner.runCallback(tcuTestCase.runTestCases); - } else + } else { tcuTestCase.runner.terminate(); + } }; - }); diff --git a/dom/canvas/test/webgl-conf/checkout/extra/tex-image-with-video-test.js b/dom/canvas/test/webgl-conf/checkout/extra/tex-image-with-video-test.js index 01f56b56c3..d2d6be0e28 100644 --- a/dom/canvas/test/webgl-conf/checkout/extra/tex-image-with-video-test.js +++ b/dom/canvas/test/webgl-conf/checkout/extra/tex-image-with-video-test.js @@ -27,7 +27,6 @@ function generateTest(pixelFormat, pixelType, prologue) { { src: "../resources/red-green.mp4" , type: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', }, { src: "../resources/red-green.webmvp8.webm", type: 'video/webm; codecs="vp8, vorbis"', }, { src: "../resources/red-green.webmvp9.webm", type: 'video/webm; codecs="vp9"', }, - { src: "../resources/red-green.theora.ogv", type: 'video/ogg; codecs="theora, vorbis"', }, ]; var videoNdx = 0; diff --git a/dom/canvas/test/webgl-conf/checkout/js/js-test-pre.js b/dom/canvas/test/webgl-conf/checkout/js/js-test-pre.js index e1cb9f749c..adc1f8a5aa 100644 --- a/dom/canvas/test/webgl-conf/checkout/js/js-test-pre.js +++ b/dom/canvas/test/webgl-conf/checkout/js/js-test-pre.js @@ -93,20 +93,25 @@ const RESULTS = { fail: 0, }; +// We cache these values since they will potentially be accessed many (100k+) +// times and accessing window can be significantly slower than a local variable. +const locationPathname = window.location.pathname; +const webglTestHarness = window.parent.webglTestHarness; + function reportTestResultsToHarness(success, msg) { if (success) { RESULTS.pass += 1; } else { RESULTS.fail += 1; } - if (window.parent.webglTestHarness) { - window.parent.webglTestHarness.reportResults(window.location.pathname, success, msg); + if (webglTestHarness) { + webglTestHarness.reportResults(locationPathname, success, msg); } } function reportSkippedTestResultsToHarness(success, msg) { - if (window.parent.webglTestHarness) { - window.parent.webglTestHarness.reportResults(window.location.pathname, success, msg, true); + if (webglTestHarness) { + webglTestHarness.reportResults(locationPathname, success, msg, true); } } @@ -116,8 +121,8 @@ function notifyFinishedToHarness() { } window._didNotifyFinishedToHarness = true; - if (window.parent.webglTestHarness) { - window.parent.webglTestHarness.notifyFinished(window.location.pathname); + if (webglTestHarness) { + webglTestHarness.notifyFinished(locationPathname); } if (window.nonKhronosFrameworkNotifyDone) { window.nonKhronosFrameworkNotifyDone(); @@ -268,9 +273,9 @@ function getCurrentTestName() */ function testPassedOptions(msg, addSpan) { + reportTestResultsToHarness(true, _currentTestName + ": " + msg); if (addSpan && !quietMode()) { - reportTestResultsToHarness(true, _currentTestName + ": " + msg); _addSpan('<span><span class="pass">PASS</span> ' + escapeHTML(_currentTestName) + ": " + escapeHTML(msg) + '</span>'); } if (_jsTestPreVerboseLogging) { @@ -285,9 +290,9 @@ function testPassedOptions(msg, addSpan) */ function testSkippedOptions(msg, addSpan) { + reportSkippedTestResultsToHarness(true, _currentTestName + ": " + msg); if (addSpan && !quietMode()) { - reportSkippedTestResultsToHarness(true, _currentTestName + ": " + msg); _addSpan('<span><span class="warn">SKIP</span> ' + escapeHTML(_currentTestName) + ": " + escapeHTML(msg) + '</span>'); } if (_jsTestPreVerboseLogging) { diff --git a/dom/canvas/test/webgl-conf/checkout/js/tests/compressed-texture-utils.js b/dom/canvas/test/webgl-conf/checkout/js/tests/compressed-texture-utils.js index 46d155f5f1..04396c9b32 100644 --- a/dom/canvas/test/webgl-conf/checkout/js/tests/compressed-texture-utils.js +++ b/dom/canvas/test/webgl-conf/checkout/js/tests/compressed-texture-utils.js @@ -67,6 +67,10 @@ let testCompressedFormatsUnavailableWhenExtensionDisabled = function(gl, compres if (compressedFormats.hasOwnProperty(name)) { gl.compressedTexImage2D(gl.TEXTURE_2D, 0, compressedFormats[name], testSize, testSize, 0, new Uint8Array(expectedByteLength(testSize, testSize, compressedFormats[name]))); wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "Trying to use format " + name + " with extension disabled."); + if (gl.texStorage2D) { + gl.texStorage2D(gl.TEXTURE_2D, 1, compressedFormats[name], testSize, testSize); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "Trying to use format " + name + " with texStorage2D with extension disabled."); + } } } gl.bindTexture(gl.TEXTURE_2D, null); @@ -255,4 +259,4 @@ return { testTexStorageLevelDimensions: testTexStorageLevelDimensions, }; -})();
\ No newline at end of file +})(); diff --git a/dom/canvas/test/webgl-conf/checkout/js/tests/drawingbuffer-storage-test.js b/dom/canvas/test/webgl-conf/checkout/js/tests/drawingbuffer-storage-test.js new file mode 100644 index 0000000000..330171b320 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/js/tests/drawingbuffer-storage-test.js @@ -0,0 +1,275 @@ +/* +Copyright (c) 2023 The Khronos Group Inc. +Use of this source code is governed by an MIT-style license that can be +found in the LICENSE.txt file. +*/ + +"use strict"; + +let gl; +let oldViewport; +let width; +let height; +let format; +let hasDrawingBufferStorage; +let maxRenderbufferSize; + +function runTest(contextVersion) { + description(); + debug(""); + + function initialize() { + let canvas = document.createElement("canvas"); + gl = wtu.create3DContext(canvas, {antialias: false}); + if (!gl) { + testFailed("context does not exist"); + return [0, 0]; + } + + hasDrawingBufferStorage = `drawingBufferStorage` in gl; + if (!hasDrawingBufferStorage) { + testPassed("drawingBufferStorage not present -- skipping test"); + return; + } + + maxRenderbufferSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE); + } + + function testPixel(expected, actual, tol) { + let str = 'approx equal: expected: ' + expected + ', actual: ' + actual + ', tolerance: ' + tol; + for (let i = 0; i < 4; ++i) { + if (Math.abs(expected[i] - actual[i]) > tol) { + testFailed(str); + return; + } + } + testPassed(str); + } + + function srgbToLinear(x) { + if (x < 0.0) + return 0.0; + if (x < 0.04045) + return x / 12.92; + if (x < 1.0) { + return Math.pow((x + 0.055)/1.044, 2.4); + } + return 1.0; + } + + function testClearColor() { + // Make a fresh canvas. + let canvas = document.createElement("canvas"); + canvas.width = 16; + canvas.height = 16; + + gl = wtu.create3DContext(canvas, {antialias: false}); + if (!gl) { + testFailed("context does not exist"); + return; + } + testPassed("context exists"); + shouldBe('gl.drawingBufferFormat', 'gl.RGBA8'); + + let testCase = function(f, size, clearColor, expectedPixel, tolerance) { + format = f; + width = size[0]; + height = size[1]; + + gl.drawingBufferStorage(format, width, height); + shouldBe('gl.getError()', 'gl.NO_ERROR'); + + shouldBe('gl.drawingBufferFormat', 'format'); + shouldBe('gl.drawingBufferWidth', 'width'); + shouldBe('gl.drawingBufferHeight', 'height'); + + gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); + gl.clear(gl.COLOR_BUFFER_BIT); + + let buf; + if (format == 0x881A /*RGBA16F*/) { + buf = new Float32Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.FLOAT, buf); + } else { + buf = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, buf); + } + testPixel(expectedPixel, buf, tolerance); + } + + debug('Testing RGBA8'); + testCase(gl.RGBA8, [16, 32], + [16 / 255, 32 / 255, 64 / 255, 128 / 255], + [16, 32, 64, 128], + 0); + + // WebGL 1 must use EXT_sRGB for SRGB8_ALPHA8. + let srgb8_alpha8 = gl.SRGB8_ALPHA8; + if (!srgb8_alpha8) { + let ext = gl.getExtension('EXT_sRGB'); + if (ext) { + srgb8_alpha8 = ext.SRGB8_ALPHA8_EXT; + } + } + if (srgb8_alpha8) { + debug('Testing SRGB8_ALPHA8'); + testCase(srgb8_alpha8, [16, 32], + [srgbToLinear(64/255), srgbToLinear(16/255), srgbToLinear(32/255), 128 / 255], + [64, 16, 32, 128], + 1); + } + + if (gl.getExtension('EXT_color_buffer_float')) { + // WebGL 1 must use EXT_color_buffer_half_float for RGBA16F. + let rgba16f = gl.RGBA16F; + if (!rgba16f) { + let ext = gl.getExtension('EXT_color_buffer_half_float'); + if (ext) { + rgba16f = ext.RGBA16F_EXT; + } + } + + debug('Testing RGBA16F'); + testCase(rgba16f, [18, 28], + [0.25, 0.5, 0.75, 0.125], + [0.25, 0.5, 0.75, 0.125], + 0.00001); + } else { + debug('Skipping RGBA16F'); + } + + shouldBe('gl.getError()', 'gl.NO_ERROR'); + } + + function testNoAlpha() { + let canvas = document.createElement("canvas"); + canvas.width = 16; + canvas.height = 16; + gl = wtu.create3DContext(canvas, {alpha:false}); + if (!gl) { + testFailed("context does not exist"); + return; + } + debug('Testing alpha:false'); + + // Report RGB8 for the format. + shouldBe('gl.drawingBufferFormat', 'gl.RGB8'); + + // If WebGLContextAttributes.alpha is false, generate INVALID_OPERATION. + gl.drawingBufferStorage(gl.RGBA8, 16, 16); + shouldBe('gl.getError()', 'gl.INVALID_OPERATION'); + } + + function testMissingExtension() { + let canvas = document.createElement("canvas"); + canvas.width = 16; + canvas.height = 16; + gl = wtu.create3DContext(canvas); + if (!gl) { + testFailed("context does not exist"); + return; + } + + debug('Testing use of RGBA16F without enabling EXT_color_buffer_float'); + gl.drawingBufferStorage(gl.RGBA16F, 16, 16); + shouldBe('gl.getError()', 'gl.INVALID_ENUM'); + } + + function testMaxSize() { + let canvas = document.createElement("canvas"); + canvas.width = 16; + canvas.height = 16; + gl = wtu.create3DContext(canvas); + if (!gl) { + testFailed("context does not exist"); + return; + } + + debug('Testing maximum size'); + gl.drawingBufferStorage(gl.RGBA8, maxRenderbufferSize, maxRenderbufferSize); + shouldBe('gl.getError()', 'gl.NONE'); + shouldBe('gl.drawingBufferWidth', 'maxRenderbufferSize'); + shouldBe('gl.drawingBufferHeight', 'maxRenderbufferSize'); + + debug('Testing over-maximum width and ehgith'); + gl.drawingBufferStorage(gl.RGBA8, maxRenderbufferSize+1, 16); + shouldBe('gl.getError()', 'gl.INVALID_VALUE'); + gl.drawingBufferStorage(gl.RGBA8, 16, maxRenderbufferSize+1); + shouldBe('gl.getError()', 'gl.INVALID_VALUE'); + shouldBe('gl.drawingBufferWidth', 'maxRenderbufferSize'); + shouldBe('gl.drawingBufferHeight', 'maxRenderbufferSize'); + } + + function testDrawToCanvas() { + let canvasGL = document.createElement("canvas"); + canvasGL.width = 16; + canvasGL.height = 16; + gl = wtu.create3DContext(canvasGL); + if (!gl) { + testFailed("context does not exist"); + return; + } + + let canvas2D = document.createElement("canvas"); + canvas2D.width = 16; + canvas2D.height = 16; + let ctx = canvas2D.getContext('2d'); + let imageData = new ImageData(16, 16); + + let testCase = function(f, clearColor, canvasColor, tolerance) { + gl.drawingBufferStorage(f, 16, 16); + gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); + gl.clear(gl.COLOR_BUFFER_BIT); + + ctx.putImageData(imageData, 0, 0); + ctx.drawImage(canvasGL, 0, 0); + testPixel(canvasColor, ctx.getImageData(8, 8, 1, 1).data, tolerance); + } + + debug('Drawing RGBA to canvas'); + testCase(gl.RGBA8, [16/255, 32/255, 64/255, 64/255], [64, 128, 255, 64], 0); + + // WebGL 1 must use EXT_sRGB for SRGB8_ALPHA8. + let srgb8_alpha8 = gl.SRGB8_ALPHA8; + if (!srgb8_alpha8) { + let ext = gl.getExtension('EXT_sRGB'); + if (ext) { + srgb8_alpha8 = ext.SRGB8_ALPHA8_EXT; + } + } + if (srgb8_alpha8) { + debug('Drawing opaque SRGB8_ALPHA8 to canvas'); + testCase(srgb8_alpha8, + [srgbToLinear(64/255), srgbToLinear(32/255), srgbToLinear(16/255), 1.0], + [64, 32, 16, 255], + 1); + + debug('Drawing transparent SRGB8_ALPHA8 to canvas'); + // We set the tolerance to 5 because of compounding error. The backbuffer + // may be off by 1, and then un-premultiplying alpha of 64/55 will multiply + // that error by 4. Then add one to be safe. + testCase(srgb8_alpha8, + [srgbToLinear(32/255), srgbToLinear(64/255), srgbToLinear(16/255), 64/255], + [128, 255, 64, 64], + 5); + } + + if (gl.getExtension('EXT_color_buffer_float')) { + debug('Drawing transparent RGBA16F to canvas'); + testCase(gl.RGBA16F, + [32/255, 64/255, 16/255, 64/255], + [128, 255, 64, 64], + 1); + } + } + + let wtu = WebGLTestUtils; + initialize(); + if (hasDrawingBufferStorage) { + testClearColor(); + testNoAlpha(); + testMissingExtension(); + testMaxSize(); + testDrawToCanvas(); + } +} diff --git a/dom/canvas/test/webgl-conf/checkout/js/tests/ext-color-buffer-half-float.js b/dom/canvas/test/webgl-conf/checkout/js/tests/ext-color-buffer-half-float.js index 51509e8a6e..2975ec0fe4 100644 --- a/dom/canvas/test/webgl-conf/checkout/js/tests/ext-color-buffer-half-float.js +++ b/dom/canvas/test/webgl-conf/checkout/js/tests/ext-color-buffer-half-float.js @@ -422,6 +422,33 @@ if (!gl) { runRGB16FNegativeTest(); } + if (version == 1) { + debug(""); + debug("Testing that component type framebuffer attachment queries are rejected with the extension disabled"); + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + + const rbo = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,gl.RENDERBUFFER, rbo); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGB565, 8, 8); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Setup renderbuffer should succeed."); + shouldBeNull('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, 0x8211 /* FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE */)'); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "Query must fail."); + gl.deleteRenderbuffer(rbo); + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 8, 8, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "Setup texture should succeed."); + shouldBeNull('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, 0x8211 /* FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE */)'); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "Query must fail."); + gl.deleteTexture(tex); + + gl.deleteFramebuffer(fbo); + } + let oesTextureHalfFloat = null; if (version == 1) { // oesTextureHalfFloat implicitly enables EXT_color_buffer_half_float if supported @@ -466,6 +493,47 @@ if (!gl) { runCopyTexImageTest(true); runUniqueObjectTest(); + + { + debug(""); + debug("Testing that component type framebuffer attachment queries are accepted with the extension enabled"); + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + + const rbo = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rbo); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,gl.RENDERBUFFER, rbo); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGB565, 8, 8); + shouldBe('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, ext.FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT)', 'ext.UNSIGNED_NORMALIZED_EXT'); + gl.renderbufferStorage(gl.RENDERBUFFER, ext.RGBA16F_EXT, 8, 8); + shouldBe('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, ext.FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT)', 'gl.FLOAT'); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "No errors after valid renderbuffer attachment queries."); + + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT,gl.RENDERBUFFER, rbo); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, 8, 8); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "No errors after depth-stencil renderbuffer setup."); + shouldBeNull('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, ext.FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT)'); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "Component type query is not allowed for combined depth-stencil attachments."); + gl.deleteRenderbuffer(rbo); + + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 8, 8, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + shouldBe('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, ext.FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT)', 'ext.UNSIGNED_NORMALIZED_EXT'); + const tex_ext = gl.getExtension("OES_texture_half_float"); + if (version > 1 || tex_ext) { + if (version > 1) + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, 8, 8, 0, gl.RGBA, gl.HALF_FLOAT, null); + else + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 8, 8, 0, gl.RGBA, tex_ext.HALF_FLOAT_OES, null); + shouldBe('gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, ext.FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT)', 'gl.FLOAT'); + } + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "No errors after valid texture attachment queries."); + gl.deleteTexture(tex); + + gl.deleteFramebuffer(fbo); + } } } diff --git a/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-2d-with-image-bitmap-from-video.js b/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-2d-with-image-bitmap-from-video.js index 14cf4628be..504b70564e 100644 --- a/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-2d-with-image-bitmap-from-video.js +++ b/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-2d-with-image-bitmap-from-video.js @@ -14,7 +14,6 @@ function generateTest(internalFormat, pixelFormat, pixelType, prologue, resource { src: resourcePath + "red-green.mp4" , type: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', }, { src: resourcePath + "red-green.webmvp8.webm" , type: 'video/webm; codecs="vp8, vorbis"', }, { src: resourcePath + "red-green.bt601.vp9.webm", type: 'video/webm; codecs="vp9"', }, - { src: resourcePath + "red-green.theora.ogv" , type: 'video/ogg; codecs="theora, vorbis"', }, ]; function init() diff --git a/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-2d-with-video.js b/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-2d-with-video.js index 6e8bcf96e9..8dadde2d69 100644 --- a/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-2d-with-video.js +++ b/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-2d-with-video.js @@ -27,7 +27,6 @@ function generateTest(internalFormat, pixelFormat, pixelType, prologue, resource { src: resourcePath + "red-green.mp4" , type: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', }, { src: resourcePath + "red-green.webmvp8.webm" , type: 'video/webm; codecs="vp8, vorbis"', }, { src: resourcePath + "red-green.bt601.vp9.webm", type: 'video/webm; codecs="vp9"', }, - { src: resourcePath + "red-green.theora.ogv" , type: 'video/ogg; codecs="theora, vorbis"', }, ]; function init() diff --git a/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-3d-with-image-bitmap-from-video.js b/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-3d-with-image-bitmap-from-video.js index a268f7d8d5..b1dbd33913 100644 --- a/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-3d-with-image-bitmap-from-video.js +++ b/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-3d-with-image-bitmap-from-video.js @@ -14,7 +14,6 @@ function generateTest(internalFormat, pixelFormat, pixelType, prologue, resource { src: resourcePath + "red-green.mp4" , type: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', }, { src: resourcePath + "red-green.webmvp8.webm" , type: 'video/webm; codecs="vp8, vorbis"', }, { src: resourcePath + "red-green.bt601.vp9.webm", type: 'video/webm; codecs="vp9"', }, - { src: resourcePath + "red-green.theora.ogv" , type: 'video/ogg; codecs="theora, vorbis"', }, ]; function init() diff --git a/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-3d-with-video.js b/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-3d-with-video.js index 0c2c40e8a5..43ad660070 100644 --- a/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-3d-with-video.js +++ b/dom/canvas/test/webgl-conf/checkout/js/tests/tex-image-and-sub-image-3d-with-video.js @@ -29,7 +29,6 @@ function generateTest(internalFormat, pixelFormat, pixelType, prologue, resource { src: resourcePath + "red-green.mp4" , type: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', }, { src: resourcePath + "red-green.bt601.vp9.webm", type: 'video/webm; codecs="vp9"', }, { src: resourcePath + "red-green.webmvp8.webm", type: 'video/webm; codecs="vp8, vorbis"', }, - { src: resourcePath + "red-green.theora.ogv", type: 'video/ogg; codecs="theora, vorbis"', }, ]; function init() diff --git a/dom/canvas/test/webgl-conf/checkout/js/tests/webgl-blend-func-extended.js b/dom/canvas/test/webgl-conf/checkout/js/tests/webgl-blend-func-extended.js new file mode 100644 index 0000000000..086d9cfb16 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/js/tests/webgl-blend-func-extended.js @@ -0,0 +1,548 @@ +"use strict"; +description("This test verifies the functionality of the WEBGL_blend_func_extended extension, if it is available."); + +debug(""); + +var wtu = WebGLTestUtils; +var gl = wtu.create3DContext("c", undefined, contextVersion); +var ext; + +function runTestNoExtension() { + debug(""); + debug("Testing getParameter without the extension"); + shouldBeNull("gl.getParameter(0x88FC /* MAX_DUAL_SOURCE_DRAW_BUFFERS_WEBGL */)"); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "parameter unknown"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + if (contextVersion == 1) { + debug(""); + debug("Testing SRC_ALPHA_SATURATE without the extension"); + + gl.blendFunc(gl.ONE, gl.SRC_ALPHA_SATURATE); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "SRC_ALPHA_SATURATE not accepted as blendFunc dfactor"); + gl.blendFuncSeparate(gl.ONE, gl.SRC_ALPHA_SATURATE, gl.ONE, gl.ONE); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "SRC_ALPHA_SATURATE not accepted as blendFuncSeparate dstRGB"); + gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.SRC_ALPHA_SATURATE); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, "SRC_ALPHA_SATURATE not accepted as blendFuncSeparate dstAlpha"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + } + + debug(""); + debug("Testing SRC1 blend funcs without the extension"); + + const extFuncs = { + SRC1_COLOR_WEBGL: 0x88F9, + SRC1_ALPHA_WEBGL: 0x8589, + ONE_MINUS_SRC1_COLOR_WEBGL: 0x88FA, + ONE_MINUS_SRC1_ALPHA_WEBGL: 0x88FB + }; + + for (const func in extFuncs) { + gl.blendFunc(extFuncs[func], gl.ONE); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `${func} not accepted as blendFunc sfactor`); + gl.blendFunc(gl.ONE, extFuncs[func]); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `${func} not accepted as blendFunc dfactor`); + gl.blendFuncSeparate(extFuncs[func], gl.ONE, gl.ONE, gl.ONE); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `${func} not accepted as blendFuncSeparate srcRGB`); + gl.blendFuncSeparate(gl.ONE, extFuncs[func], gl.ONE, gl.ONE); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `${func} not accepted as blendFuncSeparate dstRGB`); + gl.blendFuncSeparate(gl.ONE, gl.ONE, extFuncs[func], gl.ONE); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `${func} not accepted as blendFuncSeparate srcAlpha`); + gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, extFuncs[func]); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `${func} not accepted as blendFuncSeparate dstAlpha`); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + } + + const dbi = gl.getExtension("OES_draw_buffers_indexed"); + if (!dbi) return; + + debug(""); + debug("Testing indexed SRC1 blend funcs without the extension"); + for (const func in extFuncs) { + dbi.blendFunciOES(0, extFuncs[func], gl.ONE); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `${func} not accepted as blendFunciOES src`); + dbi.blendFunciOES(0, gl.ONE, extFuncs[func]); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `${func} not accepted as blendFunciOES dst`); + dbi.blendFuncSeparateiOES(0, extFuncs[func], gl.ONE, gl.ONE, gl.ONE); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `${func} not accepted as blendFuncSeparateiOES srcRGB`); + dbi.blendFuncSeparateiOES(0, gl.ONE, extFuncs[func], gl.ONE, gl.ONE); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `${func} not accepted as blendFuncSeparateiOES dstRGB`); + dbi.blendFuncSeparateiOES(0, gl.ONE, gl.ONE, extFuncs[func], gl.ONE); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `${func} not accepted as blendFuncSeparateiOES srcAlpha`); + dbi.blendFuncSeparateiOES(0, gl.ONE, gl.ONE, gl.ONE, extFuncs[func]); + wtu.glErrorShouldBe(gl, gl.INVALID_ENUM, `${func} not accepted as blendFuncSeparateiOES dstAlpha`); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + } +} + +function runEnumTests() { + debug(""); + debug("Testing enums"); + shouldBe("ext.SRC1_COLOR_WEBGL", "0x88F9"); + shouldBe("ext.SRC1_ALPHA_WEBGL", "0x8589"); + shouldBe("ext.ONE_MINUS_SRC1_COLOR_WEBGL", "0x88FA"); + shouldBe("ext.ONE_MINUS_SRC1_ALPHA_WEBGL", "0x88FB"); + shouldBe("ext.MAX_DUAL_SOURCE_DRAW_BUFFERS_WEBGL", "0x88FC"); +} + +function runQueryTests() { + debug(""); + debug("Testing getParameter"); + shouldBeGreaterThanOrEqual("gl.getParameter(ext.MAX_DUAL_SOURCE_DRAW_BUFFERS_WEBGL)", "1"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + + if (contextVersion == 1) { + debug(""); + debug("Testing SRC_ALPHA_SATURATE with the extension"); + + gl.blendFunc(gl.ONE, gl.SRC_ALPHA_SATURATE); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "SRC_ALPHA_SATURATE accepted as blendFunc dfactor"); + shouldBe("gl.getParameter(gl.BLEND_DST_RGB)", "gl.SRC_ALPHA_SATURATE"); + shouldBe("gl.getParameter(gl.BLEND_DST_ALPHA)", "gl.SRC_ALPHA_SATURATE"); + gl.blendFuncSeparate(gl.ONE, gl.SRC_ALPHA_SATURATE, gl.ONE, gl.ONE); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "SRC_ALPHA_SATURATE accepted as blendFuncSeparate dstRGB"); + shouldBe("gl.getParameter(gl.BLEND_DST_RGB)", "gl.SRC_ALPHA_SATURATE"); + gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.SRC_ALPHA_SATURATE); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "SRC_ALPHA_SATURATE accepted as blendFuncSeparate dstAlpha"); + shouldBe("gl.getParameter(gl.BLEND_DST_ALPHA)", "gl.SRC_ALPHA_SATURATE"); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + } + + const extFuncs = [ + "SRC1_COLOR_WEBGL", + "SRC1_ALPHA_WEBGL", + "ONE_MINUS_SRC1_COLOR_WEBGL", + "ONE_MINUS_SRC1_ALPHA_WEBGL" + ]; + + debug(""); + debug("Testing blend state updates with SRC1 blend funcs"); + for (const func of extFuncs) { + gl.blendFunc(ext[func], gl.ONE); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, `${func} accepted as blendFunc sfactor`); + shouldBe("gl.getParameter(gl.BLEND_SRC_RGB)", `ext.${func}`); + shouldBe("gl.getParameter(gl.BLEND_SRC_ALPHA)", `ext.${func}`); + gl.blendFunc(gl.ONE, ext[func]); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, `${func} accepted as blendFunc dfactor`); + shouldBe("gl.getParameter(gl.BLEND_DST_RGB)", `ext.${func}`); + shouldBe("gl.getParameter(gl.BLEND_DST_ALPHA)", `ext.${func}`); + gl.blendFuncSeparate(ext[func], gl.ONE, gl.ONE, gl.ONE); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, `${func} accepted as blendFuncSeparate srcRGB`); + shouldBe("gl.getParameter(gl.BLEND_SRC_RGB)", `ext.${func}`); + gl.blendFuncSeparate(gl.ONE, ext[func], gl.ONE, gl.ONE); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, `${func} accepted as blendFuncSeparate dstRGB`); + shouldBe("gl.getParameter(gl.BLEND_DST_RGB)", `ext.${func}`); + gl.blendFuncSeparate(gl.ONE, gl.ONE, ext[func], gl.ONE); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, `${func} accepted as blendFuncSeparate srcAlpha`); + shouldBe("gl.getParameter(gl.BLEND_SRC_ALPHA)", `ext.${func}`); + gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, ext[func]); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, `${func} accepted as blendFuncSeparate dstAlpha`); + shouldBe("gl.getParameter(gl.BLEND_DST_ALPHA)", `ext.${func}`); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + } + + const dbi = gl.getExtension("OES_draw_buffers_indexed"); + if (!dbi) return; + + debug(""); + debug("Testing indexed blend state updates with SRC1 blend funcs"); + for (const func of extFuncs) { + dbi.blendFunciOES(0, ext[func], gl.ONE); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, `${func} accepted as blendFunciOES src`); + shouldBe("gl.getIndexedParameter(gl.BLEND_SRC_RGB, 0)", `ext.${func}`); + shouldBe("gl.getIndexedParameter(gl.BLEND_SRC_ALPHA, 0)", `ext.${func}`); + dbi.blendFunciOES(0, gl.ONE, ext[func]); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, `${func} accepted as blendFunciOES dst`); + shouldBe("gl.getIndexedParameter(gl.BLEND_DST_RGB, 0)", `ext.${func}`); + shouldBe("gl.getIndexedParameter(gl.BLEND_DST_ALPHA, 0)", `ext.${func}`); + dbi.blendFuncSeparateiOES(0, ext[func], gl.ONE, gl.ONE, gl.ONE); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, `${func} not accepted as blendFuncSeparateiOES srcRGB`); + shouldBe("gl.getIndexedParameter(gl.BLEND_SRC_RGB, 0)", `ext.${func}`); + dbi.blendFuncSeparateiOES(0, gl.ONE, ext[func], gl.ONE, gl.ONE); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, `${func} not accepted as blendFuncSeparateiOES dstRGB`); + shouldBe("gl.getIndexedParameter(gl.BLEND_DST_RGB, 0)", `ext.${func}`); + dbi.blendFuncSeparateiOES(0, gl.ONE, gl.ONE, ext[func], gl.ONE); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, `${func} not accepted as blendFuncSeparateiOES srcAlpha`); + shouldBe("gl.getIndexedParameter(gl.BLEND_SRC_ALPHA, 0)", `ext.${func}`); + dbi.blendFuncSeparateiOES(0, gl.ONE, gl.ONE, gl.ONE, ext[func]); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, `${func} not accepted as blendFuncSeparateiOES dstAlpha`); + shouldBe("gl.getIndexedParameter(gl.BLEND_DST_ALPHA, 0)", `ext.${func}`); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "should be no errors"); + } +} + +function runShaderTests(extensionEnabled) { + debug(""); + debug("Testing various shader compiles with extension " + (extensionEnabled ? "enabled" : "disabled")); + + const shaderSets = []; + + const macro100 = `precision mediump float; + void main() { + #ifdef GL_EXT_blend_func_extended + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + #else + #error no GL_EXT_blend_func_extended; + #endif + }`; + const macro300 = `#version 300 es + out mediump vec4 my_FragColor; + void main() { + #ifdef GL_EXT_blend_func_extended + my_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + #else + #error no GL_EXT_blend_func_extended; + #endif + }`; + shaderSets.push([wtu.simpleVertexShader, macro100]); + if (contextVersion == 2) { + shaderSets.push([wtu.simpleVertexShaderESSL300, macro300]); + } + + for (const shaders of shaderSets) { + // Expect the macro shader to succeed ONLY if enabled + if (wtu.setupProgram(gl, shaders)) { + if (extensionEnabled) { + testPassed("Macro defined in shaders when extension is enabled"); + } else { + testFailed("Macro defined in shaders when extension is disabled"); + } + } else { + if (extensionEnabled) { + testFailed("Macro not defined in shaders when extension is enabled"); + } else { + testPassed("Macro not defined in shaders when extension is disabled"); + } + } + } + + shaderSets.length = 0; + + const missing100 = ` + void main() { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + gl_SecondaryFragColorEXT = vec4(0.0, 1.0, 0.0, 1.0); + }`; + shaderSets.push([wtu.simpleVertexShader, missing100]); + + const missing300 = `#version 300 es + layout(location = 0) out mediump vec4 oColor0; + layout(location = 0, index = 1) out mediump vec4 oColor1; + void main() { + oColor0 = vec4(1.0, 0.0, 0.0, 1.0); + oColor1 = vec4(0.0, 1.0, 0.0, 1.0); + }`; + if (contextVersion == 2) { + shaderSets.push([wtu.simpleVertexShaderESSL300, missing300]); + } + + // Always expect the shader missing the #extension pragma to fail (whether enabled or not) + for (const shaders of shaderSets) { + if (wtu.setupProgram(gl, shaders)) { + testFailed("Secondary fragment output allowed without #extension pragma"); + } else { + testPassed("Secondary fragment output disallowed without #extension pragma"); + } + } + + shaderSets.length = 0; + + const valid100 = `#extension GL_EXT_blend_func_extended : enable + void main() { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + gl_SecondaryFragColorEXT = vec4(0.0, 1.0, 0.0, 1.0); + }`; + shaderSets.push([wtu.simpleVertexShader, valid100]); + + const valid300 = `#version 300 es + #extension GL_EXT_blend_func_extended : enable + layout(location = 0) out mediump vec4 oColor0; + layout(location = 0, index = 1) out mediump vec4 oColor1; + void main() { + oColor0 = vec4(1.0, 0.0, 0.0, 1.0); + oColor1 = vec4(0.0, 1.0, 0.0, 1.0); + }`; + if (contextVersion == 2) { + shaderSets.push([wtu.simpleVertexShaderESSL300, valid300]); + } + + // Try to compile a shader using a secondary fragment output that should only succeed if enabled + for (const shaders of shaderSets) { + if (wtu.setupProgram(gl, shaders)) { + if (extensionEnabled) { + testPassed("Secondary fragment output compiled successfully when extension enabled"); + } else { + testFailed("Secondary fragment output compiled successfully when extension disabled"); + } + } else { + if (extensionEnabled) { + testFailed("Secondary fragment output failed to compile when extension enabled"); + } else { + testPassed("Secondary fragment output failed to compile when extension disabled"); + } + } + } + + // ESSL 3.00: Testing that multiple outputs require explicit locations + if (contextVersion == 2) { + const locations300 = `#version 300 es + #extension GL_EXT_blend_func_extended : enable + out mediump vec4 color0; + out mediump vec4 color1; + void main() { + color0 = vec4(1.0, 0.0, 0.0, 1.0); + color1 = vec4(0.0, 1.0, 0.0, 1.0); + }`; + if (wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, locations300])) { + testFailed("Multiple fragment outputs compiled successfully without explicit locations"); + } else { + testPassed("Multiple fragment outputs failed to compile without explicit locations"); + } + } +} + +function runMissingOutputsTests() { + debug(""); + debug("Test draw calls with missing fragment outputs"); + + wtu.setupUnitQuad(gl); + gl.blendFunc(gl.ONE, ext.SRC1_COLOR_WEBGL); + + for (const enabled of [false, true]) { + if (enabled) { + gl.enable(gl.BLEND); + } else { + gl.disable(gl.BLEND); + } + + for (const maskedOut of [false, true]) { + gl.colorMask(!maskedOut, false, false, false); + + const label = `Dual-source blending ${enabled ? "ENABLED" : "DISABLED"}, ` + + `missing fragment outputs, and ` + + `${maskedOut ? "" : "NOT "}all color channels masked out`; + debug(`ESSL 1.00: ${label}`); + + { + const none = "void main() {}"; + wtu.setupProgram(gl, [wtu.simpleVertexShader, none]); + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, maskedOut ? gl.NO_ERROR : gl.INVALID_OPERATION, + "no fragment outputs"); + } + + { + const fragColor = ` + void main() { + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + }`; + wtu.setupProgram(gl, [wtu.simpleVertexShader, fragColor]); + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, (!enabled || maskedOut) ? gl.NO_ERROR : gl.INVALID_OPERATION, + "only gl_FragColor"); + } + + { + const secondaryFragColor = `#extension GL_EXT_blend_func_extended : enable + void main() { + gl_SecondaryFragColorEXT = vec4(0.0, 1.0, 0.0, 1.0); + }`; + wtu.setupProgram(gl, [wtu.simpleVertexShader, secondaryFragColor]); + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, maskedOut ? gl.NO_ERROR : gl.INVALID_OPERATION, + "only gl_SecondaryFragColorEXT"); + } + + if (contextVersion == 1) continue; + + debug(`ESSL 3.00: ${label}`); + + { + const none = `#version 300 es + void main() {}`; + wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, none]); + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, maskedOut ? gl.NO_ERROR : gl.INVALID_OPERATION, + "no fragment outputs"); + } + + { + const color0 = `#version 300 es + out mediump vec4 color0; + void main() { + color0 = vec4(1.0, 0.0, 0.0, 1.0); + }`; + wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, color0]); + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, (!enabled || maskedOut) ? gl.NO_ERROR : gl.INVALID_OPERATION, + "only index 0 output"); + } + + { + const color1 = `#version 300 es + #extension GL_EXT_blend_func_extended : enable + layout(location = 0, index = 1) out mediump vec4 color1; + void main() { + color1 = vec4(0.0, 1.0, 0.0, 1.0); + }`; + wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, color1]); + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, maskedOut ? gl.NO_ERROR : gl.INVALID_OPERATION, + "only index 1 output"); + } + } + } + gl.colorMask(true, true, true, true); +} + +function runDrawBuffersLimitTests() { + const dbi = gl.getExtension("OES_draw_buffers_indexed"); + if (!dbi) return; + + debug(""); + debug("Testing that dual-source blending limits the number of active draw buffers"); + + const rb0 = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rb0); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA8, 1, 1); + + const rb1 = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, rb1); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA8, 1, 1); + + gl.bindRenderbuffer(gl.RENDERBUFFER, null); + + const fbo = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, rb0); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT1, gl.RENDERBUFFER, rb1); + wtu.framebufferStatusShouldBe(gl, gl.FRAMEBUFFER, gl.FRAMEBUFFER_COMPLETE); + + const fs = `#version 300 es + #extension GL_EXT_blend_func_extended : enable + layout(location = 0, index = 0) out mediump vec4 color0; + layout(location = 0, index = 1) out mediump vec4 color1; + void main() { + color0 = vec4(1.0, 0.0, 0.0, 1.0); + color1 = vec4(0.0, 1.0, 0.0, 1.0); + }`; + wtu.setupProgram(gl, [wtu.simpleVertexShaderESSL300, fs]); + + wtu.setupUnitQuad(gl); + + // Enable both draw buffers + gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]); + + // Mask out draw buffer 1 to pass missing fragment outputs check + dbi.colorMaskiOES(1, false, false, false, false); + + const extFuncs = [ + "SRC1_COLOR_WEBGL", + "SRC1_ALPHA_WEBGL", + "ONE_MINUS_SRC1_COLOR_WEBGL", + "ONE_MINUS_SRC1_ALPHA_WEBGL" + ]; + + for (const func of extFuncs) { + for (let slot = 0; slot < 4; slot++) { + let param; + switch (slot) { + case 0: + param = "srcRGB"; + gl.blendFuncSeparate(ext[func], gl.ONE, gl.ONE, gl.ONE); + break; + case 1: + param = "dstRGB"; + gl.blendFuncSeparate(gl.ONE, ext[func], gl.ONE, gl.ONE); + break; + case 2: + param = "srcAlpha"; + gl.blendFuncSeparate(gl.ONE, gl.ONE, ext[func], gl.ONE); + break; + case 3: + param = "dstAlpha"; + gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, ext[func]); + break; + } + debug(`Testing ${func} with ${param}`); + + // Limit must be applied even with blending disabled + gl.disable(gl.BLEND); + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "blending disabled"); + + gl.enable(gl.BLEND); + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, gl.INVALID_OPERATION, "blending enabled"); + + // Limit is not applied when non-SRC1 funcs are used + gl.blendFunc(gl.ONE, gl.ONE); + wtu.drawUnitQuad(gl); + wtu.glErrorShouldBe(gl, gl.NO_ERROR, "dual-source blending disabled"); + } + } + gl.bindFramebuffer(gl.FRAMEBUFFER, null); +} + +function runBlendingTests() { + debug(""); + debug("Testing rendering with two most common dual-source blending configurations"); + + const fs = `#extension GL_EXT_blend_func_extended : enable + uniform mediump vec4 u_src0; + uniform mediump vec4 u_src1; + void main() { + gl_FragColor = u_src0; + gl_SecondaryFragColorEXT = u_src1; + }`; + const program = wtu.setupProgram(gl, [wtu.simpleVertexShader, fs]); + const uSrc0 = gl.getUniformLocation(program, "u_src0"); + const uSrc1 = gl.getUniformLocation(program, "u_src1"); + + gl.enable(gl.BLEND); + wtu.setupUnitQuad(gl); + gl.clearColor(1.0, 1.0, 1.0, 1.0); + + gl.clear(gl.COLOR_BUFFER_BIT); + gl.blendFunc(gl.ONE, ext.SRC1_COLOR_WEBGL); + gl.uniform4f(uSrc0, 0.250, 0.375, 0.500, 0.625); + gl.uniform4f(uSrc1, 0.125, 0.125, 0.125, 0.125); + wtu.drawUnitQuad(gl); + wtu.checkCanvas(gl, [96, 128, 159, 191], "Multiply destination by SRC1 and add SRC0", 2); + + gl.clear(gl.COLOR_BUFFER_BIT); + gl.blendFunc(ext.SRC1_COLOR_WEBGL, ext.ONE_MINUS_SRC1_COLOR_WEBGL); + gl.uniform4f(uSrc0, 0.125, 0.125, 0.125, 0.125); + gl.uniform4f(uSrc1, 0.500, 0.375, 0.250, 0.125); + wtu.drawUnitQuad(gl); + wtu.checkCanvas(gl, [143, 171, 199, 227], "Per-channel color interpolation using SRC1", 2); +} + +function runTest() { + if (!gl) { + testFailed("context does not exist"); + return; + } + testPassed("context exists"); + + runTestNoExtension(); + runShaderTests(false); + + ext = gl.getExtension("WEBGL_blend_func_extended"); + wtu.runExtensionSupportedTest(gl, "WEBGL_blend_func_extended", ext !== null); + + if (ext !== null) { + runEnumTests(); + runQueryTests(); + runShaderTests(true); + runMissingOutputsTests(); + runDrawBuffersLimitTests(); + runBlendingTests(); + } else { + testPassed("No WEBGL_blend_func_extended support -- this is legal"); + } +} + +runTest(); + +var successfullyParsed = true; diff --git a/dom/canvas/test/webgl-conf/checkout/js/webgl-test-harness.js b/dom/canvas/test/webgl-conf/checkout/js/webgl-test-harness.js index f48d9d2ad7..ca6cbfcd36 100644 --- a/dom/canvas/test/webgl-conf/checkout/js/webgl-test-harness.js +++ b/dom/canvas/test/webgl-conf/checkout/js/webgl-test-harness.js @@ -538,15 +538,36 @@ TestHarness.prototype.runTests = function(opt_options) { this.startNextTest(); }; -TestHarness.prototype.setTimeout = function(test) { - var that = this; - test.timeoutId = this.window.setTimeout(function() { - that.timeout(test); - }, this.timeoutDelay); +TestHarness.prototype._bumpTimeout = function(test) { + const newTimeoutAt = performance.now() + this.timeoutDelay; + if (test.timeoutAt) { + test.timeoutAt = newTimeoutAt; + return; + } + test.timeoutAt = newTimeoutAt; + + const harness = this; + + function enqueueWatchdog() { + const remaining = test.timeoutAt - performance.now(); + //console.log(`watchdog started at ${performance.now()}, ${test.timeoutAt} requested`); + this.window.setTimeout(() => { + if (!test.timeoutAt) return; // Timeout was cleared. + const remainingAtCheckTime = test.timeoutAt - performance.now(); + if (performance.now() >= test.timeoutAt) { + //console.log(`watchdog won at ${performance.now()}, ${test.timeoutAt} requested`); + harness.timeout(test); + return; + } + //console.log(`watchdog lost at ${performance.now()}, as ${test.timeoutAt} is now requested`); + enqueueWatchdog(); + }, remaining); + } + enqueueWatchdog(); }; TestHarness.prototype.clearTimeout = function(test) { - this.window.clearTimeout(test.timeoutId); + test.timeoutAt = null; }; TestHarness.prototype.startNextTest = function() { @@ -577,7 +598,7 @@ TestHarness.prototype.startTest = function(iframe, testFile, webglVersion) { "dumpShaders": this.dumpShaders, "quiet": this.quiet }); - this.setTimeout(test); + this._bumpTimeout(test); } else { this.reportResults(url, !!this.allowSkip, "skipped", true); this.notifyFinished(url); @@ -595,11 +616,15 @@ TestHarness.prototype.getTest = function(url) { TestHarness.prototype.reportResults = function(url, success, msg, skipped) { url = FilterURL(url); var test = this.getTest(url); - this.clearTimeout(test); - log((success ? "PASS" : "FAIL") + ": " + msg); + if (0) { + // This is too slow to leave on for tests like + // deqp/functional/gles3/vertexarrays/multiple_attributes.output.html + // which has 33013505 calls to reportResults. + log((success ? "PASS" : "FAIL") + ": " + msg); + } this.reportFunc(TestHarness.reportType.TEST_RESULT, url, msg, success, skipped); // For each result we get, reset the timeout - this.setTimeout(test); + this._bumpTimeout(test); }; TestHarness.prototype.dequeTest = function(test) { diff --git a/dom/canvas/test/webgl-conf/checkout/py/lint/lint.allowlist b/dom/canvas/test/webgl-conf/checkout/py/lint/lint.allowlist index eb4efce09e..b610bfa4e1 100644 --- a/dom/canvas/test/webgl-conf/checkout/py/lint/lint.allowlist +++ b/dom/canvas/test/webgl-conf/checkout/py/lint/lint.allowlist @@ -21,7 +21,7 @@ UNNECESSARY EXECUTABLE PERMISSION:specs/1.0 UNNECESSARY EXECUTABLE PERMISSION:specs/2.0 ## Ignore INDENT TABS ## - + INDENT TABS:*.frag INDENT TABS:*.vert #The original dEQP tests used tabs throughout. @@ -46,7 +46,6 @@ INVALID UNICODE:conformance/glsl/misc/non-ascii.vert.html *:*.mp3 *:*.m4a *:*.oga -*:*.ogv *:*.webm *:*.mp4 *:*.m4v diff --git a/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-originals/exif-orientation-test-8-90-cw.jpg b/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-originals/exif-orientation-test-6-90-cw.jpg Binary files differindex d4e75fac7d..d4e75fac7d 100644 --- a/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-originals/exif-orientation-test-8-90-cw.jpg +++ b/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-originals/exif-orientation-test-6-90-cw.jpg diff --git a/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-originals/exif-orientation-test-6-90-ccw.jpg b/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-originals/exif-orientation-test-8-90-ccw.jpg Binary files differindex b4679aedd9..b4679aedd9 100644 --- a/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-originals/exif-orientation-test-6-90-ccw.jpg +++ b/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-originals/exif-orientation-test-8-90-ccw.jpg diff --git a/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-test-6-90-ccw.jpg b/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-test-6-90-cw.jpg Binary files differindex f8c9a6b0cc..f8c9a6b0cc 100644 --- a/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-test-6-90-ccw.jpg +++ b/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-test-6-90-cw.jpg diff --git a/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-test-8-90-cw.jpg b/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-test-8-90-ccw.jpg Binary files differindex 594c86ff4e..594c86ff4e 100644 --- a/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-test-8-90-cw.jpg +++ b/dom/canvas/test/webgl-conf/checkout/resources/exif-orientation-test-8-90-ccw.jpg diff --git a/dom/canvas/test/webgl-conf/checkout/resources/npot-video.theora.ogv b/dom/canvas/test/webgl-conf/checkout/resources/npot-video.theora.ogv Binary files differdeleted file mode 100644 index 4458678fbf..0000000000 --- a/dom/canvas/test/webgl-conf/checkout/resources/npot-video.theora.ogv +++ /dev/null diff --git a/dom/canvas/test/webgl-conf/checkout/resources/red-gradient-8x1-10bit-untagged.png b/dom/canvas/test/webgl-conf/checkout/resources/red-gradient-8x1-10bit-untagged.png Binary files differnew file mode 100644 index 0000000000..d01209e9b5 --- /dev/null +++ b/dom/canvas/test/webgl-conf/checkout/resources/red-gradient-8x1-10bit-untagged.png diff --git a/dom/canvas/test/webgl-conf/checkout/resources/red-green.theora.ogv b/dom/canvas/test/webgl-conf/checkout/resources/red-green.theora.ogv Binary files differdeleted file mode 100644 index 1543915a10..0000000000 --- a/dom/canvas/test/webgl-conf/checkout/resources/red-green.theora.ogv +++ /dev/null diff --git a/dom/canvas/test/webgl-conf/checkout/test-guidelines.md b/dom/canvas/test/webgl-conf/checkout/test-guidelines.md index 679892b1f0..b02738726e 100644 --- a/dom/canvas/test/webgl-conf/checkout/test-guidelines.md +++ b/dom/canvas/test/webgl-conf/checkout/test-guidelines.md @@ -18,7 +18,7 @@ the WebGL Working Group when "official" snapshots are taken. These lines must appear in a comment at the top of every code file under sdk/tests/conformance ``` -Copyright (c) 2019 The Khronos Group Inc. +Copyright (c) 2023 The Khronos Group Inc. Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt file. ``` @@ -102,7 +102,7 @@ found in the LICENSE.txt file. * Tests that are short and run synchronously end with - <script src="../../resources/js-test-post.js"></script> + <script src="../../js/js-test-post.js"></script> * Tests that take a long time use setTimeout so as not to freeze the browser. @@ -144,7 +144,7 @@ found in the LICENSE.txt file. * Vendors may place test harness specific code in the testing infrastructure. - resources/js-test-pre.js + js/js-test-pre.js conformance/more/unit.js * Indent with spaces not tabs. (not everyone uses your tab settings). diff --git a/dom/canvas/test/webgl-conf/cherry_picks.txt b/dom/canvas/test/webgl-conf/cherry_picks.txt index cf84cd4492..f5342bb2b8 100644 --- a/dom/canvas/test/webgl-conf/cherry_picks.txt +++ b/dom/canvas/test/webgl-conf/cherry_picks.txt @@ -1,54 +1,17 @@ -commit 4f57098d0dbad68f41c87835fca5a6f0ba669350 -Author: Gregg Tavares <github@greggman.com> -Date: Thu Nov 24 10:29:32 2022 -0800 +commit cd04892d7d7ac986a83383b06bfa792c9c369f8c +Author: Kelsey Gilbert <kelsey.gilbert@mozilla.com> +Date: Tue Apr 2 14:43:02 2024 -0700 - Test calling getUniform from non-current program - - It's possible this is already tested but I didn't see anything - obvious and Firefox fails this test - -commit d308751948807f08b36d06b0e8c835a1ffe078ae -Author: Kelsey Gilbert <jdashg@gmail.com> -Date: Tue Oct 18 15:21:32 2022 -0700 - - Add conformance/textures/misc/texture-srgb-upload.html. - - Test for both webgl1+ext and webgl2. - Test uploads from ArrayBuffer and (inlined) video. - Ensure that srgb textures fetch/decode 0x7f as 0.21 not 0.5. - - Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1696693 - -commit 50024c70fc8c8f1613275cffe90dea02a20297f3 -Author: Kelsey Gilbert <jdashg@gmail.com> -Date: Wed Aug 17 15:01:24 2022 -0700 - - Add test of gl_VertexID and gl_InstanceID. - - Includes details in new test about both state-of-specification, and known errata. - - Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1779800 - -commit 92af460e46a82d60140b5a1df1379feb79730d3a -Author: Kelsey Gilbert <jdashg@gmail.com> -Date: Tue Sep 6 15:21:32 2022 -0700 - - Add test for getUniformIndices. - - Also test to ensure that it returns the correct id by checking the name from getActiveUniform. - - Firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1755973 + Remove more theora/.ogv from sdk/*. -Cherries picked +Above: cherries picked ================================================================================ -Merge base from: ups/main +Below: merge base from: ups/main -commit 4996b40a69857919579a12f828188c9f428c402c -Author: Alexey Knyazev <3479527+lexaknyazev@users.noreply.github.com> -Date: Sat Aug 20 01:53:01 2022 +0400 +commit 4c5b8bfe586d983fae6a0571cc702f43e5f5b719 +Author: Ken Russell <kbrussel@alum.mit.edu> +Date: Tue Apr 2 14:17:01 2024 -0700 - Allow makeXRCompatible in OffscreenCanvas contexts (#3480) + Verify invalidated uniform locations when there is no current program. (#3633) - Fixes failures of: - conformance/offscreencanvas/methods.html - conformance/offscreencanvas/methods-worker.html + Associated with crbug.com/331119482 . diff --git a/dom/canvas/test/webgl-conf/generated-mochitest.toml b/dom/canvas/test/webgl-conf/generated-mochitest.toml index 0cee17acf4..e63ef0abfd 100644 --- a/dom/canvas/test/webgl-conf/generated-mochitest.toml +++ b/dom/canvas/test/webgl-conf/generated-mochitest.toml @@ -65,6 +65,7 @@ support-files = [ "checkout/conformance/canvas/draw-webgl-to-canvas-test.html", "checkout/conformance/canvas/drawingbuffer-hd-dpi-test.html", "checkout/conformance/canvas/drawingbuffer-static-canvas-test.html", + "checkout/conformance/canvas/drawingbuffer-storage-test.html", "checkout/conformance/canvas/drawingbuffer-test.html", "checkout/conformance/canvas/framebuffer-bindings-affected-by-to-data-url.html", "checkout/conformance/canvas/framebuffer-bindings-unaffected-on-resize.html", @@ -104,15 +105,19 @@ support-files = [ "checkout/conformance/extensions/angle-instanced-arrays-out-of-bounds.html", "checkout/conformance/extensions/angle-instanced-arrays.html", "checkout/conformance/extensions/ext-blend-minmax.html", + "checkout/conformance/extensions/ext-clip-control.html", "checkout/conformance/extensions/ext-color-buffer-half-float.html", + "checkout/conformance/extensions/ext-depth-clamp.html", "checkout/conformance/extensions/ext-disjoint-timer-query.html", "checkout/conformance/extensions/ext-float-blend.html", "checkout/conformance/extensions/ext-frag-depth.html", + "checkout/conformance/extensions/ext-polygon-offset-clamp.html", "checkout/conformance/extensions/ext-sRGB.html", "checkout/conformance/extensions/ext-shader-texture-lod.html", "checkout/conformance/extensions/ext-texture-compression-bptc.html", "checkout/conformance/extensions/ext-texture-compression-rgtc.html", "checkout/conformance/extensions/ext-texture-filter-anisotropic.html", + "checkout/conformance/extensions/ext-texture-mirror-clamp-to-edge.html", "checkout/conformance/extensions/get-extension.html", "checkout/conformance/extensions/khr-parallel-shader-compile.html", "checkout/conformance/extensions/oes-element-index-uint.html", @@ -133,6 +138,7 @@ support-files = [ "checkout/conformance/extensions/oes-vertex-array-object-bufferData.html", "checkout/conformance/extensions/oes-vertex-array-object.html", "checkout/conformance/extensions/s3tc-and-rgtc.html", + "checkout/conformance/extensions/webgl-blend-func-extended.html", "checkout/conformance/extensions/webgl-compressed-texture-astc.html", "checkout/conformance/extensions/webgl-compressed-texture-etc.html", "checkout/conformance/extensions/webgl-compressed-texture-etc1.html", @@ -147,6 +153,7 @@ support-files = [ "checkout/conformance/extensions/webgl-draw-buffers-max-draw-buffers.html", "checkout/conformance/extensions/webgl-draw-buffers.html", "checkout/conformance/extensions/webgl-multi-draw.html", + "checkout/conformance/extensions/webgl-polygon-mode.html", "checkout/conformance/extensions/webgl-webcodecs-video-frame.html", "checkout/conformance/glsl/00_test_list.txt", "checkout/conformance/glsl/bugs/00_test_list.txt", @@ -2597,6 +2604,7 @@ support-files = [ "checkout/conformance2/buffers/uniform-buffers.html", "checkout/conformance2/canvas/00_test_list.txt", "checkout/conformance2/canvas/compositing.html", + "checkout/conformance2/canvas/drawingbuffer-storage-test.html", "checkout/conformance2/canvas/to-data-url-with-pack-params.html", "checkout/conformance2/context/00_test_list.txt", "checkout/conformance2/context/constants-and-properties-2.html", @@ -2611,10 +2619,15 @@ support-files = [ "checkout/conformance2/extensions/00_test_list.txt", "checkout/conformance2/extensions/ext-color-buffer-float.html", "checkout/conformance2/extensions/ext-color-buffer-half-float.html", + "checkout/conformance2/extensions/ext-conservative-depth.html", "checkout/conformance2/extensions/ext-disjoint-timer-query-webgl2.html", + "checkout/conformance2/extensions/ext-render-snorm.html", "checkout/conformance2/extensions/ext-texture-filter-anisotropic.html", "checkout/conformance2/extensions/ext-texture-norm16.html", + "checkout/conformance2/extensions/nv-shader-noperspective-interpolation.html", "checkout/conformance2/extensions/oes-draw-buffers-indexed.html", + "checkout/conformance2/extensions/oes-sample-variables.html", + "checkout/conformance2/extensions/oes-shader-multisample-interpolation.html", "checkout/conformance2/extensions/ovr_multiview2.html", "checkout/conformance2/extensions/ovr_multiview2_depth.html", "checkout/conformance2/extensions/ovr_multiview2_draw_buffers.html", @@ -2627,7 +2640,13 @@ support-files = [ "checkout/conformance2/extensions/promoted-extensions-in-shaders.html", "checkout/conformance2/extensions/promoted-extensions.html", "checkout/conformance2/extensions/required-extensions.html", + "checkout/conformance2/extensions/webgl-blend-func-extended.html", + "checkout/conformance2/extensions/webgl-clip-cull-distance.html", "checkout/conformance2/extensions/webgl-multi-draw-instanced-base-vertex-base-instance.html", + "checkout/conformance2/extensions/webgl-provoking-vertex.html", + "checkout/conformance2/extensions/webgl-render-shared-exponent.html", + "checkout/conformance2/extensions/webgl-shader-pixel-local-storage.html", + "checkout/conformance2/extensions/webgl-stencil-texturing.html", "checkout/conformance2/glsl3/00_test_list.txt", "checkout/conformance2/glsl3/array-as-return-value.html", "checkout/conformance2/glsl3/array-assign-constructor.html", @@ -2672,6 +2691,7 @@ support-files = [ "checkout/conformance2/glsl3/shader-with-mis-matching-uniform-block.html", "checkout/conformance2/glsl3/short-circuiting-in-loop-condition.html", "checkout/conformance2/glsl3/switch-case.html", + "checkout/conformance2/glsl3/texture-bias.html", "checkout/conformance2/glsl3/texture-offset-non-constant-offset.html", "checkout/conformance2/glsl3/texture-offset-out-of-range.html", "checkout/conformance2/glsl3/texture-offset-uniform-texture-coordinate.html", @@ -2714,6 +2734,7 @@ support-files = [ "checkout/conformance2/programs/gl-get-frag-data-location.html", "checkout/conformance2/programs/sampler-uniforms.html", "checkout/conformance2/query/00_test_list.txt", + "checkout/conformance2/query/occlusion-query-scissor.html", "checkout/conformance2/query/occlusion-query.html", "checkout/conformance2/query/query.html", "checkout/conformance2/reading/00_test_list.txt", @@ -2747,7 +2768,6 @@ support-files = [ "checkout/conformance2/rendering/blitframebuffer-stencil-only.html", "checkout/conformance2/rendering/blitframebuffer-test.html", "checkout/conformance2/rendering/blitframebuffer-unaffected-by-colormask.html", - "checkout/conformance2/rendering/builtin-vert-attribs.html", "checkout/conformance2/rendering/canvas-resizing-with-pbo-bound.html", "checkout/conformance2/rendering/clear-func-buffer-type-match.html", "checkout/conformance2/rendering/clear-srgb-color-buffer.html", @@ -3511,6 +3531,7 @@ support-files = [ "checkout/conformance2/textures/misc/tex-3d-mipmap-levels-intel-bug.html", "checkout/conformance2/textures/misc/tex-3d-size-limit.html", "checkout/conformance2/textures/misc/tex-base-level-bug.html", + "checkout/conformance2/textures/misc/tex-image-10bpc.html", "checkout/conformance2/textures/misc/tex-image-and-sub-image-with-array-buffer-view-sub-source.html", "checkout/conformance2/textures/misc/tex-image-with-bad-args-from-dom-elements.html", "checkout/conformance2/textures/misc/tex-image-with-bad-args.html", @@ -3758,6 +3779,19 @@ support-files = [ "checkout/conformance2/vertex_arrays/00_test_list.txt", "checkout/conformance2/vertex_arrays/vertex-array-object-and-disabled-attributes.html", "checkout/conformance2/vertex_arrays/vertex-array-object.html", + "checkout/conformance2/wasm/00_test_list.txt", + "checkout/conformance2/wasm/bufferdata-16gb-wasm-memory.html", + "checkout/conformance2/wasm/bufferdata-4gb-wasm-memory.html", + "checkout/conformance2/wasm/buffersubdata-16gb-wasm-memory.html", + "checkout/conformance2/wasm/buffersubdata-4gb-wasm-memory.html", + "checkout/conformance2/wasm/getbuffersubdata-16gb-wasm-memory.html", + "checkout/conformance2/wasm/getbuffersubdata-4gb-wasm-memory.html", + "checkout/conformance2/wasm/readpixels-16gb-wasm-memory.html", + "checkout/conformance2/wasm/readpixels-4gb-wasm-memory.html", + "checkout/conformance2/wasm/teximage2d-16gb-wasm-memory.html", + "checkout/conformance2/wasm/teximage2d-4gb-wasm-memory.html", + "checkout/conformance2/wasm/texsubimage2d-16gb-wasm-memory.html", + "checkout/conformance2/wasm/texsubimage2d-4gb-wasm-memory.html", "checkout/deqp/00_test_list.txt", "checkout/deqp/LICENSE", "checkout/deqp/README.md", @@ -5018,6 +5052,7 @@ support-files = [ "checkout/js/tests/compressed-tex-image.js", "checkout/js/tests/compressed-texture-utils.js", "checkout/js/tests/context-methods.js", + "checkout/js/tests/drawingbuffer-storage-test.js", "checkout/js/tests/ext-color-buffer-half-float.js", "checkout/js/tests/ext-float-blend.js", "checkout/js/tests/ext-texture-filter-anisotropic.js", @@ -5068,6 +5103,7 @@ support-files = [ "checkout/js/tests/texture-corner-case-videos.js", "checkout/js/tests/typed-array-test-cases.js", "checkout/js/tests/typed-array-worker.js", + "checkout/js/tests/webgl-blend-func-extended.js", "checkout/js/tests/webgl-compressed-texture-size-limit.js", "checkout/js/tests/webgl-draw-buffers-utils.js", "checkout/js/webgl-test-harness.js", @@ -5090,18 +5126,18 @@ support-files = [ "checkout/resources/exif-orientation-originals/exif-orientation-test-3-rotate-180.jpg", "checkout/resources/exif-orientation-originals/exif-orientation-test-4-mirror-vertical.jpg", "checkout/resources/exif-orientation-originals/exif-orientation-test-5-mirror-horizontal-90-ccw.jpg", - "checkout/resources/exif-orientation-originals/exif-orientation-test-6-90-ccw.jpg", + "checkout/resources/exif-orientation-originals/exif-orientation-test-6-90-cw.jpg", "checkout/resources/exif-orientation-originals/exif-orientation-test-7-mirror-horizontal-90-cw.jpg", - "checkout/resources/exif-orientation-originals/exif-orientation-test-8-90-cw.jpg", + "checkout/resources/exif-orientation-originals/exif-orientation-test-8-90-ccw.jpg", "checkout/resources/exif-orientation-originals/exif-orientation-test.psd", "checkout/resources/exif-orientation-test-1-normal.jpg", "checkout/resources/exif-orientation-test-2-mirror-horizontal.jpg", "checkout/resources/exif-orientation-test-3-rotate-180.jpg", "checkout/resources/exif-orientation-test-4-mirror-vertical.jpg", "checkout/resources/exif-orientation-test-5-mirror-horizontal-90-ccw.jpg", - "checkout/resources/exif-orientation-test-6-90-ccw.jpg", + "checkout/resources/exif-orientation-test-6-90-cw.jpg", "checkout/resources/exif-orientation-test-7-mirror-horizontal-90-cw.jpg", - "checkout/resources/exif-orientation-test-8-90-cw.jpg", + "checkout/resources/exif-orientation-test-8-90-ccw.jpg", "checkout/resources/floatUniformShader.vert", "checkout/resources/fragmentShader.frag", "checkout/resources/glsl-feature-tests.css", @@ -5128,10 +5164,10 @@ support-files = [ "checkout/resources/noopUniformShaderES3.vert", "checkout/resources/npot-video-1920x1080.mp4", "checkout/resources/npot-video.mp4", - "checkout/resources/npot-video.theora.ogv", "checkout/resources/npot-video.webmvp8.webm", "checkout/resources/ogles-tests.css", "checkout/resources/opengl_logo.jpg", + "checkout/resources/red-gradient-8x1-10bit-untagged.png", "checkout/resources/red-green-128x128-linear-profile.jpg", "checkout/resources/red-green-128x128-linear-profile.psd", "checkout/resources/red-green-480x272-sar-136x135-dar-16x9.mp4", @@ -5145,7 +5181,6 @@ support-files = [ "checkout/resources/red-green.mp4", "checkout/resources/red-green.png", "checkout/resources/red-green.svg", - "checkout/resources/red-green.theora.ogv", "checkout/resources/red-green.webmvp8.webm", "checkout/resources/red-indexed.png", "checkout/resources/samplerForWebGL2UniformShader.frag", @@ -5254,6 +5289,9 @@ subsuite = "webgl2-core" ["generated/test_2_conformance2__buffers__uniform-buffers.html"] subsuite = "webgl2-core" +["generated/test_2_conformance2__canvas__drawingbuffer-storage-test.html"] +subsuite = "webgl2-core" + ["generated/test_2_conformance2__canvas__to-data-url-with-pack-params.html"] subsuite = "webgl2-core" @@ -5295,9 +5333,15 @@ skip-if = ["os == 'mac' && debug"] ["generated/test_2_conformance2__extensions__ext-color-buffer-half-float.html"] subsuite = "webgl2-core" +["generated/test_2_conformance2__extensions__ext-conservative-depth.html"] +subsuite = "webgl2-core" + ["generated/test_2_conformance2__extensions__ext-disjoint-timer-query-webgl2.html"] subsuite = "webgl2-core" +["generated/test_2_conformance2__extensions__ext-render-snorm.html"] +subsuite = "webgl2-core" + ["generated/test_2_conformance2__extensions__ext-texture-filter-anisotropic.html"] subsuite = "webgl2-core" @@ -5309,9 +5353,18 @@ fail-if = [ "os == 'linux'", ] +["generated/test_2_conformance2__extensions__nv-shader-noperspective-interpolation.html"] +subsuite = "webgl2-core" + ["generated/test_2_conformance2__extensions__oes-draw-buffers-indexed.html"] subsuite = "webgl2-core" +["generated/test_2_conformance2__extensions__oes-sample-variables.html"] +subsuite = "webgl2-core" + +["generated/test_2_conformance2__extensions__oes-shader-multisample-interpolation.html"] +subsuite = "webgl2-core" + ["generated/test_2_conformance2__extensions__ovr_multiview2.html"] subsuite = "webgl2-core" fail-if = ["os == 'win'"] @@ -5357,10 +5410,28 @@ subsuite = "webgl2-core" subsuite = "webgl2-core" fail-if = ["os == 'linux'"] +["generated/test_2_conformance2__extensions__webgl-blend-func-extended.html"] +subsuite = "webgl2-core" + +["generated/test_2_conformance2__extensions__webgl-clip-cull-distance.html"] +subsuite = "webgl2-core" + ["generated/test_2_conformance2__extensions__webgl-multi-draw-instanced-base-vertex-base-instance.html"] subsuite = "webgl2-core" fail-if = ["os == 'mac' && !apple_silicon"] +["generated/test_2_conformance2__extensions__webgl-provoking-vertex.html"] +subsuite = "webgl2-core" + +["generated/test_2_conformance2__extensions__webgl-render-shared-exponent.html"] +subsuite = "webgl2-core" + +["generated/test_2_conformance2__extensions__webgl-shader-pixel-local-storage.html"] +subsuite = "webgl2-core" + +["generated/test_2_conformance2__extensions__webgl-stencil-texturing.html"] +subsuite = "webgl2-core" + ["generated/test_2_conformance2__glsl3__array-as-return-value.html"] subsuite = "webgl2-ext" @@ -5493,6 +5564,10 @@ subsuite = "webgl2-ext" subsuite = "webgl2-ext" fail-if = ["os == 'win'"] +["generated/test_2_conformance2__glsl3__texture-bias.html"] +subsuite = "webgl2-ext" +fail-if = ["os == 'win'"] + ["generated/test_2_conformance2__glsl3__texture-offset-non-constant-offset.html"] subsuite = "webgl2-ext" @@ -5620,6 +5695,9 @@ fail-if = ["os == 'android'"] ["generated/test_2_conformance2__programs__sampler-uniforms.html"] subsuite = "webgl2-core" +["generated/test_2_conformance2__query__occlusion-query-scissor.html"] +subsuite = "webgl2-core" + ["generated/test_2_conformance2__query__occlusion-query.html"] subsuite = "webgl2-core" skip-if = ["true"] @@ -5721,9 +5799,6 @@ subsuite = "webgl2-core" ["generated/test_2_conformance2__rendering__blitframebuffer-unaffected-by-colormask.html"] subsuite = "webgl2-core" -["generated/test_2_conformance2__rendering__builtin-vert-attribs.html"] -subsuite = "webgl2-core" - ["generated/test_2_conformance2__rendering__canvas-resizing-with-pbo-bound.html"] subsuite = "webgl2-core" @@ -8083,6 +8158,10 @@ subsuite = "webgl2-core" subsuite = "webgl2-core" fail-if = ["os == 'mac'"] # macosx1014 due to 1563418 +["generated/test_2_conformance2__textures__misc__tex-image-10bpc.html"] +subsuite = "webgl2-core" +fail-if = ["true"] + ["generated/test_2_conformance2__textures__misc__tex-image-and-sub-image-with-array-buffer-view-sub-source.html"] subsuite = "webgl2-core" @@ -8875,6 +8954,54 @@ subsuite = "webgl2-core" ["generated/test_2_conformance2__vertex_arrays__vertex-array-object.html"] subsuite = "webgl2-core" +["generated/test_2_conformance2__wasm__bufferdata-16gb-wasm-memory.html"] +subsuite = "webgl2-core" +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__bufferdata-4gb-wasm-memory.html"] +subsuite = "webgl2-core" +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__buffersubdata-16gb-wasm-memory.html"] +subsuite = "webgl2-core" +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__buffersubdata-4gb-wasm-memory.html"] +subsuite = "webgl2-core" +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__getbuffersubdata-16gb-wasm-memory.html"] +subsuite = "webgl2-core" +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__getbuffersubdata-4gb-wasm-memory.html"] +subsuite = "webgl2-core" +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__readpixels-16gb-wasm-memory.html"] +subsuite = "webgl2-core" +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__readpixels-4gb-wasm-memory.html"] +subsuite = "webgl2-core" +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__teximage2d-16gb-wasm-memory.html"] +subsuite = "webgl2-core" +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__teximage2d-4gb-wasm-memory.html"] +subsuite = "webgl2-core" +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__texsubimage2d-16gb-wasm-memory.html"] +subsuite = "webgl2-core" +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__texsubimage2d-4gb-wasm-memory.html"] +subsuite = "webgl2-core" +fail-if = ["true"] + ["generated/test_2_conformance__attribs__gl-bindAttribLocation-aliasing.html"] subsuite = "webgl2-core" @@ -9067,15 +9194,24 @@ subsuite = "webgl2-core" ["generated/test_2_conformance__context__zero-sized-canvas.html"] subsuite = "webgl2-core" +["generated/test_2_conformance__extensions__ext-clip-control.html"] +subsuite = "webgl2-core" + ["generated/test_2_conformance__extensions__ext-color-buffer-half-float.html"] subsuite = "webgl2-core" +["generated/test_2_conformance__extensions__ext-depth-clamp.html"] +subsuite = "webgl2-core" + ["generated/test_2_conformance__extensions__ext-disjoint-timer-query.html"] subsuite = "webgl2-core" ["generated/test_2_conformance__extensions__ext-float-blend.html"] subsuite = "webgl2-core" +["generated/test_2_conformance__extensions__ext-polygon-offset-clamp.html"] +subsuite = "webgl2-core" + ["generated/test_2_conformance__extensions__ext-texture-compression-bptc.html"] subsuite = "webgl2-core" @@ -9085,6 +9221,9 @@ subsuite = "webgl2-core" ["generated/test_2_conformance__extensions__ext-texture-filter-anisotropic.html"] subsuite = "webgl2-core" +["generated/test_2_conformance__extensions__ext-texture-mirror-clamp-to-edge.html"] +subsuite = "webgl2-core" + ["generated/test_2_conformance__extensions__get-extension.html"] subsuite = "webgl2-core" @@ -9125,6 +9264,9 @@ subsuite = "webgl2-core" ["generated/test_2_conformance__extensions__webgl-multi-draw.html"] subsuite = "webgl2-core" +["generated/test_2_conformance__extensions__webgl-polygon-mode.html"] +subsuite = "webgl2-core" + ["generated/test_2_conformance__glsl__bugs__angle-ambiguous-function-call.html"] subsuite = "webgl2-ext" @@ -14187,9 +14329,15 @@ skip-if = ["os == 'mac' && os_version == '10.15'"] ["generated/test_conformance__extensions__ext-blend-minmax.html"] subsuite = "webgl1-core" +["generated/test_conformance__extensions__ext-clip-control.html"] +subsuite = "webgl1-core" + ["generated/test_conformance__extensions__ext-color-buffer-half-float.html"] subsuite = "webgl1-core" +["generated/test_conformance__extensions__ext-depth-clamp.html"] +subsuite = "webgl1-core" + ["generated/test_conformance__extensions__ext-disjoint-timer-query.html"] subsuite = "webgl1-core" @@ -14199,6 +14347,9 @@ subsuite = "webgl1-core" ["generated/test_conformance__extensions__ext-frag-depth.html"] subsuite = "webgl1-core" +["generated/test_conformance__extensions__ext-polygon-offset-clamp.html"] +subsuite = "webgl1-core" + ["generated/test_conformance__extensions__ext-sRGB.html"] subsuite = "webgl1-core" fail-if = ["os == 'android'"] @@ -14215,6 +14366,9 @@ subsuite = "webgl1-core" ["generated/test_conformance__extensions__ext-texture-filter-anisotropic.html"] subsuite = "webgl1-core" +["generated/test_conformance__extensions__ext-texture-mirror-clamp-to-edge.html"] +subsuite = "webgl1-core" + ["generated/test_conformance__extensions__get-extension.html"] subsuite = "webgl1-core" @@ -14278,6 +14432,9 @@ subsuite = "webgl1-core" ["generated/test_conformance__extensions__s3tc-and-rgtc.html"] subsuite = "webgl1-core" +["generated/test_conformance__extensions__webgl-blend-func-extended.html"] +subsuite = "webgl1-core" + ["generated/test_conformance__extensions__webgl-compressed-texture-astc.html"] subsuite = "webgl1-core" @@ -14320,6 +14477,9 @@ subsuite = "webgl1-core" ["generated/test_conformance__extensions__webgl-multi-draw.html"] subsuite = "webgl1-core" +["generated/test_conformance__extensions__webgl-polygon-mode.html"] +subsuite = "webgl1-core" + ["generated/test_conformance__glsl__bugs__angle-ambiguous-function-call.html"] subsuite = "webgl1-ext" @@ -16705,7 +16865,7 @@ subsuite = "webgl1-core" skip-if = [ "win11_2009", # win11 - 50/50 intermittent "os == 'android' && android_version == '33'", #Bug 1873144 - ] + ] ["generated/test_conformance__textures__misc__texture-npot.html"] subsuite = "webgl1-core" diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__canvas__drawingbuffer-storage-test.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__canvas__drawingbuffer-storage-test.html new file mode 100644 index 0000000000..00fba64f26 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__canvas__drawingbuffer-storage-test.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/canvas/drawingbuffer-storage-test.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__ext-conservative-depth.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__ext-conservative-depth.html new file mode 100644 index 0000000000..2ae10d93fe --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__ext-conservative-depth.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/extensions/ext-conservative-depth.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__rendering__builtin-vert-attribs.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__ext-render-snorm.html index 0c6200a9a9..aa47f840a0 100644 --- a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__rendering__builtin-vert-attribs.html +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__ext-render-snorm.html @@ -12,6 +12,6 @@ <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> </head> <body> - <iframe src='../mochi-single.html?checkout/conformance2/rendering/builtin-vert-attribs.html?webglVersion=2'></iframe> + <iframe src='../mochi-single.html?checkout/conformance2/extensions/ext-render-snorm.html?webglVersion=2'></iframe> </body> </html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__nv-shader-noperspective-interpolation.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__nv-shader-noperspective-interpolation.html new file mode 100644 index 0000000000..c4e820eaae --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__nv-shader-noperspective-interpolation.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/extensions/nv-shader-noperspective-interpolation.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__oes-sample-variables.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__oes-sample-variables.html new file mode 100644 index 0000000000..a7582e4fa4 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__oes-sample-variables.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/extensions/oes-sample-variables.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__oes-shader-multisample-interpolation.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__oes-shader-multisample-interpolation.html new file mode 100644 index 0000000000..f7fae5d1c5 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__oes-shader-multisample-interpolation.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/extensions/oes-shader-multisample-interpolation.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-blend-func-extended.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-blend-func-extended.html new file mode 100644 index 0000000000..d0ad1a13bf --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-blend-func-extended.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/extensions/webgl-blend-func-extended.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-clip-cull-distance.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-clip-cull-distance.html new file mode 100644 index 0000000000..f2b113ac78 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-clip-cull-distance.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/extensions/webgl-clip-cull-distance.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-provoking-vertex.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-provoking-vertex.html new file mode 100644 index 0000000000..132519dff3 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-provoking-vertex.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/extensions/webgl-provoking-vertex.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-render-shared-exponent.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-render-shared-exponent.html new file mode 100644 index 0000000000..7d4a15493b --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-render-shared-exponent.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/extensions/webgl-render-shared-exponent.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-shader-pixel-local-storage.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-shader-pixel-local-storage.html new file mode 100644 index 0000000000..4b30aadc2b --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-shader-pixel-local-storage.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/extensions/webgl-shader-pixel-local-storage.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-stencil-texturing.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-stencil-texturing.html new file mode 100644 index 0000000000..f867f36977 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__extensions__webgl-stencil-texturing.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/extensions/webgl-stencil-texturing.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__glsl3__texture-bias.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__glsl3__texture-bias.html new file mode 100644 index 0000000000..6ec1ed29d4 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__glsl3__texture-bias.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/glsl3/texture-bias.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__query__occlusion-query-scissor.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__query__occlusion-query-scissor.html new file mode 100644 index 0000000000..9337ee8fd4 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__query__occlusion-query-scissor.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/query/occlusion-query-scissor.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__textures__misc__tex-image-10bpc.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__textures__misc__tex-image-10bpc.html new file mode 100644 index 0000000000..c6f5f90019 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__textures__misc__tex-image-10bpc.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/textures/misc/tex-image-10bpc.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__bufferdata-16gb-wasm-memory.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__bufferdata-16gb-wasm-memory.html new file mode 100644 index 0000000000..d005e42f84 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__bufferdata-16gb-wasm-memory.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/wasm/bufferdata-16gb-wasm-memory.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__bufferdata-4gb-wasm-memory.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__bufferdata-4gb-wasm-memory.html new file mode 100644 index 0000000000..81d331a5be --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__bufferdata-4gb-wasm-memory.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/wasm/bufferdata-4gb-wasm-memory.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__buffersubdata-16gb-wasm-memory.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__buffersubdata-16gb-wasm-memory.html new file mode 100644 index 0000000000..09b63204a7 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__buffersubdata-16gb-wasm-memory.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/wasm/buffersubdata-16gb-wasm-memory.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__buffersubdata-4gb-wasm-memory.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__buffersubdata-4gb-wasm-memory.html new file mode 100644 index 0000000000..6d0240794b --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__buffersubdata-4gb-wasm-memory.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/wasm/buffersubdata-4gb-wasm-memory.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__getbuffersubdata-16gb-wasm-memory.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__getbuffersubdata-16gb-wasm-memory.html new file mode 100644 index 0000000000..3bbe93a68a --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__getbuffersubdata-16gb-wasm-memory.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/wasm/getbuffersubdata-16gb-wasm-memory.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__getbuffersubdata-4gb-wasm-memory.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__getbuffersubdata-4gb-wasm-memory.html new file mode 100644 index 0000000000..d310a733f6 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__getbuffersubdata-4gb-wasm-memory.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/wasm/getbuffersubdata-4gb-wasm-memory.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__readpixels-16gb-wasm-memory.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__readpixels-16gb-wasm-memory.html new file mode 100644 index 0000000000..342b9e7015 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__readpixels-16gb-wasm-memory.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/wasm/readpixels-16gb-wasm-memory.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__readpixels-4gb-wasm-memory.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__readpixels-4gb-wasm-memory.html new file mode 100644 index 0000000000..d772a2824a --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__readpixels-4gb-wasm-memory.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/wasm/readpixels-4gb-wasm-memory.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__teximage2d-16gb-wasm-memory.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__teximage2d-16gb-wasm-memory.html new file mode 100644 index 0000000000..5ce7bdb9bd --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__teximage2d-16gb-wasm-memory.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/wasm/teximage2d-16gb-wasm-memory.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__teximage2d-4gb-wasm-memory.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__teximage2d-4gb-wasm-memory.html new file mode 100644 index 0000000000..135f5b4a32 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__teximage2d-4gb-wasm-memory.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/wasm/teximage2d-4gb-wasm-memory.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__texsubimage2d-16gb-wasm-memory.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__texsubimage2d-16gb-wasm-memory.html new file mode 100644 index 0000000000..eca227b3a5 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__texsubimage2d-16gb-wasm-memory.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/wasm/texsubimage2d-16gb-wasm-memory.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__texsubimage2d-4gb-wasm-memory.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__texsubimage2d-4gb-wasm-memory.html new file mode 100644 index 0000000000..b5b2d1cc6f --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance2__wasm__texsubimage2d-4gb-wasm-memory.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance2/wasm/texsubimage2d-4gb-wasm-memory.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__ext-clip-control.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__ext-clip-control.html new file mode 100644 index 0000000000..0cdc6e1101 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__ext-clip-control.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance/extensions/ext-clip-control.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__ext-depth-clamp.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__ext-depth-clamp.html new file mode 100644 index 0000000000..9cb87c0570 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__ext-depth-clamp.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance/extensions/ext-depth-clamp.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__ext-polygon-offset-clamp.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__ext-polygon-offset-clamp.html new file mode 100644 index 0000000000..b0df2ff3bc --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__ext-polygon-offset-clamp.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance/extensions/ext-polygon-offset-clamp.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__ext-texture-mirror-clamp-to-edge.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__ext-texture-mirror-clamp-to-edge.html new file mode 100644 index 0000000000..470477e4de --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__ext-texture-mirror-clamp-to-edge.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance/extensions/ext-texture-mirror-clamp-to-edge.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__webgl-polygon-mode.html b/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__webgl-polygon-mode.html new file mode 100644 index 0000000000..642d0dc652 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_2_conformance__extensions__webgl-polygon-mode.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance/extensions/webgl-polygon-mode.html?webglVersion=2'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__ext-clip-control.html b/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__ext-clip-control.html new file mode 100644 index 0000000000..7fb1b875ce --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__ext-clip-control.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance/extensions/ext-clip-control.html'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__ext-depth-clamp.html b/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__ext-depth-clamp.html new file mode 100644 index 0000000000..f6162ae15f --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__ext-depth-clamp.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance/extensions/ext-depth-clamp.html'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__ext-polygon-offset-clamp.html b/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__ext-polygon-offset-clamp.html new file mode 100644 index 0000000000..b1a736ebb3 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__ext-polygon-offset-clamp.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance/extensions/ext-polygon-offset-clamp.html'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__ext-texture-mirror-clamp-to-edge.html b/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__ext-texture-mirror-clamp-to-edge.html new file mode 100644 index 0000000000..d13072949f --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__ext-texture-mirror-clamp-to-edge.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance/extensions/ext-texture-mirror-clamp-to-edge.html'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__webgl-blend-func-extended.html b/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__webgl-blend-func-extended.html new file mode 100644 index 0000000000..66d1fcefda --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__webgl-blend-func-extended.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance/extensions/webgl-blend-func-extended.html'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__webgl-polygon-mode.html b/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__webgl-polygon-mode.html new file mode 100644 index 0000000000..c7783dc938 --- /dev/null +++ b/dom/canvas/test/webgl-conf/generated/test_conformance__extensions__webgl-polygon-mode.html @@ -0,0 +1,17 @@ +<!-- GENERATED FILE, DO NOT EDIT --> +<!DOCTYPE HTML> +<html> + <head> + <meta charset='utf-8'/> + <title> + Mochitest wrapper for WebGL Conformance Test Suite tests + </title> + <link rel='stylesheet' type='text/css' href='../iframe-passthrough.css'/> + + <script src='/tests/SimpleTest/SimpleTest.js'></script> + <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/> + </head> + <body> + <iframe src='../mochi-single.html?checkout/conformance/extensions/webgl-polygon-mode.html'></iframe> + </body> +</html> diff --git a/dom/canvas/test/webgl-conf/mochitest-errata.toml b/dom/canvas/test/webgl-conf/mochitest-errata.toml index 5bf2b3f89b..e036a20ba1 100644 --- a/dom/canvas/test/webgl-conf/mochitest-errata.toml +++ b/dom/canvas/test/webgl-conf/mochitest-errata.toml @@ -30,6 +30,68 @@ prefs = "media.seamless-looping-video=false" fail-if = ["true"] #################### +# Failures from 2024-04 CTS update + +["generated/test_2_conformance2__textures__misc__tex-image-10bpc.html"] +# gl.checkFramebufferStatus(gl.FRAMEBUFFER) should be 36053. Was 36054. +# uniquePixels.size should be >= 7. Was 1 (of type number). +fail-if = ["true"] + + +["generated/test_2_conformance2__wasm__bufferdata-16gb-wasm-memory.html"] +# successfullyParsed should be true (of type boolean). Was undefined (of type undefined). +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__bufferdata-4gb-wasm-memory.html"] +# successfullyParsed should be true (of type boolean). Was undefined (of type undefined). +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__buffersubdata-16gb-wasm-memory.html"] +# successfullyParsed should be true (of type boolean). Was undefined (of type undefined). +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__buffersubdata-4gb-wasm-memory.html"] +# successfullyParsed should be true (of type boolean). Was undefined (of type undefined). +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__getbuffersubdata-16gb-wasm-memory.html"] +# successfullyParsed should be true (of type boolean). Was undefined (of type undefined). +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__getbuffersubdata-4gb-wasm-memory.html"] +# successfullyParsed should be true (of type boolean). Was undefined (of type undefined). +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__readpixels-16gb-wasm-memory.html"] +# successfullyParsed should be true (of type boolean). Was undefined (of type undefined). +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__readpixels-4gb-wasm-memory.html"] +# successfullyParsed should be true (of type boolean). Was undefined (of type undefined). +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__teximage2d-16gb-wasm-memory.html"] +# successfullyParsed should be true (of type boolean). Was undefined (of type undefined). +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__teximage2d-4gb-wasm-memory.html"] +# successfullyParsed should be true (of type boolean). Was undefined (of type undefined). +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__texsubimage2d-16gb-wasm-memory.html"] +# successfullyParsed should be true (of type boolean). Was undefined (of type undefined). +fail-if = ["true"] + +["generated/test_2_conformance2__wasm__texsubimage2d-4gb-wasm-memory.html"] +# successfullyParsed should be true (of type boolean). Was undefined (of type undefined). +fail-if = ["true"] + +["generated/test_2_conformance2__glsl3__texture-bias.html"] +# should be: 3,3,3,3 +# should be: 13,13,13,13 +fail-if = ["os == 'win'"] + +#################### # Bugs surfaced during fx106 CTS update ["generated/test_conformance__programs__program-handling.html"] diff --git a/dom/canvas/test/webgl-conf/moz.yaml b/dom/canvas/test/webgl-conf/moz.yaml index 7ebde7dcb2..fc40ba1b4d 100644 --- a/dom/canvas/test/webgl-conf/moz.yaml +++ b/dom/canvas/test/webgl-conf/moz.yaml @@ -13,8 +13,8 @@ origin: license: MIT - release: commit 4996b40a69857919579a12f828188c9f428c402c Fri Aug 19 14:53:01 2022 -0700 - revision: 4996b40a69857919579a12f828188c9f428c402c + release: commit 4c5b8bfe586d983fae6a0571cc702f43e5f5b719 Tue Apr 2 14:17:01 2024 -0700 + revision: 4c5b8bfe586d983fae6a0571cc702f43e5f5b719 updatebot: diff --git a/dom/canvas/test/webgl-mochitest/mochitest.toml b/dom/canvas/test/webgl-mochitest/mochitest.toml index 88e2fab88b..88b9b8ae91 100644 --- a/dom/canvas/test/webgl-mochitest/mochitest.toml +++ b/dom/canvas/test/webgl-mochitest/mochitest.toml @@ -8,7 +8,6 @@ support-files = [ "webgl-util.js", "test_video_fastpath.js", "red-green.mp4", - "red-green.theora.ogv", "red-green.webmvp8.webm", "red-green.webmvp9.webm", ] @@ -199,20 +198,12 @@ support-files = ["blank_15000x10000.png"] ["test_video_fastpath_mp4.html"] skip-if = ["win11_2009 && bits == 32"] # No fast video path for h264 decoder (done in RDD, can't be read in content) -["test_video_fastpath_theora.html"] -skip-if = [ - "os == 'linux' && os_version == '18.04'", - "apple_catalina", - "apple_silicon", - "win11_2009 && bits == 32", # No fast video path for theora decoder (done in RDD, can't be read in content) -] - ["test_video_fastpath_vp8.html"] skip-if = [ "os == 'linux' && os_version == '18.04'", "apple_catalina", "apple_silicon", - "win11_2009 && bits == 32", # No fast video path for theora decoder (done in RDD, can't be read in content) + "win11_2009 && bits == 32", # No fast video path for vp8 decoder (done in RDD, can't be read in content) ] ["test_video_fastpath_vp9.html"] @@ -220,7 +211,7 @@ skip-if = [ "os == 'linux' && os_version == '18.04'", "apple_catalina", "apple_silicon", - "win11_2009 && bits == 32", # No fast video path for theora decoder (done in RDD, can't be read in content) + "win11_2009 && bits == 32", # No fast video path for vp9 decoder (done in RDD, can't be read in content) ] ["test_webgl2_alpha_luminance.html"] diff --git a/dom/canvas/test/webgl-mochitest/red-green.theora.ogv b/dom/canvas/test/webgl-mochitest/red-green.theora.ogv Binary files differdeleted file mode 100644 index 1543915a10..0000000000 --- a/dom/canvas/test/webgl-mochitest/red-green.theora.ogv +++ /dev/null diff --git a/dom/canvas/test/webgl-mochitest/test_video_fastpath_theora.html b/dom/canvas/test/webgl-mochitest/test_video_fastpath_theora.html deleted file mode 100644 index 4f4fbab88b..0000000000 --- a/dom/canvas/test/webgl-mochitest/test_video_fastpath_theora.html +++ /dev/null @@ -1,21 +0,0 @@ -<html> - <head> - <meta name="timeout" content="long"/> - <meta http-equiv="Content-type" content="text/html;charset=UTF-8"/> - <title>Video Fastpath upload test</title> - <script src="/tests/SimpleTest/SimpleTest.js"></script> - <script src="test_video_fastpath.js"></script> - <link rel="stylesheet" href="/tests/SimpleTest/test.css"> - </head> - <body> - <script> - - function runTest() { - startTest("red-green.theora.ogv"); - } - - SimpleTest.waitForExplicitFinish(); - SpecialPowers.pushPrefEnv({"set" : [["webgl.enable-privileged-extensions", true]]}, runTest); - </script> - </body> -</html> diff --git a/dom/chrome-webidl/ChromeUtils.webidl b/dom/chrome-webidl/ChromeUtils.webidl index 3ccb125a1e..e1a4c3bedf 100644 --- a/dom/chrome-webidl/ChromeUtils.webidl +++ b/dom/chrome-webidl/ChromeUtils.webidl @@ -975,6 +975,7 @@ dictionary PartitionKeyPatternDictionary { DOMString scheme; DOMString baseDomain; long port; + boolean foreignByAncestorContext; }; dictionary CompileScriptOptionsDictionary { diff --git a/dom/chrome-webidl/InspectorUtils.webidl b/dom/chrome-webidl/InspectorUtils.webidl index 64cd610804..b379118e8a 100644 --- a/dom/chrome-webidl/InspectorUtils.webidl +++ b/dom/chrome-webidl/InspectorUtils.webidl @@ -89,16 +89,24 @@ namespace InspectorUtils { sequence<DOMString> getRegisteredCssHighlights(Document document, optional boolean activeOnly = false); sequence<InspectorCSSPropertyDefinition> getCSSRegisteredProperties(Document document); - // Get the start and end offsets of the first rule body within initialText + // Get the first rule body text within initialText // Consider the following example: // p { // line-height: 2em; // color: blue; // } - // Calling the function with the whole text above would return offsets we can use to - // get "line-height: 2em; color: blue;" + // Calling the function with the whole text above would return: + // "line-height: 2em; color: blue;" // Returns null when opening curly bracket wasn't found in initialText - InspectorGetRuleBodyTextResult? getRuleBodyTextOffsets(UTF8String initialText); + UTF8String? getRuleBodyText(UTF8String initialText); + + // Returns string where the rule body text at passed line and column in styleSheetText + // is replaced by newBodyText. + UTF8String? replaceBlockRuleBodyTextInStylesheet( + UTF8String styleSheetText, + unsigned long line, + unsigned long column, + UTF8String newBodyText); }; dictionary SupportsOptions { @@ -177,11 +185,6 @@ dictionary InspectorCSSPropertyDefinition { required boolean fromJS; }; -dictionary InspectorGetRuleBodyTextResult { - required double startOffset; - required double endOffset; -}; - dictionary InspectorStyleSheetRuleCountAndAtRulesResult { required sequence<CSSRule> atRules; required unsigned long ruleCount; diff --git a/dom/chrome-webidl/PlacesObservers.webidl b/dom/chrome-webidl/PlacesObservers.webidl index 662d8558ff..77c917a3c5 100644 --- a/dom/chrome-webidl/PlacesObservers.webidl +++ b/dom/chrome-webidl/PlacesObservers.webidl @@ -11,6 +11,12 @@ interface PlacesWeakCallbackWrapper { constructor(PlacesEventCallback callback); }; +// Counters for number of events sent in the current session. +[ChromeOnly, Exposed=Window] +interface PlacesEventCounts { + readonly maplike<DOMString, unsigned long long>; +}; + // Global singleton which should handle all events for places. [ChromeOnly, Exposed=Window] namespace PlacesObservers { @@ -28,4 +34,6 @@ namespace PlacesObservers { PlacesWeakCallbackWrapper listener); [Throws] undefined notifyListeners(sequence<PlacesEvent> events); + + readonly attribute PlacesEventCounts counts; }; diff --git a/dom/chrome-webidl/StripOnShareRule.webidl b/dom/chrome-webidl/StripOnShareRule.webidl index 446070ed8b..c151dd06a2 100644 --- a/dom/chrome-webidl/StripOnShareRule.webidl +++ b/dom/chrome-webidl/StripOnShareRule.webidl @@ -10,6 +10,6 @@ */ [GenerateInitFromJSON] dictionary StripRule { - sequence<DOMString> queryParams = []; - sequence<DOMString> topLevelSites = []; + sequence<UTF8String> queryParams = []; + sequence<UTF8String> topLevelSites = []; }; diff --git a/dom/chrome-webidl/UniFFI.webidl b/dom/chrome-webidl/UniFFI.webidl index 7a3f68fbbd..e24fc9cc5d 100644 --- a/dom/chrome-webidl/UniFFI.webidl +++ b/dom/chrome-webidl/UniFFI.webidl @@ -35,7 +35,7 @@ typedef unsigned long long UniFFICallbackObjectHandle; // Opaque type used to represent a pointer from Rust [ChromeOnly, Exposed=Window] -interface UniFFIPointer {}; +interface UniFFIPointer { }; // Types that can be passed or returned from scaffolding functions // diff --git a/dom/credentialmanagement/identity/IdentityCredential.cpp b/dom/credentialmanagement/identity/IdentityCredential.cpp index 182974e81d..c7541a8d69 100644 --- a/dom/credentialmanagement/identity/IdentityCredential.cpp +++ b/dom/credentialmanagement/identity/IdentityCredential.cpp @@ -602,15 +602,14 @@ RefPtr<IdentityCredential::GetTokenPromise> IdentityCredential::FetchToken( MakeSafeRefPtr<InternalRequest>(tokenLocation, fragment); internalRequest->SetMethod("POST"_ns); URLParams bodyValue; - bodyValue.Set(u"account_id"_ns, aAccount.mId); - bodyValue.Set(u"client_id"_ns, aProvider.mClientId); + bodyValue.Set("account_id"_ns, NS_ConvertUTF16toUTF8(aAccount.mId)); + bodyValue.Set("client_id"_ns, aProvider.mClientId); if (aProvider.mNonce.WasPassed()) { - bodyValue.Set(u"nonce"_ns, aProvider.mNonce.Value()); + bodyValue.Set("nonce"_ns, aProvider.mNonce.Value()); } - bodyValue.Set(u"disclosure_text_shown"_ns, u"false"_ns); - nsString bodyString; - bodyValue.Serialize(bodyString, true); - nsCString bodyCString = NS_ConvertUTF16toUTF8(bodyString); + bodyValue.Set("disclosure_text_shown"_ns, "false"_ns); + nsAutoCString bodyCString; + bodyValue.Serialize(bodyCString, true); nsCOMPtr<nsIInputStream> streamBody; rv = NS_NewCStringInputStream(getter_AddRefs(streamBody), bodyCString); if (NS_FAILED(rv)) { @@ -1097,7 +1096,7 @@ already_AddRefed<Promise> IdentityCredential::LogoutRPs( nsContentPolicyType::TYPE_WEB_IDENTITY); RefPtr<Request> domRequest = new Request(global, std::move(internalRequest), nullptr); - RequestOrUSVString fetchInput; + RequestOrUTF8String fetchInput; fetchInput.SetAsRequest() = domRequest; RootedDictionary<RequestInit> requestInit(RootingCx()); IgnoredErrorResult error; diff --git a/dom/credentialmanagement/identity/IdentityNetworkHelpers.h b/dom/credentialmanagement/identity/IdentityNetworkHelpers.h index d812269612..e8107ef280 100644 --- a/dom/credentialmanagement/identity/IdentityNetworkHelpers.h +++ b/dom/credentialmanagement/identity/IdentityNetworkHelpers.h @@ -28,7 +28,7 @@ RefPtr<TPromise> FetchJSONStructure(Request* aRequest) { new typename TPromise::Private(__func__); // Fetch the provided request - RequestOrUSVString fetchInput; + RequestOrUTF8String fetchInput; fetchInput.SetAsRequest() = aRequest; RootedDictionary<RequestInit> requestInit(RootingCx()); IgnoredErrorResult error; diff --git a/dom/events/Clipboard.cpp b/dom/events/Clipboard.cpp index b163bc816f..b797f93961 100644 --- a/dom/events/Clipboard.cpp +++ b/dom/events/Clipboard.cpp @@ -132,6 +132,11 @@ class ClipboardGetCallbackForRead final : public ClipboardGetCallback { } RefPtr<Promise> p(std::move(mPromise)); + if (entries.IsEmpty()) { + p->MaybeResolve(nsTArray<RefPtr<ClipboardItem>>{}); + return NS_OK; + } + // We currently only support one clipboard item. p->MaybeResolve( AutoTArray<RefPtr<ClipboardItem>, 1>{MakeRefPtr<ClipboardItem>( diff --git a/dom/events/EventDispatcher.cpp b/dom/events/EventDispatcher.cpp index dd3de015b4..61e5adf6f1 100644 --- a/dom/events/EventDispatcher.cpp +++ b/dom/events/EventDispatcher.cpp @@ -47,6 +47,7 @@ #include "mozilla/dom/SimpleGestureEvent.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/StorageEvent.h" +#include "mozilla/dom/TextEvent.h" #include "mozilla/dom/TimeEvent.h" #include "mozilla/dom/TouchEvent.h" #include "mozilla/dom/TransitionEvent.h" @@ -1141,9 +1142,8 @@ nsresult EventDispatcher::Dispatch(EventTarget* aTarget, if (!postVisitor.mDOMEvent) { // This is tiny bit slow, but happens only once per event. // Similar code also in EventListenerManager. - nsCOMPtr<EventTarget> et = aEvent->mOriginalTarget; - RefPtr<Event> event = - EventDispatcher::CreateEvent(et, aPresContext, aEvent, u""_ns); + RefPtr<Event> event = EventDispatcher::CreateEvent( + aEvent->mOriginalTarget, aPresContext, aEvent, u""_ns); event.swap(postVisitor.mDOMEvent); } nsAutoString typeStr; @@ -1151,12 +1151,11 @@ nsresult EventDispatcher::Dispatch(EventTarget* aTarget, AUTO_PROFILER_LABEL_DYNAMIC_LOSSY_NSSTRING( "EventDispatcher::Dispatch", OTHER, typeStr); - nsCOMPtr<nsIDocShell> docShell; - docShell = nsContentUtils::GetDocShellForEventTarget(aEvent->mTarget); MarkerInnerWindowId innerWindowId; - if (nsCOMPtr<nsPIDOMWindowInner> inner = - do_QueryInterface(aEvent->mTarget->GetOwnerGlobal())) { - innerWindowId = MarkerInnerWindowId{inner->WindowID()}; + if (nsIGlobalObject* global = aEvent->mTarget->GetOwnerGlobal()) { + if (nsPIDOMWindowInner* inner = global->GetAsInnerWindow()) { + innerWindowId = MarkerInnerWindowId{inner->WindowID()}; + } } struct DOMEventMarker { @@ -1206,7 +1205,7 @@ nsresult EventDispatcher::Dispatch(EventTarget* aTarget, auto startTime = TimeStamp::Now(); profiler_add_marker("DOMEvent", geckoprofiler::category::DOM, - {MarkerTiming::IntervalStart(), + {MarkerTiming::IntervalStart(startTime), MarkerInnerWindowId(innerWindowId)}, DOMEventMarker{}, typeStr, target, startTime, aEvent->mTimeStamp); @@ -1406,6 +1405,9 @@ nsresult EventDispatcher::DispatchDOMEvent(EventTarget* aTarget, case eEditorInputEventClass: return NS_NewDOMInputEvent(aOwner, aPresContext, aEvent->AsEditorInputEvent()); + case eLegacyTextEventClass: + return NS_NewDOMTextEvent(aOwner, aPresContext, + aEvent->AsLegacyTextEvent()); case eDragEventClass: return NS_NewDOMDragEvent(aOwner, aPresContext, aEvent->AsDragEvent()); case eClipboardEventClass: @@ -1450,10 +1452,15 @@ nsresult EventDispatcher::DispatchDOMEvent(EventTarget* aTarget, if (aEventType.LowerCaseEqualsLiteral("keyboardevent")) { return NS_NewDOMKeyboardEvent(aOwner, aPresContext, nullptr); } - if (aEventType.LowerCaseEqualsLiteral("compositionevent") || - aEventType.LowerCaseEqualsLiteral("textevent")) { + if (aEventType.LowerCaseEqualsLiteral("compositionevent")) { return NS_NewDOMCompositionEvent(aOwner, aPresContext, nullptr); } + if (aEventType.LowerCaseEqualsLiteral("textevent")) { + if (!StaticPrefs::dom_events_textevent_enabled()) { + return NS_NewDOMCompositionEvent(aOwner, aPresContext, nullptr); + } + return NS_NewDOMTextEvent(aOwner, aPresContext, nullptr); + } if (aEventType.LowerCaseEqualsLiteral("mutationevent") || aEventType.LowerCaseEqualsLiteral("mutationevents")) { return NS_NewDOMMutationEvent(aOwner, aPresContext, nullptr); diff --git a/dom/events/EventListenerManager.cpp b/dom/events/EventListenerManager.cpp index 269ccd9de7..72d4eb1b30 100644 --- a/dom/events/EventListenerManager.cpp +++ b/dom/events/EventListenerManager.cpp @@ -455,27 +455,6 @@ void EventListenerManager::AddEventListenerInternal( window->SetHasFormSelectEventListeners(); } break; - case eMarqueeStart: - if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { - if (Document* doc = window->GetExtantDoc()) { - doc->SetUseCounter(eUseCounter_custom_onstart); - } - } - break; - case eMarqueeBounce: - if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { - if (Document* doc = window->GetExtantDoc()) { - doc->SetUseCounter(eUseCounter_custom_onbounce); - } - } - break; - case eMarqueeFinish: - if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { - if (Document* doc = window->GetExtantDoc()) { - doc->SetUseCounter(eUseCounter_custom_onfinish); - } - } - break; case eScrollPortOverflow: if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { if (Document* doc = window->GetExtantDoc()) { @@ -608,18 +587,6 @@ void EventListenerManager::AddEventListenerInternal( nsPrintfCString("resolvedEventMessage=%s", ToChar(resolvedEventMessage)) .get()); - NS_ASSERTION(aTypeAtom != nsGkAtoms::onstart, - nsPrintfCString("resolvedEventMessage=%s", - ToChar(resolvedEventMessage)) - .get()); - NS_ASSERTION(aTypeAtom != nsGkAtoms::onbounce, - nsPrintfCString("resolvedEventMessage=%s", - ToChar(resolvedEventMessage)) - .get()); - NS_ASSERTION(aTypeAtom != nsGkAtoms::onfinish, - nsPrintfCString("resolvedEventMessage=%s", - ToChar(resolvedEventMessage)) - .get()); NS_ASSERTION(aTypeAtom != nsGkAtoms::onoverflow, nsPrintfCString("resolvedEventMessage=%s", ToChar(resolvedEventMessage)) @@ -1417,10 +1384,10 @@ already_AddRefed<nsPIDOMWindowInner> EventListenerManager::WindowFromListener( if (global) { innerWindow = global->GetAsInnerWindow(); // Can be nullptr } - } else { + } else if (mTarget) { // This ensures `window.event` can be set properly for // nsWindowRoot to handle KeyPress event. - if (aListener && aTypeAtom == nsGkAtoms::onkeypress && mTarget && + if (aListener && aTypeAtom == nsGkAtoms::onkeypress && mTarget->IsRootWindow()) { nsPIWindowRoot* root = mTarget->AsWindowRoot(); if (nsPIDOMWindowOuter* outerWindow = root->GetWindow()) { @@ -1431,7 +1398,9 @@ already_AddRefed<nsPIDOMWindowInner> EventListenerManager::WindowFromListener( // listener->mListener.GetXPCOMCallback(). // In most cases, it would be the same as for // the target, so let's do that. - innerWindow = GetInnerWindowForTarget(); // Can be nullptr + if (nsIGlobalObject* global = mTarget->GetOwnerGlobal()) { + innerWindow = global->GetAsInnerWindow(); + } } } } diff --git a/dom/events/EventNameList.h b/dom/events/EventNameList.h index 92c76d000e..185f75f2bc 100644 --- a/dom/events/EventNameList.h +++ b/dom/events/EventNameList.h @@ -150,7 +150,6 @@ EVENT(abort, eImageAbort, EventNameType_All, eBasicEventClass) EVENT(beforetoggle, eBeforeToggle, EventNameType_HTMLXUL, eBasicEventClass) -EVENT(bounce, eMarqueeBounce, EventNameType_HTMLMarqueeOnly, eBasicEventClass) EVENT(cancel, eCancel, EventNameType_HTMLXUL, eBasicEventClass) EVENT(canplay, eCanPlay, EventNameType_HTML, eBasicEventClass) EVENT(canplaythrough, eCanPlayThrough, EventNameType_HTML, eBasicEventClass) @@ -179,7 +178,6 @@ EVENT(drop, eDrop, EventNameType_HTMLXUL, eDragEventClass) EVENT(durationchange, eDurationChange, EventNameType_HTML, eBasicEventClass) EVENT(emptied, eEmptied, EventNameType_HTML, eBasicEventClass) EVENT(ended, eEnded, EventNameType_HTML, eBasicEventClass) -EVENT(finish, eMarqueeFinish, EventNameType_HTMLMarqueeOnly, eBasicEventClass) EVENT(formdata, eFormData, EventNameType_HTML, eBasicEventClass) EVENT(fullscreenchange, eFullscreenChange, EventNameType_HTML, eBasicEventClass) EVENT(fullscreenerror, eFullscreenError, EventNameType_HTML, eBasicEventClass) @@ -227,6 +225,8 @@ EVENT(gotpointercapture, ePointerGotCapture, EventNameType_All, EVENT(lostpointercapture, ePointerLostCapture, EventNameType_All, ePointerEventClass) EVENT(selectstart, eSelectStart, EventNameType_HTMLXUL, eBasicEventClass) +NON_IDL_EVENT(textInput, eLegacyTextInput, EventNameType_None, + eLegacyTextEventClass) EVENT(contextlost, eContextLost, EventNameType_HTML, eBasicEventClass) EVENT(contextrestored, eContextRestored, EventNameType_HTML, eBasicEventClass) @@ -246,7 +246,6 @@ EVENT(seeking, eSeeking, EventNameType_HTML, eBasicEventClass) EVENT(select, eFormSelect, EventNameType_HTMLXUL, eBasicEventClass) EVENT(slotchange, eSlotChange, EventNameType_All, eBasicEventClass) EVENT(stalled, eStalled, EventNameType_HTML, eBasicEventClass) -EVENT(start, eMarqueeStart, EventNameType_HTMLMarqueeOnly, eBasicEventClass) EVENT(submit, eFormSubmit, EventNameType_HTMLXUL, eBasicEventClass) EVENT(suspend, eSuspend, EventNameType_HTML, eBasicEventClass) EVENT(timeupdate, eTimeUpdate, EventNameType_HTML, eBasicEventClass) diff --git a/dom/events/EventStateManager.cpp b/dom/events/EventStateManager.cpp index 0ae756211b..3c24cdb30a 100644 --- a/dom/events/EventStateManager.cpp +++ b/dom/events/EventStateManager.cpp @@ -227,6 +227,9 @@ static nsINode* GetCommonAncestorForMouseUp( return parent; } +LazyLogModule sMouseBoundaryLog("MouseBoundaryEvents"); +LazyLogModule sPointerBoundaryLog("PointerBoundaryEvents"); + /******************************************************************/ /* mozilla::UITimerCallback */ /******************************************************************/ @@ -264,8 +267,8 @@ UITimerCallback::Notify(nsITimer* aTimer) { if (XRE_IsParentProcess()) { hal::BatteryInformation batteryInfo; hal::GetCurrentBatteryInformation(&batteryInfo); - glean::power_battery::percentage_when_user_active.AccumulateSamples( - {uint64_t(batteryInfo.level() * 100)}); + glean::power_battery::percentage_when_user_active.AccumulateSingleSample( + uint64_t(batteryInfo.level() * 100)); } } mPreviousCount = gMouseOrKeyboardEventCounter; @@ -282,10 +285,6 @@ UITimerCallback::GetName(nsACString& aName) { /* mozilla::OverOutElementsWrapper */ /******************************************************************/ -OverOutElementsWrapper::OverOutElementsWrapper() : mLastOverFrame(nullptr) {} - -OverOutElementsWrapper::~OverOutElementsWrapper() = default; - NS_IMPL_CYCLE_COLLECTION(OverOutElementsWrapper, mDeepestEnterEventTarget, mDispatchingOverEventTarget, mDispatchingOutOrDeepestLeaveEventTarget) @@ -296,6 +295,116 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(OverOutElementsWrapper) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END +void OverOutElementsWrapper::ContentRemoved(nsIContent& aContent) { + if (!mDeepestEnterEventTarget) { + return; + } + + if (!nsContentUtils::ContentIsFlattenedTreeDescendantOf( + mDeepestEnterEventTarget, &aContent)) { + return; + } + + LogModule* const logModule = mType == BoundaryEventType::Mouse + ? sMouseBoundaryLog + : sPointerBoundaryLog; + + if (!StaticPrefs:: + dom_events_mouse_pointer_boundary_keep_enter_targets_after_over_target_removed()) { + MOZ_LOG(logModule, LogLevel::Info, + ("The last \"over\" event target (%p) is removed", + mDeepestEnterEventTarget.get())); + mDeepestEnterEventTarget = nullptr; + return; + } + + if (mDispatchingOverEventTarget && + (mDeepestEnterEventTarget == mDispatchingOverEventTarget || + nsContentUtils::ContentIsFlattenedTreeDescendantOf( + mDispatchingOverEventTarget, &aContent))) { + if (mDispatchingOverEventTarget == + mDispatchingOutOrDeepestLeaveEventTarget) { + MOZ_LOG(logModule, LogLevel::Info, + ("The dispatching \"%s\" event target (%p) is removed", + mDeepestEnterEventTargetIsOverEventTarget ? "out" : "leave", + mDispatchingOutOrDeepestLeaveEventTarget.get())); + mDispatchingOutOrDeepestLeaveEventTarget = nullptr; + } + MOZ_LOG(logModule, LogLevel::Info, + ("The dispatching \"over\" event target (%p) is removed", + mDispatchingOverEventTarget.get())); + mDispatchingOverEventTarget = nullptr; + } + if (mDispatchingOutOrDeepestLeaveEventTarget && + (mDeepestEnterEventTarget == mDispatchingOutOrDeepestLeaveEventTarget || + nsContentUtils::ContentIsFlattenedTreeDescendantOf( + mDispatchingOutOrDeepestLeaveEventTarget, &aContent))) { + MOZ_LOG(logModule, LogLevel::Info, + ("The dispatching \"%s\" event target (%p) is removed", + mDeepestEnterEventTargetIsOverEventTarget ? "out" : "leave", + mDispatchingOutOrDeepestLeaveEventTarget.get())); + mDispatchingOutOrDeepestLeaveEventTarget = nullptr; + } + MOZ_LOG(logModule, LogLevel::Info, + ("The last \"%s\" event target (%p) is removed and now the last " + "deepest enter target becomes %s(%p)", + mDeepestEnterEventTargetIsOverEventTarget ? "over" : "enter", + mDeepestEnterEventTarget.get(), + aContent.GetFlattenedTreeParent() + ? ToString(*aContent.GetFlattenedTreeParent()).c_str() + : "nullptr", + aContent.GetFlattenedTreeParent())); + mDeepestEnterEventTarget = aContent.GetFlattenedTreeParent(); + mDeepestEnterEventTargetIsOverEventTarget = false; +} + +void OverOutElementsWrapper::DidDispatchOverAndEnterEvent( + nsIContent* aOriginalOverTargetInComposedDoc) { + mDispatchingOverEventTarget = nullptr; + + // Pointer Events define that once the `pointerover` event target is removed + // from the tree, `pointerout` should not be fired on that and the closest + // connected ancestor at the target removal should be kept as the deepest + // `pointerleave` target. Therefore, we don't need the special handling for + // `pointerout` event target if the last `pointerover` target is temporarily + // removed from the tree. + if (mType == OverOutElementsWrapper::BoundaryEventType::Pointer) { + return; + } + + // Assume that the caller checks whether aOriginalOverTarget is in the + // original document. If we don't enable the strict mouse/pointer event + // boundary event dispatching by the pref (see below), + // mDeepestEnterEventTarget is set to nullptr when the last "over" target is + // removed. Therefore, we cannot check whether aOriginalOverTarget is in the + // original document here. + if (!aOriginalOverTargetInComposedDoc) { + return; + } + MOZ_ASSERT_IF(mDeepestEnterEventTarget, + mDeepestEnterEventTarget->GetComposedDoc() == + aOriginalOverTargetInComposedDoc->GetComposedDoc()); + // If the "mouseover" event target is removed temporarily while we're + // dispatching "mouseover" and "mouseenter" events and the target gets back + // under the deepest enter event target, we should restore the "mouseover" + // target. + if ((!StaticPrefs:: + dom_events_mouse_pointer_boundary_keep_enter_targets_after_over_target_removed() && + !mDeepestEnterEventTarget) || + (!mDeepestEnterEventTargetIsOverEventTarget && mDeepestEnterEventTarget && + nsContentUtils::ContentIsFlattenedTreeDescendantOf( + aOriginalOverTargetInComposedDoc, mDeepestEnterEventTarget))) { + mDeepestEnterEventTarget = aOriginalOverTargetInComposedDoc; + mDeepestEnterEventTargetIsOverEventTarget = true; + LogModule* const logModule = mType == BoundaryEventType::Mouse + ? sMouseBoundaryLog + : sPointerBoundaryLog; + MOZ_LOG(logModule, LogLevel::Info, + ("The \"over\" event target (%p) is restored", + mDeepestEnterEventTarget.get())); + } +} + /******************************************************************/ /* mozilla::EventStateManager */ /******************************************************************/ @@ -2169,8 +2278,8 @@ void EventStateManager::MaybeFirePointerCancel(WidgetInputEvent* aEvent) { WidgetPointerEvent event(aTouchEvent->IsTrusted(), ePointerCancel, aTouchEvent->mWidget); - PointerEventHandler::InitPointerEventFromTouch( - event, *aTouchEvent, *aTouchEvent->mTouches[0], true); + PointerEventHandler::InitPointerEventFromTouch(event, *aTouchEvent, + *aTouchEvent->mTouches[0]); event.convertToPointer = false; presShell->HandleEventWithTarget(&event, targetFrame, content, &status); @@ -3453,6 +3562,13 @@ void EventStateManager::PostHandleKeyboardEvent( } } +static bool NeedsActiveContentChange(const WidgetMouseEvent* aMouseEvent) { + // If the mouse event is a synthesized mouse event due to a touch, do + // not set/clear the activation state. Element activation is handled by APZ. + return !aMouseEvent || + aMouseEvent->mInputSource != MouseEvent_Binding::MOZ_SOURCE_TOUCH; +} + nsresult EventStateManager::PostHandleEvent(nsPresContext* aPresContext, WidgetEvent* aEvent, nsIFrame* aTargetFrame, @@ -3696,7 +3812,9 @@ nsresult EventStateManager::PostHandleEvent(nsPresContext* aPresContext, } // XXX Why do we always set this is active? Active window may be changed // by a mousedown event listener. - SetActiveManager(this, activeContent); + if (NeedsActiveContentChange(mouseEvent)) { + SetActiveManager(this, activeContent); + } } break; case ePointerCancel: case ePointerUp: { @@ -3724,10 +3842,7 @@ nsresult EventStateManager::PostHandleEvent(nsPresContext* aPresContext, PresShell::ReleaseCapturingContent(); WidgetMouseEvent* mouseUpEvent = aEvent->AsMouseEvent(); - // If the mouseup event is a synthesized mouse event due to a touch, do - // not clear the activation state. Element activation is handled by APZ. - if (!mouseUpEvent || mouseUpEvent->mInputSource != - dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH) { + if (NeedsActiveContentChange(mouseUpEvent)) { ClearGlobalActiveContent(this); } if (mouseUpEvent && EventCausesClickEvents(*mouseUpEvent)) { @@ -4803,6 +4918,10 @@ class EnterLeaveDispatcher { void EventStateManager::NotifyMouseOut(WidgetMouseEvent* aMouseEvent, nsIContent* aMovingInto) { + const bool isPointer = aMouseEvent->mClass == ePointerEventClass; + LogModule* const logModule = + isPointer ? sPointerBoundaryLog : sMouseBoundaryLog; + RefPtr<OverOutElementsWrapper> wrapper = GetWrapperByEventID(aMouseEvent); // If there is no deepest "leave" event target, that means the last "over" @@ -4816,6 +4935,11 @@ void EventStateManager::NotifyMouseOut(WidgetMouseEvent* aMouseEvent, return; } + MOZ_LOG(logModule, LogLevel::Info, + ("NotifyMouseOut: the source event is %s (IsReal()=%s)", + ToChar(aMouseEvent->mMessage), + aMouseEvent->IsReal() ? "true" : "false")); + // XXX If a content node is a container of remove content, it should be // replaced with them and its children should not be visible. Therefore, // if the deepest "enter" target is not the last "over" target, i.e., the @@ -4829,6 +4953,10 @@ void EventStateManager::NotifyMouseOut(WidgetMouseEvent* aMouseEvent, if (RefPtr<nsPresContext> presContext = docshell->GetPresContext()) { EventStateManager* kidESM = presContext->EventStateManager(); // Not moving into any element in this subdocument + MOZ_LOG(logModule, LogLevel::Info, + ("Notifying child EventStateManager (%p) of \"out\" " + "event...", + kidESM)); kidESM->NotifyMouseOut(aMouseEvent, nullptr); } } @@ -4846,7 +4974,6 @@ void EventStateManager::NotifyMouseOut(WidgetMouseEvent* aMouseEvent, // hover state itself, and we have optimizations for hover switching between // two nearby elements both deep in the DOM tree that would be defeated by // switching the hover state to null here. - bool isPointer = aMouseEvent->mClass == ePointerEventClass; if (!aMovingInto && !isPointer) { // Unset :hover SetContentState(nullptr, ElementState::HOVER); @@ -4859,12 +4986,27 @@ void EventStateManager::NotifyMouseOut(WidgetMouseEvent* aMouseEvent, // "out" events hould be fired only when the deepest "leave" event target // is the last "over" event target. if (nsCOMPtr<nsIContent> outEventTarget = wrapper->GetOutEventTarget()) { + MOZ_LOG(logModule, LogLevel::Info, + ("Dispatching %s event to %s (%p)", + isPointer ? "ePointerOut" : "eMouseOut", + outEventTarget ? ToString(*outEventTarget).c_str() : "nullptr", + outEventTarget.get())); DispatchMouseOrPointerEvent(aMouseEvent, isPointer ? ePointerOut : eMouseOut, outEventTarget, aMovingInto); } + + MOZ_LOG(logModule, LogLevel::Info, + ("Dispatching %s event to %s (%p) and its ancestors", + isPointer ? "ePointerLeave" : "eMouseLeave", + wrapper->GetDeepestLeaveEventTarget() + ? ToString(*wrapper->GetDeepestLeaveEventTarget()).c_str() + : "nullptr", + wrapper->GetDeepestLeaveEventTarget())); leaveDispatcher.Dispatch(); + MOZ_LOG(logModule, LogLevel::Info, + ("Dispatched \"out\" and/or \"leave\" events")); wrapper->DidDispatchOutAndOrLeaveEvent(); } @@ -4884,6 +5026,10 @@ void EventStateManager::NotifyMouseOver(WidgetMouseEvent* aMouseEvent, nsIContent* aContent) { NS_ASSERTION(aContent, "Mouse must be over something"); + const bool isPointer = aMouseEvent->mClass == ePointerEventClass; + LogModule* const logModule = + isPointer ? sPointerBoundaryLog : sMouseBoundaryLog; + RefPtr<OverOutElementsWrapper> wrapper = GetWrapperByEventID(aMouseEvent); // If we have next "out" event target and it's the new "over" target, we don't @@ -4897,6 +5043,11 @@ void EventStateManager::NotifyMouseOver(WidgetMouseEvent* aMouseEvent, return; } + MOZ_LOG(logModule, LogLevel::Info, + ("NotifyMouseOver: the source event is %s (IsReal()=%s)", + ToChar(aMouseEvent->mMessage), + aMouseEvent->IsReal() ? "true" : "false")); + // Check to see if we're a subdocument and if so update the parent // document's ESM state to indicate that the mouse is over the // content associated with our subdocument. @@ -4906,6 +5057,10 @@ void EventStateManager::NotifyMouseOver(WidgetMouseEvent* aMouseEvent, if (PresShell* parentPresShell = parentDoc->GetPresShell()) { RefPtr<EventStateManager> parentESM = parentPresShell->GetPresContext()->EventStateManager(); + MOZ_LOG(logModule, LogLevel::Info, + ("Notifying parent EventStateManager (%p) of \"over\" " + "event...", + parentESM.get())); parentESM->NotifyMouseOver(aMouseEvent, docContent); } } @@ -4922,8 +5077,6 @@ void EventStateManager::NotifyMouseOver(WidgetMouseEvent* aMouseEvent, nsCOMPtr<nsIContent> deepestLeaveEventTarget = wrapper->GetDeepestLeaveEventTarget(); - bool isPointer = aMouseEvent->mClass == ePointerEventClass; - EnterLeaveDispatcher enterDispatcher(this, aContent, deepestLeaveEventTarget, aMouseEvent, isPointer ? ePointerEnter : eMouseEnter); @@ -4939,12 +5092,26 @@ void EventStateManager::NotifyMouseOver(WidgetMouseEvent* aMouseEvent, // Fire mouseover // XXX If aContent has already been removed from the DOM tree, what should we // do? At least, dispatching `mouseover` on it is odd. + MOZ_LOG(logModule, LogLevel::Info, + ("Dispatching %s event to %s (%p)", + isPointer ? "ePointerOver" : "eMoustOver", + aContent ? ToString(*aContent).c_str() : "nullptr", aContent)); wrapper->mLastOverFrame = DispatchMouseOrPointerEvent( aMouseEvent, isPointer ? ePointerOver : eMouseOver, aContent, deepestLeaveEventTarget); + + MOZ_LOG(logModule, LogLevel::Info, + ("Dispatching %s event to %s (%p) and its ancestors", + isPointer ? "ePointerEnter" : "eMouseEnter", + aContent ? ToString(*aContent).c_str() : "nullptr", aContent)); enterDispatcher.Dispatch(); - wrapper->DidDispatchOverAndEnterEvent(); + MOZ_LOG(logModule, LogLevel::Info, + ("Dispatched \"over\" and \"enter\" events (the original \"over\" " + "event target was in the document %p, and now in %p)", + aContent->GetComposedDoc(), mDocument.get())); + wrapper->DidDispatchOverAndEnterEvent( + aContent->GetComposedDoc() == mDocument ? aContent : nullptr); } // Returns the center point of the window's client area. This is @@ -5135,11 +5302,13 @@ OverOutElementsWrapper* EventStateManager::GetWrapperByEventID( if (!pointer) { MOZ_ASSERT(aEvent->AsMouseEvent() != nullptr); if (!mMouseEnterLeaveHelper) { - mMouseEnterLeaveHelper = new OverOutElementsWrapper(); + mMouseEnterLeaveHelper = new OverOutElementsWrapper( + OverOutElementsWrapper::BoundaryEventType::Mouse); } return mMouseEnterLeaveHelper; } - return mPointersEnterLeaveHelper.GetOrInsertNew(pointer->pointerId); + return mPointersEnterLeaveHelper.GetOrInsertNew( + pointer->pointerId, OverOutElementsWrapper::BoundaryEventType::Pointer); } /* static */ @@ -6895,40 +7064,4 @@ bool EventStateManager::WheelPrefs::IsOverOnePageScrollAllowedY( MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL; } -void OverOutElementsWrapper::ContentRemoved(nsIContent& aContent) { - if (!mDeepestEnterEventTarget) { - return; - } - - if (!nsContentUtils::ContentIsFlattenedTreeDescendantOf( - mDeepestEnterEventTarget, &aContent)) { - return; - } - - if (!StaticPrefs:: - dom_events_mouse_pointer_boundary_keep_enter_targets_after_over_target_removed()) { - mDeepestEnterEventTarget = nullptr; - return; - } - - if (mDispatchingOverEventTarget && - (mDeepestEnterEventTarget == mDispatchingOverEventTarget || - nsContentUtils::ContentIsFlattenedTreeDescendantOf( - mDispatchingOverEventTarget, &aContent))) { - if (mDispatchingOverEventTarget == - mDispatchingOutOrDeepestLeaveEventTarget) { - mDispatchingOutOrDeepestLeaveEventTarget = nullptr; - } - mDispatchingOverEventTarget = nullptr; - } - if (mDispatchingOutOrDeepestLeaveEventTarget && - (mDeepestEnterEventTarget == mDispatchingOutOrDeepestLeaveEventTarget || - nsContentUtils::ContentIsFlattenedTreeDescendantOf( - mDispatchingOutOrDeepestLeaveEventTarget, &aContent))) { - mDispatchingOutOrDeepestLeaveEventTarget = nullptr; - } - mDeepestEnterEventTarget = aContent.GetFlattenedTreeParent(); - mDeepestEnterEventTargetIsOverEventTarget = false; -} - } // namespace mozilla diff --git a/dom/events/EventStateManager.h b/dom/events/EventStateManager.h index b3bb3b5170..a4e709d507 100644 --- a/dom/events/EventStateManager.h +++ b/dom/events/EventStateManager.h @@ -56,10 +56,11 @@ class RemoteDragStartData; } // namespace dom class OverOutElementsWrapper final : public nsISupports { - ~OverOutElementsWrapper(); + ~OverOutElementsWrapper() = default; public: - OverOutElementsWrapper(); + enum class BoundaryEventType : bool { Mouse, Pointer }; + explicit OverOutElementsWrapper(BoundaryEventType aType) : mType(aType) {} NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS(OverOutElementsWrapper) @@ -72,7 +73,8 @@ class OverOutElementsWrapper final : public nsISupports { mDispatchingOverEventTarget = aOverEventTarget; mDeepestEnterEventTargetIsOverEventTarget = true; } - void DidDispatchOverAndEnterEvent() { mDispatchingOverEventTarget = nullptr; } + void DidDispatchOverAndEnterEvent( + nsIContent* aOriginalOverTargetInComposedDoc); [[nodiscard]] bool IsDispatchingOverEventOn( nsIContent* aOverEventTarget) const { MOZ_ASSERT(aOverEventTarget); @@ -135,6 +137,8 @@ class OverOutElementsWrapper final : public nsISupports { // the DOM tree, this is set to nullptr. nsCOMPtr<nsIContent> mDispatchingOutOrDeepestLeaveEventTarget; + const BoundaryEventType mType; + // Once the last "over" element is removed from the tree, this is set // to false. Then, mDeepestEnterEventTarget may be an ancestor of the // "over" element which should be the deepest target of next "leave" diff --git a/dom/events/InvokeEvent.cpp b/dom/events/InvokeEvent.cpp index 4b7dc1e6da..3312f8138d 100644 --- a/dom/events/InvokeEvent.cpp +++ b/dom/events/InvokeEvent.cpp @@ -47,9 +47,13 @@ already_AddRefed<InvokeEvent> InvokeEvent::Constructor( Element* InvokeEvent::GetInvoker() { EventTarget* currentTarget = GetCurrentTarget(); if (currentTarget) { + nsINode* currentTargetNode = currentTarget->GetAsNode(); + if (!currentTargetNode) { + return nullptr; + } nsINode* retargeted = nsContentUtils::Retarget( - static_cast<nsINode*>(mInvoker), currentTarget->GetAsNode()); - return retargeted->AsElement(); + static_cast<nsINode*>(mInvoker), currentTargetNode); + return retargeted ? retargeted->AsElement() : nullptr; } MOZ_ASSERT(!mEvent->mFlags.mIsBeingDispatched); return mInvoker; diff --git a/dom/events/PointerEventHandler.cpp b/dom/events/PointerEventHandler.cpp index ef6f2b12c0..f631b6e494 100644 --- a/dom/events/PointerEventHandler.cpp +++ b/dom/events/PointerEventHandler.cpp @@ -15,6 +15,7 @@ #include "mozilla/dom/BrowserChild.h" #include "mozilla/dom/BrowserParent.h" #include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" #include "mozilla/dom/MouseEventBinding.h" namespace mozilla { @@ -80,7 +81,8 @@ void PointerEventHandler::UpdateActivePointerState(WidgetMouseEvent* aEvent, // In this case we have to know information about available mouse pointers sActivePointersIds->InsertOrUpdate( aEvent->pointerId, - MakeUnique<PointerInfo>(false, aEvent->mInputSource, true, nullptr)); + MakeUnique<PointerInfo>(false, aEvent->mInputSource, true, false, + nullptr)); MaybeCacheSpoofedPointerID(aEvent->mInputSource, aEvent->pointerId); break; @@ -94,6 +96,7 @@ void PointerEventHandler::UpdateActivePointerState(WidgetMouseEvent* aEvent, pointerEvent->pointerId, MakeUnique<PointerInfo>( true, pointerEvent->mInputSource, pointerEvent->mIsPrimary, + pointerEvent->mFromTouchEvent, aTargetContent ? aTargetContent->OwnerDoc() : nullptr)); MaybeCacheSpoofedPointerID(pointerEvent->mInputSource, pointerEvent->pointerId); @@ -112,7 +115,8 @@ void PointerEventHandler::UpdateActivePointerState(WidgetMouseEvent* aEvent, sActivePointersIds->InsertOrUpdate( pointerEvent->pointerId, MakeUnique<PointerInfo>(false, pointerEvent->mInputSource, - pointerEvent->mIsPrimary, nullptr)); + pointerEvent->mIsPrimary, + pointerEvent->mFromTouchEvent, nullptr)); } else { sActivePointersIds->Remove(pointerEvent->pointerId); } @@ -314,7 +318,7 @@ void PointerEventHandler::ProcessPointerCaptureForTouch( continue; } WidgetPointerEvent event(aEvent->IsTrusted(), eVoidEvent, aEvent->mWidget); - InitPointerEventFromTouch(event, *aEvent, *touch, i == 0); + InitPointerEventFromTouch(event, *aEvent, *touch); CheckPointerCaptureState(&event); } } @@ -461,11 +465,13 @@ Element* PointerEventHandler::GetPointerCapturingElement( void PointerEventHandler::ReleaseIfCaptureByDescendant(nsIContent* aContent) { // We should check that aChild does not contain pointer capturing elements. // If it does we should release the pointer capture for the elements. - for (const auto& entry : *sPointerCaptureList) { - PointerCaptureInfo* data = entry.GetWeak(); - if (data && data->mPendingElement && - data->mPendingElement->IsInclusiveDescendantOf(aContent)) { - ReleasePointerCaptureById(entry.GetKey()); + if (!sPointerCaptureList->IsEmpty()) { + for (const auto& entry : *sPointerCaptureList) { + PointerCaptureInfo* data = entry.GetWeak(); + if (data && data->mPendingElement && + data->mPendingElement->IsInclusiveDescendantOf(aContent)) { + ReleasePointerCaptureById(entry.GetKey()); + } } } } @@ -546,7 +552,7 @@ void PointerEventHandler::InitPointerEventFromMouse( /* static */ void PointerEventHandler::InitPointerEventFromTouch( WidgetPointerEvent& aPointerEvent, const WidgetTouchEvent& aTouchEvent, - const mozilla::dom::Touch& aTouch, bool aIsPrimary) { + const mozilla::dom::Touch& aTouch) { // Use mButton/mButtons only when mButton got a value (from pen input) int16_t button = aTouchEvent.mMessage == eTouchMove ? MouseButton::eNotPressed : aTouchEvent.mButton != MouseButton::eNotPressed @@ -558,7 +564,10 @@ void PointerEventHandler::InitPointerEventFromTouch( ? aTouchEvent.mButtons : MouseButtonsFlag::ePrimaryFlag; - aPointerEvent.mIsPrimary = aIsPrimary; + // Only the first touch would be the primary pointer. + aPointerEvent.mIsPrimary = aTouchEvent.mMessage == eTouchStart + ? !HasActiveTouchPointer() + : GetPointerPrimaryState(aTouch.Identifier()); aPointerEvent.pointerId = aTouch.Identifier(); aPointerEvent.mRefPoint = aTouch.mRefPoint; aPointerEvent.mModifiers = aTouchEvent.mModifiers; @@ -679,7 +688,7 @@ void PointerEventHandler::DispatchPointerFromMouseOrTouch( WidgetPointerEvent event(touchEvent->IsTrusted(), pointerMessage, touchEvent->mWidget); - InitPointerEventFromTouch(event, *touchEvent, *touch, i == 0); + InitPointerEventFromTouch(event, *touchEvent, *touch); event.convertToPointer = touch->convertToPointer = false; event.mCoalescedWidgetEvents = touch->mCoalescedWidgetEvents; if (aMouseOrTouchEvent->mMessage == eTouchStart) { @@ -739,6 +748,15 @@ void PointerEventHandler::NotifyDestroyPresContext( iter.Remove(); } } + // Clean up active pointer info + for (auto iter = sActivePointersIds->Iter(); !iter.Done(); iter.Next()) { + PointerInfo* data = iter.UserData(); + MOZ_ASSERT(data, "how could we have a null PointerInfo here?"); + if (data->mActiveDocument && + data->mActiveDocument->GetPresContext() == aPresContext) { + iter.Remove(); + } + } } bool PointerEventHandler::IsDragAndDropEnabled(WidgetMouseEvent& aEvent) { @@ -771,6 +789,16 @@ bool PointerEventHandler::GetPointerPrimaryState(uint32_t aPointerId) { } /* static */ +bool PointerEventHandler::HasActiveTouchPointer() { + for (auto iter = sActivePointersIds->ConstIter(); !iter.Done(); iter.Next()) { + if (iter.Data()->mFromTouchEvent) { + return true; + } + } + return false; +} + +/* static */ void PointerEventHandler::DispatchGotOrLostPointerCaptureEvent( bool aIsGotCapture, const WidgetPointerEvent* aPointerEvent, Element* aCaptureTarget) { diff --git a/dom/events/PointerEventHandler.h b/dom/events/PointerEventHandler.h index 0211bfe0d8..87b41f9d94 100644 --- a/dom/events/PointerEventHandler.h +++ b/dom/events/PointerEventHandler.h @@ -50,13 +50,16 @@ class PointerInfo final { uint16_t mPointerType; bool mActiveState; bool mPrimaryState; + bool mFromTouchEvent; bool mPreventMouseEventByContent; WeakPtr<dom::Document> mActiveDocument; explicit PointerInfo(bool aActiveState, uint16_t aPointerType, - bool aPrimaryState, dom::Document* aActiveDocument) + bool aPrimaryState, bool aFromTouchEvent, + dom::Document* aActiveDocument) : mPointerType(aPointerType), mActiveState(aActiveState), mPrimaryState(aPrimaryState), + mFromTouchEvent(aFromTouchEvent), mPreventMouseEventByContent(false), mActiveDocument(aActiveDocument) {} }; @@ -208,8 +211,7 @@ class PointerEventHandler final { static void InitPointerEventFromTouch(WidgetPointerEvent& aPointerEvent, const WidgetTouchEvent& aTouchEvent, - const mozilla::dom::Touch& aTouch, - bool aIsPrimary); + const mozilla::dom::Touch& aTouch); static bool ShouldGeneratePointerEventFromMouse(WidgetGUIEvent* aEvent) { return aEvent->mMessage == eMouseDown || aEvent->mMessage == eMouseUp || @@ -250,6 +252,10 @@ class PointerEventHandler final { // event with pointerId static bool GetPointerPrimaryState(uint32_t aPointerId); + // HasActiveTouchPointer returns true if there is active pointer event that is + // generated from touch event. + static bool HasActiveTouchPointer(); + MOZ_CAN_RUN_SCRIPT static void DispatchGotOrLostPointerCaptureEvent( bool aIsGotCapture, const WidgetPointerEvent* aPointerEvent, diff --git a/dom/events/TextEvent.cpp b/dom/events/TextEvent.cpp new file mode 100644 index 0000000000..95ed15f8e2 --- /dev/null +++ b/dom/events/TextEvent.cpp @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/BasePrincipal.h" // for nsIPrincipal::IsSystemPrincipal() +#include "mozilla/EventForwards.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/TextEvent.h" +#include "nsGlobalWindowInner.h" +#include "nsIPrincipal.h" +#include "nsPresContext.h" + +namespace mozilla::dom { + +TextEvent::TextEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalLegacyTextEvent* aEvent) + : UIEvent(aOwner, aPresContext, + aEvent + ? aEvent + : new InternalLegacyTextEvent(false, eVoidEvent, nullptr)) { + NS_ASSERTION(mEvent->mClass == eLegacyTextEventClass, "event type mismatch"); + mEventIsInternal = !aEvent; +} + +void TextEvent::InitTextEvent(const nsAString& typeArg, bool canBubbleArg, + bool cancelableArg, nsGlobalWindowInner* viewArg, + const nsAString& dataArg) { + if (NS_WARN_IF(mEvent->mFlags.mIsBeingDispatched)) { + return; + } + + UIEvent::InitUIEvent(typeArg, canBubbleArg, cancelableArg, viewArg, 0); + + static_cast<InternalLegacyTextEvent*>(mEvent)->mData = dataArg; +} + +void TextEvent::GetData(nsAString& aData, + nsIPrincipal& aSubjectPrincipal) const { + InternalLegacyTextEvent* textEvent = mEvent->AsLegacyTextEvent(); + MOZ_ASSERT(textEvent); + if (mEvent->IsTrusted() && !aSubjectPrincipal.IsSystemPrincipal() && + !StaticPrefs::dom_event_clipboardevents_enabled() && + ExposesClipboardDataOrDataTransfer(textEvent->mInputType)) { + aData.Truncate(); + return; + } + if (!textEvent->mDataTransfer) { + aData = textEvent->mData; + return; + } + textEvent->mDataTransfer->GetData(u"text/plain"_ns, aData, aSubjectPrincipal, + IgnoreErrors()); +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<TextEvent> NS_NewDOMTextEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + InternalLegacyTextEvent* aEvent) { + RefPtr<TextEvent> it = new TextEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/TextEvent.h b/dom/events/TextEvent.h new file mode 100644 index 0000000000..0546a24eed --- /dev/null +++ b/dom/events/TextEvent.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_TextEvent_h +#define mozilla_dom_TextEvent_h + +#include "mozilla/dom/UIEvent.h" + +#include "mozilla/dom/TextEventBinding.h" +#include "mozilla/EventForwards.h" + +class nsIPrincipal; + +namespace mozilla::dom { + +class TextEvent : public UIEvent { + public: + TextEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalLegacyTextEvent* aEvent); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(TextEvent, UIEvent) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return TextEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + void GetData(nsAString& aData, nsIPrincipal& aSubjectPrincipal) const; + void InitTextEvent(const nsAString& typeArg, bool canBubbleArg, + bool cancelableArg, nsGlobalWindowInner* viewArg, + const nsAString& dataArg); + + protected: + ~TextEvent() = default; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::TextEvent> NS_NewDOMTextEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::InternalLegacyTextEvent* aEvent); + +#endif // mozilla_dom_InputEvent_h_ diff --git a/dom/events/moz.build b/dom/events/moz.build index 62e1f7d57b..405bdff895 100644 --- a/dom/events/moz.build +++ b/dom/events/moz.build @@ -94,6 +94,7 @@ EXPORTS.mozilla.dom += [ "SimpleGestureEvent.h", "StorageEvent.h", "TextClause.h", + "TextEvent.h", "Touch.h", "TouchEvent.h", "TransitionEvent.h", @@ -154,6 +155,7 @@ UNIFIED_SOURCES += [ "StorageEvent.cpp", "TextClause.cpp", "TextComposition.cpp", + "TextEvent.cpp", "Touch.cpp", "TouchEvent.cpp", "TransitionEvent.cpp", diff --git a/dom/events/test/clipboard/mochitest.toml b/dom/events/test/clipboard/mochitest.toml index 7829915267..98cbd3ddb5 100644 --- a/dom/events/test/clipboard/mochitest.toml +++ b/dom/events/test/clipboard/mochitest.toml @@ -1,5 +1,12 @@ [DEFAULT] +["test_async_clipboard_datatransfer.html"] +scheme = "https" +skip-if = [ + "headless", # headless doesn't support custom type + "os == 'android'", # android doesn't support custom type +] + ["test_paste_image.html"] skip-if = [ "headless", # Bug 1405869 diff --git a/dom/events/test/clipboard/test_async_clipboard.xhtml b/dom/events/test/clipboard/test_async_clipboard.xhtml index ec54809077..8a882fd24d 100644 --- a/dom/events/test/clipboard/test_async_clipboard.xhtml +++ b/dom/events/test/clipboard/test_async_clipboard.xhtml @@ -74,15 +74,7 @@ }); const items = await navigator.clipboard.read(); - - // Bug 1756955: at least on Ubuntu 20.04, clearing the clipboard leads to - // one item with no types. - if (!items.length || - (items.length == 1 && !items[0].types.length)) { - ok(true, "read() read the right thing from empty clipboard"); - } else { - ok(false, "read() read the wrong thing from empty clipboard"); - } + ok(!items.length, "read() read the right thing from empty clipboard"); } async function testNoContentsReadText() { diff --git a/dom/events/test/clipboard/test_async_clipboard_datatransfer.html b/dom/events/test/clipboard/test_async_clipboard_datatransfer.html new file mode 100644 index 0000000000..ab4151f4f5 --- /dev/null +++ b/dom/events/test/clipboard/test_async_clipboard_datatransfer.html @@ -0,0 +1,89 @@ +<html> +<head> +<title>Test for bug 1756955</title> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script> + +const kIsMac = navigator.platform.indexOf("Mac") > -1; + +async function copyByDataTransfer(aItems) { + const copyPromise = new Promise(resolve => { + document.addEventListener("copy", (e) => { + e.preventDefault(); + for (const [key, value] of Object.entries(aItems)) { + e.clipboardData.setData(key, value); + } + resolve(); + }, { once: true }); + }); + synthesizeKey( + "c", + kIsMac ? { accelKey: true } : { ctrlKey: true } + ); + await copyPromise; +} + +async function paste(aCallback) { + const pastePromise = new Promise(resolve => { + document.addEventListener("paste", (e) => { + resolve(aCallback(e.clipboardData)); + }, { once: true }); + }); + synthesizeKey( + "v", + kIsMac ? { accelKey: true } : { ctrlKey: true } + ); + await pastePromise; +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.events.asyncClipboard.readText", true], + ["dom.events.asyncClipboard.clipboardItem", true], + ], + }); +}); + +add_task(async function test_mandatory_type() { + const items = { + "text/plain": "X" + Math.random(), + "custom/foo": "X" + Math.random(), + }; + await copyByDataTransfer(items); + await paste(async (clipboardData) => { + for (const [key, value] of Object.entries(items)) { + is(clipboardData.getData(key), value, `Check ${key} type`); + } + + let clipboardItems = await navigator.clipboard.read(); + is(clipboardItems.length, 1, "Should only one clipboardItem"); + is(clipboardItems[0].types.length, 1, "Should only one type"); + is(await clipboardItems[0].getType("text/plain").then(blob => blob.text()), + items["text/plain"], + "Check text/plain type in clipbordItem"); + }); +}); + +add_task(async function test_no_mandatory_type() { + const items = { + "custom/foo": "X" + Math.random(), + }; + await copyByDataTransfer(items); + await paste(async (clipboardData) => { + for (const [key, value] of Object.entries(items)) { + is(clipboardData.getData(key), value, `Check ${key} type`); + } + + let clipboardItems = await navigator.clipboard.read(); + is(clipboardItems.length, 0, "Should only have no clipboardItem"); + }); +}); + +</script> +</head> +<body> +</body> +</html> diff --git a/dom/events/test/mochitest.toml b/dom/events/test/mochitest.toml index 53675a8f49..19a082b1e3 100644 --- a/dom/events/test/mochitest.toml +++ b/dom/events/test/mochitest.toml @@ -448,8 +448,6 @@ skip-if = [ ["test_legacy_touch_api.html"] -["test_marquee_events.html"] - ["test_messageEvent.html"] ["test_messageEvent_init.html"] diff --git a/dom/events/test/pointerevents/mochitest.toml b/dom/events/test/pointerevents/mochitest.toml index 340704f94e..c22e1bdf94 100644 --- a/dom/events/test/pointerevents/mochitest.toml +++ b/dom/events/test/pointerevents/mochitest.toml @@ -126,17 +126,6 @@ skip-if = [ "http2", ] -["test_wpt_pointerevent_change-touch-action-onpointerdown_touch-manual.html"] -support-files = ["wpt/pointerevent_change-touch-action-onpointerdown_touch-manual.html"] -disabled = "disabled" - -["test_wpt_pointerevent_constructor.html"] -support-files = ["wpt/pointerevent_constructor.html"] -skip-if = [ - "http3", - "http2", -] - ["test_wpt_pointerevent_drag_interaction-manual.html"] support-files = ["wpt/html/pointerevent_drag_interaction-manual.html"] skip-if = [ @@ -158,10 +147,6 @@ skip-if = [ support-files = ["wpt/pointerevent_multiple_primary_pointers_boundary_events-manual.html"] disabled = "should be investigated" -["test_wpt_pointerevent_pointerId_scope-manual.html"] -support-files = ["wpt/resources/pointerevent_pointerId_scope-iframe.html"] -disabled = "should be investigated" - ["test_wpt_pointerevent_pointercancel_touch-manual.html"] support-files = ["wpt/pointerevent_pointercancel_touch-manual.html"] skip-if = [ diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_change-touch-action-onpointerdown_touch-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_change-touch-action-onpointerdown_touch-manual.html deleted file mode 100644 index f95b16c850..0000000000 --- a/dom/events/test/pointerevents/test_wpt_pointerevent_change-touch-action-onpointerdown_touch-manual.html +++ /dev/null @@ -1,39 +0,0 @@ -<!DOCTYPE HTML> -<html> -<!-- -https://bugzilla.mozilla.org/show_bug.cgi?id=1000870 ---> - <head> - <meta charset="utf-8"> - <title>Test for Bug 1000870</title> - <meta name="author" content="Maksim Lebedev" /> - <script src="/tests/SimpleTest/SimpleTest.js"></script> - <script src="/tests/SimpleTest/EventUtils.js"></script> - <script type="text/javascript" src="mochitest_support_external.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> - <script type="text/javascript"> - SimpleTest.waitForExplicitFinish(); - function startTest() { - runTestInNewWindow("pointerevent_change-touch-action-onpointerdown_touch-manual.html"); - } - function executeTest(int_win) { - const WM_VSCROLL = 0x0115; - sendTouchEvent(int_win, "target0", "touchstart"); - sendTouchEvent(int_win, "target0", "touchmove"); - sendTouchEvent(int_win, "target0", "touchend"); - - // NOTE: This testcase is about that modifying touch-action during a - // pointerdown callback "should not" affect the gesture detection of the - // touch session started by the pointerdown. That is, a scroll should - // still fired by gesture detection, instead of launching by our own. - var utils = _getDOMWindowUtils(int_win); - var target0 = int_win.document.getElementById("target0"); - utils.sendNativeMouseScrollEvent(target0.getBoundingClientRect().left + 5, - target0.getBoundingClientRect().top + 5, - WM_VSCROLL, 10, 10, 0, 0, 0, target0); - } - </script> - </head> - <body> - </body> -</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_constructor.html b/dom/events/test/pointerevents/test_wpt_pointerevent_constructor.html deleted file mode 100644 index 058e32a967..0000000000 --- a/dom/events/test/pointerevents/test_wpt_pointerevent_constructor.html +++ /dev/null @@ -1,26 +0,0 @@ -<!DOCTYPE HTML> -<html> -<!-- -https://bugzilla.mozilla.org/show_bug.cgi?id=1000870 ---> - <head> - <meta charset="utf-8"> - <title>Test for Bug 1000870</title> - <meta name="author" content="Maksim Lebedev" /> - <script src="/tests/SimpleTest/SimpleTest.js"></script> - <script src="/tests/SimpleTest/EventUtils.js"></script> - <script type="text/javascript" src="mochitest_support_external.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> - <script type="text/javascript"> - SimpleTest.waitForExplicitFinish(); - function startTest() { - runTestInNewWindow("wpt/pointerevent_constructor.html"); - } - function executeTest(int_win) { - // Function should be, but can be empty - } - </script> - </head> - <body> - </body> -</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_pointerId_scope-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_pointerId_scope-manual.html deleted file mode 100644 index f52bf7fc20..0000000000 --- a/dom/events/test/pointerevents/test_wpt_pointerevent_pointerId_scope-manual.html +++ /dev/null @@ -1,27 +0,0 @@ -<!DOCTYPE HTML> -<html> -<!-- -https://bugzilla.mozilla.org/show_bug.cgi?id=1000870 ---> - <head> - <meta charset="utf-8"> - <title>Test for Bug 1000870</title> - <meta name="author" content="Maksim Lebedev" /> - <script src="/tests/SimpleTest/SimpleTest.js"></script> - <script src="/tests/SimpleTest/EventUtils.js"></script> - <script type="text/javascript" src="mochitest_support_external.js"></script> - <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> - <script type="text/javascript"> - SimpleTest.waitForExplicitFinish(); - function startTest() { - runTestInNewWindow("wpt/pointerevent_pointerId_scope-manual.html"); - } - function executeTest(int_win) { - sendTouchEvent(int_win, "target0", "touchstart"); - sendTouchEvent(int_win, "target0", "touchend"); - } - </script> - </head> - <body> - </body> -</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_change-touch-action-onpointerdown_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_change-touch-action-onpointerdown_touch-manual.html deleted file mode 100644 index 04d56cb7a5..0000000000 --- a/dom/events/test/pointerevents/wpt/pointerevent_change-touch-action-onpointerdown_touch-manual.html +++ /dev/null @@ -1,135 +0,0 @@ -<!doctype html> -<html> - <head> - <title>Change touch-action on pointerdown</title> - <meta name="viewport" content="width=device-width"> - <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> - <script src="/resources/testharness.js"></script> - <script src="/resources/testharnessreport.js"></script> - <script src="pointerevent_support.js"></script> - <style> - #target0 { - background: black; - width: 700px; - height: 430px; - color: white; - overflow-y: auto; - overflow-x: auto; - white-space: nowrap; - } - </style> - </head> - <body onload="run()"> - <h1>Pointer Events touch-action attribute support</h1> - <h4>Test Description: Press and hold your touch. Try to scroll text in any direction. - Then release your touch and try to scroll again. Expected: no panning. - </h4> - <p>Note: this test is for touch-devices only</p> - <div id="target0" style="touch-action: auto;"> - <p> - Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem - nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. - Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit - lobortis nisl ut aliquip ex ea commodo consequat. - </p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p> - Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem - nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. - Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit - lobortis nisl ut aliquip ex ea commodo consequat. - </p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p> - Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem - nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. - Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit - lobortis nisl ut aliquip ex ea commodo consequat. - </p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p> - Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem - nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. - Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit - lobortis nisl ut aliquip ex ea commodo consequat. - </p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - <p>Lorem ipsum dolor sit amet...</p> - </div> - <script type='text/javascript'> - var detected_pointertypes = {}; - - var styleIsChanged = false; - var scrollIsReceived = false; - var firstTouchCompleted = false; - var countToPass = 50; - var xScr0, yScr0, xScr1, yScr1; - - setup({ explicit_done: true }); - add_completion_callback(showPointerTypes); - - function run() { - var target0 = document.getElementById("target0"); - - on_event(target0, 'scroll', function(event) { - if(!scrollIsReceived && firstTouchCompleted) { - test(function() { - failOnScroll(); - }, "scroll was received while shouldn't"); - scrollIsReceived = true; - } - done(); - }); - - on_event(target0, 'pointerdown', function(event) { - detected_pointertypes[event.pointerType] = true; - if(!styleIsChanged) { - var before = document.getElementById('target0').style.touchAction; - - document.getElementById('target0').style.touchAction = 'none'; - - var after = document.getElementById('target0').style.touchAction; - - test(function() { - assert_true(before != after, "touch-action was changed"); - }, "touch-action was changed"); - - styleIsChanged = true; - } - }); - - on_event(target0, 'pointerup', function(event) { - firstTouchCompleted = true; - }); - } - </script> - <h1>touch-action: auto to none</h1> - <div id="complete-notice"> - <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> - </div> - <div id="log"></div> - </body> -</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_constructor.html b/dom/events/test/pointerevents/wpt/pointerevent_constructor.html deleted file mode 100644 index b2a779d1f7..0000000000 --- a/dom/events/test/pointerevents/wpt/pointerevent_constructor.html +++ /dev/null @@ -1,106 +0,0 @@ -<!doctype html> -<html> - <head> - <title>PointerEvent: Constructor test</title> - <meta name="viewport" content="width=device-width"> - <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> - <script src="/resources/testharness.js"></script> - <script src="/resources/testharnessreport.js"></script> - <!-- Additional helper script for common checks across event types --> - <script type="text/javascript" src="pointerevent_support.js"></script> - </head> - <body> - <h1>PointerEvent: Dispatch custom event</h1> - <h4>Test Description: This test checks if PointerEvent constructor works properly using synthetic pointerover and pointerout events. For valid results, this test must be run without generating real (trusted) pointerover or pointerout events on the black rectangle below.</h4> - <div id="target0"></div> - <script> - var detected_pointertypes = {}; - add_completion_callback(showPointerTypes); - - async_test(function() { - var target0 = document.getElementById("target0"); - // set values for non-default constructor - var testBubbles = true; - var testCancelable = true; - var testPointerId = 42; - var testPointerType = 'pen'; - var testClientX = 300; - var testClientY = 500; - var testWidth = 3; - var testHeight = 5; - var testTiltX = -45; - var testTiltY = 30; - var testButton = 0; - var testButtons = 1; - var testPressure = 0.4; - var testIsPrimary = true; - - on_event(target0, "pointerover", this.step_func(function(event) { - detected_pointertypes[ event.pointerType ] = true; - generate_tests(assert_equals, [ - ["custom bubbles", event.bubbles, testBubbles], - ["custom cancelable", event.cancelable, testCancelable], - ["custom pointerId", event.pointerId, testPointerId], - ["custom pointerType", event.pointerType, testPointerType], - ["custom button", event.button, testButton], - ["custom buttons", event.buttons, testButtons], - ["custom width", event.width, testWidth], - ["custom height", event.height, testHeight], - ["custom clientX", event.clientX, testClientX], - ["custom clientY", event.clientY, testClientY], - ["custom tiltX", event.tiltX, testTiltX], - ["custom tiltY", event.tiltY, testTiltY], - ["custom isPrimary", event.isPrimary, testIsPrimary] - ]); - test(function() { - assert_approx_equals(event.pressure, testPressure, 0.00000001, "custom pressure: "); - }, "custom pressure: "); - })); - - on_event(target0, "pointerout", this.step_func(function(event) { - generate_tests(assert_equals, [ - ["default pointerId", event.pointerId, 0], - ["default pointerType", event.pointerType, ""], - ["default width", event.width, 1], - ["default height", event.height, 1], - ["default tiltX", event.tiltX, 0], - ["default tiltY", event.tiltY, 0], - ["default pressure", event.pressure, 0], - ["default isPrimary", event.isPrimary, false] - ]); - })); - - on_event(window, "load", this.step_func_done(function() { - assert_not_equals(window.PointerEvent, undefined); - - var pointerEventCustom = new PointerEvent("pointerover", - {bubbles: testBubbles, - cancelable: testCancelable, - pointerId: testPointerId, - pointerType: testPointerType, - width: testWidth, - height: testHeight, - clientX: testClientX, - clientY: testClientY, - tiltX: testTiltX, - tiltY: testTiltY, - button: testButton, - buttons: testButtons, - pressure: testPressure, - isPrimary: testIsPrimary - }); - // A PointerEvent created with a PointerEvent constructor must have all its attributes set to the corresponding values provided to the constructor. - // For attributes where values are not provided to the constructor, the corresponding default values must be used. - // TA: 12.1 - target0.dispatchEvent(pointerEventCustom); - var pointerEventDefault = new PointerEvent("pointerout"); - target0.dispatchEvent(pointerEventDefault); - }, "PointerEvent constructor")); - }) - </script> - <div id="complete-notice"> - <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> - </div> - <div id="log"></div> - </body> -</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_pointerId_scope-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_pointerId_scope-manual.html deleted file mode 100644 index 3640cb6f6b..0000000000 --- a/dom/events/test/pointerevents/wpt/pointerevent_pointerId_scope-manual.html +++ /dev/null @@ -1,82 +0,0 @@ -<!doctype html> -<html> - <!-- -Test cases for Pointer Events v1 spec -This document references Test Assertions (abbrev TA below) written by Cathy Chan -http://www.w3.org/wiki/PointerEvents/TestAssertions ---> - <head> - <title>Pointer Events pointerdown tests</title> - <meta name="viewport" content="width=device-width"> - <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> - <script src="/resources/testharness.js"></script> - <script src="/resources/testharnessreport.js"></script> - <!-- Additional helper script for common checks across event types --> - <script type="text/javascript" src="pointerevent_support.js"></script> - <script> - var detected_pointertypes = {}; - var test_pointerEvent = async_test("pointerId of an active pointer is the same across iframes"); - // showPointerTypes is defined in pointerevent_support.js - // Requirements: the callback function will reference the test_pointerEvent object and - // will fail unless the async_test is created with the var name "test_pointerEvent". - add_completion_callback(showPointerTypes); - var detected_pointertypes = {}; - - function run() { - var target0 = document.getElementById("target0"); - var pointerover_pointerId = null; - var pointerover_pointerType = null; - - var eventList = ['pointerenter', 'pointerover', 'pointermove', 'pointerout', 'pointerleave']; - var receivedEvents = {}; - var receivedEventsInnerFrame = {}; - - - function checkPointerId(event, inner) { - detected_pointertypes[event.pointerType] = true; - var eventName = (inner ? "inner frame " : "" ) + event.type; - test_pointerEvent.step(function() { - assert_equals(event.pointerId, pointerover_pointerId, "PointerId of " + eventName + " is not correct"); - assert_equals(event.pointerType, pointerover_pointerType, "PointerType of " + eventName + " is not correct"); - }, eventName + ".pointerId were the same as first pointerover"); - } - - on_event(window, "message", function(event) { - var pe_event = JSON.parse(event.data); - receivedEventsInnerFrame[pe_event.type] = 1; - checkPointerId(pe_event, true); - if (Object.keys(receivedEvents).length == eventList.length && Object.keys(receivedEventsInnerFrame).length == eventList.length) - test_pointerEvent.done(); - }); - - eventList.forEach(function(eventName) { - on_event(target0, eventName, function (event) { - if (pointerover_pointerId === null && event.type == 'pointerover') { - pointerover_pointerId = event.pointerId; - pointerover_pointerType = event.pointerType; - } else { - checkPointerId(event, false); - } - receivedEvents[event.type] = 1; - }); - }); - } - </script> - </head> - <body onload="run()"> - <h1>Pointer Events pointerdown tests</h1> - Complete the following actions: - <ol> - <li>Start with your pointing device outside of black box, then move it into black box. If using touch just press in black box and don't release. - <li>Move your pointing device into purple box (without leaving the digitizer range if you are using hover supported pen or without releasing touch if using touch). Then move it out of the purple box. - </ol> - <div id="target0" class="touchActionNone"> - </div> - <iframe src="resources/pointerevent_pointerId_scope-iframe.html" id="innerframe"></iframe> - <div id="complete-notice"> - <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> - <p>Refresh the page to run the tests again with a different pointer type.</p> - </div> - <div id="log"></div> - </body> -</html> diff --git a/dom/events/test/test_all_synthetic_events.html b/dom/events/test/test_all_synthetic_events.html index d8de1a9148..3a00227f1b 100644 --- a/dom/events/test/test_all_synthetic_events.html +++ b/dom/events/test/test_all_synthetic_events.html @@ -376,6 +376,13 @@ const kEventConstructors = { return new TCPServerSocketEvent(aName, aProps); }, }, + TextEvent : { create (aName, aProps) { + var e = document.createEvent("textevent"); + e.initTextEvent("textInput", aProps.bubbles, aProps.cancelable, + aProps.view, aProps.data); + return e; + }, + }, TimeEvent: { create: null // Cannot create untrusted event from JS }, @@ -477,7 +484,8 @@ function test() { SimpleTest.waitForExplicitFinish(); SpecialPowers.pushPrefEnv( {"set": [["dom.w3c_touch_events.legacy_apis.enabled", true], - ["layout.css.content-visibility.enabled", true]]}, + ["layout.css.content-visibility.enabled", true], + ["dom.events.textevent.enabled", true]]}, function() { test(); SimpleTest.finish(); diff --git a/dom/events/test/test_eventctors.html b/dom/events/test/test_eventctors.html index 01ae59493e..26b9d647bf 100644 --- a/dom/events/test/test_eventctors.html +++ b/dom/events/test/test_eventctors.html @@ -924,6 +924,28 @@ is(e.dataTransfer, null, "InputEvent.dataTransfer should be null in default"); is(e.inputType, "", "InputEvent.inputType should be empty string in default"); is(e.isComposing, false, "InputEvent.isComposing should be false in default"); +// TextEvent +if (SpecialPowers.getBoolPref("dom.events.textevent.enabled")) { + try { + e = new TextEvent(); + ok(false, "TextEvent should not have constructor"); + } catch (exp) { + ok(true, "TextEvent does not have a constructor"); + } + try { + e = new TextEvent("foo"); + ok(false, "TextEvent should not have constructor"); + } catch (exp) { + ok(true, "TextEvent does not have a constructor taking a event type"); + } + try { + e = new TextEvent("foo", {}); + ok(false, "TextEvent should not have constructor"); + } catch (exp) { + ok(true, "TextEvent does not have a constructor taking event type and a dictionary"); + } +} + </script> </pre> </body> diff --git a/dom/events/test/test_marquee_events.html b/dom/events/test/test_marquee_events.html deleted file mode 100644 index 22d0eafdf1..0000000000 --- a/dom/events/test/test_marquee_events.html +++ /dev/null @@ -1,31 +0,0 @@ -<html> -<head> - <meta charset="utf-8"> - <title>Test for bug 1425874</title> - <link rel="stylesheet" href="/tests/SimpleTest/test.css"> - <script src="/tests/SimpleTest/SimpleTest.js"></script> - <script src="/tests/SimpleTest/EventUtils.js"></script> -</head> -<body> - <script> - var wasEventCalled; - function callEventWithAttributeHandler(element, evt) { - wasEventCalled = false; - let el = document.createElement(element); - el.setAttribute(`on${evt}`, "wasEventCalled = true"); - el.dispatchEvent(new Event(evt)); - return wasEventCalled; - } - - info("Make sure the EventNameType_HTMLMarqueeOnly events only compile for marquee"); - - ok(!callEventWithAttributeHandler("div", "bounce"), "no onbounce for div"); - ok(!callEventWithAttributeHandler("div", "finish"), "no onfinish for div"); - ok(!callEventWithAttributeHandler("div", "start"), "no onstart for div"); - - ok(callEventWithAttributeHandler("marquee", "bounce"), "onbounce for marquee"); - ok(callEventWithAttributeHandler("marquee", "finish"), "onfinish for marquee"); - ok(callEventWithAttributeHandler("marquee", "start"), "onstart for marquee"); - </script> -</body> -</html> diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp index 965daf35eb..e2261d2e88 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp @@ -470,7 +470,7 @@ class MainThreadFetchRunnable : public Runnable { }; already_AddRefed<Promise> FetchRequest(nsIGlobalObject* aGlobal, - const RequestOrUSVString& aInput, + const RequestOrUTF8String& aInput, const RequestInit& aInit, CallerType aCallerType, ErrorResult& aRv) { diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h index 9b9dccd1ee..2f05c6f743 100644 --- a/dom/fetch/Fetch.h +++ b/dom/fetch/Fetch.h @@ -44,13 +44,13 @@ class OwningBlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString; class ReadableStreamDefaultReader; -class RequestOrUSVString; +class RequestOrUTF8String; class WorkerPrivate; enum class CallerType : uint32_t; already_AddRefed<Promise> FetchRequest(nsIGlobalObject* aGlobal, - const RequestOrUSVString& aInput, + const RequestOrUTF8String& aInput, const RequestInit& aInit, CallerType aCallerType, ErrorResult& aRv); diff --git a/dom/fetch/FetchDriver.cpp b/dom/fetch/FetchDriver.cpp index c4acc35cf5..79b54f4c4a 100644 --- a/dom/fetch/FetchDriver.cpp +++ b/dom/fetch/FetchDriver.cpp @@ -441,7 +441,7 @@ void FetchDriver::UpdateReferrerInfoFromNewChannel(nsIChannel* aChannel) { return; } - nsAutoString computedReferrerSpec; + nsAutoCString computedReferrerSpec; mRequest->SetReferrerPolicy(referrerInfo->ReferrerPolicy()); Unused << referrerInfo->GetComputedReferrerSpec(computedReferrerSpec); mRequest->SetReferrer(computedReferrerSpec); diff --git a/dom/fetch/FetchTypes.ipdlh b/dom/fetch/FetchTypes.ipdlh index f8c594bf1a..f75de35abd 100644 --- a/dom/fetch/FetchTypes.ipdlh +++ b/dom/fetch/FetchTypes.ipdlh @@ -59,7 +59,7 @@ struct IPCInternalRequest { int64_t bodySize; nsCString preferredAlternativeDataType; uint32_t contentPolicyType; - nsString referrer; + nsCString referrer; ReferrerPolicy referrerPolicy; ReferrerPolicy environmentReferrerPolicy; RequestMode requestMode; diff --git a/dom/fetch/FetchUtil.cpp b/dom/fetch/FetchUtil.cpp index 040e23e4dc..486b58bc37 100644 --- a/dom/fetch/FetchUtil.cpp +++ b/dom/fetch/FetchUtil.cpp @@ -128,7 +128,7 @@ nsresult FetchUtil::SetRequestReferrer(nsIPrincipal* aPrincipal, Document* aDoc, MOZ_ASSERT(NS_IsMainThread()); nsresult rv = NS_OK; - nsAutoString referrer; + nsAutoCString referrer; aRequest.GetReferrer(referrer); ReferrerPolicy policy = aRequest.ReferrerPolicy_(); @@ -154,7 +154,7 @@ nsresult FetchUtil::SetRequestReferrer(nsIPrincipal* aPrincipal, Document* aDoc, rv = aChannel->SetReferrerInfoWithoutClone(referrerInfo); NS_ENSURE_SUCCESS(rv, rv); - nsAutoString computedReferrerSpec; + nsAutoCString computedReferrerSpec; referrerInfo = aChannel->GetReferrerInfo(); if (referrerInfo) { Unused << referrerInfo->GetComputedReferrerSpec(computedReferrerSpec); @@ -719,7 +719,7 @@ bool FetchUtil::StreamResponseToJS(JSContext* aCx, JS::Handle<JSObject*> aObj, switch (aMimeType) { case JS::MimeType::Wasm: - nsAutoString url; + nsAutoCString url; response->GetUrl(url); IgnoredErrorResult result; @@ -728,9 +728,8 @@ bool FetchUtil::StreamResponseToJS(JSContext* aCx, JS::Handle<JSObject*> aObj, if (NS_WARN_IF(result.Failed())) { return ThrowException(aCx, JSMSG_WASM_ERROR_CONSUMING_RESPONSE); } - NS_ConvertUTF16toUTF8 urlUTF8(url); aConsumer->noteResponseURLs( - urlUTF8.get(), sourceMapUrl.IsVoid() ? nullptr : sourceMapUrl.get()); + url.get(), sourceMapUrl.IsVoid() ? nullptr : sourceMapUrl.get()); break; } diff --git a/dom/fetch/InternalRequest.cpp b/dom/fetch/InternalRequest.cpp index b5cc7cf291..d2086e58eb 100644 --- a/dom/fetch/InternalRequest.cpp +++ b/dom/fetch/InternalRequest.cpp @@ -86,7 +86,7 @@ InternalRequest::InternalRequest(const nsACString& aURL, mHeaders(new InternalHeaders(HeadersGuardEnum::None)), mBodyLength(InternalResponse::UNKNOWN_BODY_SIZE), mContentPolicyType(nsIContentPolicy::TYPE_FETCH), - mReferrer(NS_LITERAL_STRING_FROM_CSTRING(kFETCH_CLIENT_REFERRER_STR)), + mReferrer(nsLiteralCString(kFETCH_CLIENT_REFERRER_STR)), mReferrerPolicy(ReferrerPolicy::_empty), mEnvironmentReferrerPolicy(ReferrerPolicy::_empty), mMode(RequestMode::No_cors), @@ -102,7 +102,7 @@ InternalRequest::InternalRequest( const nsACString& aMethod, already_AddRefed<InternalHeaders> aHeaders, RequestCache aCacheMode, RequestMode aMode, RequestRedirect aRequestRedirect, RequestCredentials aRequestCredentials, - const nsAString& aReferrer, ReferrerPolicy aReferrerPolicy, + const nsACString& aReferrer, ReferrerPolicy aReferrerPolicy, RequestPriority aPriority, nsContentPolicyType aContentPolicyType, const nsAString& aIntegrity) : mMethod(aMethod), diff --git a/dom/fetch/InternalRequest.h b/dom/fetch/InternalRequest.h index d2b7721f5e..3a14a907ad 100644 --- a/dom/fetch/InternalRequest.h +++ b/dom/fetch/InternalRequest.h @@ -92,7 +92,7 @@ class InternalRequest final : public AtomicSafeRefCounted<InternalRequest> { RequestCache aCacheMode, RequestMode aMode, RequestRedirect aRequestRedirect, RequestCredentials aRequestCredentials, - const nsAString& aReferrer, ReferrerPolicy aReferrerPolicy, + const nsACString& aReferrer, ReferrerPolicy aReferrerPolicy, RequestPriority aPriority, nsContentPolicyType aContentPolicyType, const nsAString& aIntegrity); @@ -159,9 +159,9 @@ class InternalRequest final : public AtomicSafeRefCounted<InternalRequest> { void GetURLListWithoutFragment(nsTArray<nsCString>& aURLList) { aURLList.Assign(mURLList); } - void GetReferrer(nsAString& aReferrer) const { aReferrer.Assign(mReferrer); } + void GetReferrer(nsACString& aReferrer) const { aReferrer.Assign(mReferrer); } - void SetReferrer(const nsAString& aReferrer) { + void SetReferrer(const nsACString& aReferrer) { #ifdef DEBUG bool validReferrer = false; if (aReferrer.IsEmpty() || @@ -179,10 +179,9 @@ class InternalRequest final : public AtomicSafeRefCounted<InternalRequest> { uint32_t pathPos; int32_t pathLen; - NS_ConvertUTF16toUTF8 ref(aReferrer); - nsresult rv = - parser->ParseURL(ref.get(), ref.Length(), &schemePos, &schemeLen, - &authorityPos, &authorityLen, &pathPos, &pathLen); + nsresult rv = parser->ParseURL( + aReferrer.BeginReading(), aReferrer.Length(), &schemePos, + &schemeLen, &authorityPos, &authorityLen, &pathPos, &pathLen); if (NS_FAILED(rv)) { NS_WARNING("Invalid referrer URL!"); } else if (schemeLen < 0 || authorityLen < 0) { @@ -263,6 +262,10 @@ class InternalRequest final : public AtomicSafeRefCounted<InternalRequest> { mIntegrity.Assign(aIntegrity); } + bool GetKeepalive() const { return mKeepalive; } + + void SetKeepalive(const bool aKeepalive) { mKeepalive = aKeepalive; } + bool MozErrors() const { return mMozErrors; } void SetMozErrors() { mMozErrors = true; } @@ -440,7 +443,7 @@ class InternalRequest final : public AtomicSafeRefCounted<InternalRequest> { // Empty string: no-referrer // "about:client": client (default) // URL: an URL - nsString mReferrer; + nsCString mReferrer; ReferrerPolicy mReferrerPolicy; // This will be used for request created from Window or Worker contexts @@ -454,6 +457,7 @@ class InternalRequest final : public AtomicSafeRefCounted<InternalRequest> { RequestRedirect mRedirectMode; RequestPriority mPriorityMode = RequestPriority::Auto; nsString mIntegrity; + bool mKeepalive = false; bool mMozErrors = false; nsCString mFragment; bool mSkipServiceWorker = false; diff --git a/dom/fetch/Request.cpp b/dom/fetch/Request.cpp index e042dd6271..f1ae053b98 100644 --- a/dom/fetch/Request.cpp +++ b/dom/fetch/Request.cpp @@ -12,6 +12,7 @@ #include "nsPIDOMWindow.h" #include "mozilla/ErrorResult.h" +#include "mozilla/StaticPrefs_network.h" #include "mozilla/dom/Headers.h" #include "mozilla/dom/Fetch.h" #include "mozilla/dom/FetchUtil.h" @@ -77,29 +78,22 @@ SafeRefPtr<InternalRequest> Request::GetInternalRequest() { namespace { already_AddRefed<nsIURI> ParseURLFromDocument(Document* aDocument, - const nsAString& aInput, + const nsACString& aInput, ErrorResult& aRv) { MOZ_ASSERT(aDocument); MOZ_ASSERT(NS_IsMainThread()); - // Don't use NS_ConvertUTF16toUTF8 because that doesn't let us handle OOM. - nsAutoCString input; - if (!AppendUTF16toUTF8(aInput, input, fallible)) { - aRv.Throw(NS_ERROR_OUT_OF_MEMORY); - return nullptr; - } - nsCOMPtr<nsIURI> resolvedURI; - nsresult rv = NS_NewURI(getter_AddRefs(resolvedURI), input, nullptr, + nsresult rv = NS_NewURI(getter_AddRefs(resolvedURI), aInput, nullptr, aDocument->GetBaseURI()); if (NS_WARN_IF(NS_FAILED(rv))) { - aRv.ThrowTypeError<MSG_INVALID_URL>(input); + aRv.ThrowTypeError<MSG_INVALID_URL>(aInput); } return resolvedURI.forget(); } -void GetRequestURLFromDocument(Document* aDocument, const nsAString& aInput, - nsAString& aRequestURL, nsACString& aURLfragment, - ErrorResult& aRv) { +void GetRequestURLFromDocument(Document* aDocument, const nsACString& aInput, + nsACString& aRequestURL, + nsACString& aURLfragment, ErrorResult& aRv) { nsCOMPtr<nsIURI> resolvedURI = ParseURLFromDocument(aDocument, aInput, aRv); if (aRv.Failed()) { return; @@ -109,7 +103,7 @@ void GetRequestURLFromDocument(Document* aDocument, const nsAString& aInput, nsAutoCString credentials; Unused << resolvedURI->GetUserPass(credentials); if (!credentials.IsEmpty()) { - aRv.ThrowTypeError<MSG_URL_HAS_CREDENTIALS>(NS_ConvertUTF16toUTF8(aInput)); + aRv.ThrowTypeError<MSG_URL_HAS_CREDENTIALS>(aInput); return; } @@ -118,12 +112,10 @@ void GetRequestURLFromDocument(Document* aDocument, const nsAString& aInput, if (NS_WARN_IF(aRv.Failed())) { return; } - nsAutoCString spec; - aRv = resolvedURIClone->GetSpec(spec); + aRv = resolvedURIClone->GetSpec(aRequestURL); if (NS_WARN_IF(aRv.Failed())) { return; } - CopyUTF8toUTF16(spec, aRequestURL); // Get the fragment from nsIURI. aRv = resolvedURI->GetRef(aURLfragment); @@ -131,24 +123,18 @@ void GetRequestURLFromDocument(Document* aDocument, const nsAString& aInput, return; } } -already_AddRefed<nsIURI> ParseURLFromChrome(const nsAString& aInput, +already_AddRefed<nsIURI> ParseURLFromChrome(const nsACString& aInput, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); - // Don't use NS_ConvertUTF16toUTF8 because that doesn't let us handle OOM. - nsAutoCString input; - if (!AppendUTF16toUTF8(aInput, input, fallible)) { - aRv.Throw(NS_ERROR_OUT_OF_MEMORY); - return nullptr; - } nsCOMPtr<nsIURI> uri; - nsresult rv = NS_NewURI(getter_AddRefs(uri), input); + nsresult rv = NS_NewURI(getter_AddRefs(uri), aInput); if (NS_FAILED(rv)) { - aRv.ThrowTypeError<MSG_INVALID_URL>(input); + aRv.ThrowTypeError<MSG_INVALID_URL>(aInput); } return uri.forget(); } -void GetRequestURLFromChrome(const nsAString& aInput, nsAString& aRequestURL, +void GetRequestURLFromChrome(const nsACString& aInput, nsACString& aRequestURL, nsACString& aURLfragment, ErrorResult& aRv) { nsCOMPtr<nsIURI> uri = ParseURLFromChrome(aInput, aRv); if (aRv.Failed()) { @@ -159,7 +145,7 @@ void GetRequestURLFromChrome(const nsAString& aInput, nsAString& aRequestURL, nsAutoCString credentials; Unused << uri->GetUserPass(credentials); if (!credentials.IsEmpty()) { - aRv.ThrowTypeError<MSG_URL_HAS_CREDENTIALS>(NS_ConvertUTF16toUTF8(aInput)); + aRv.ThrowTypeError<MSG_URL_HAS_CREDENTIALS>(aInput); return; } @@ -168,12 +154,10 @@ void GetRequestURLFromChrome(const nsAString& aInput, nsAString& aRequestURL, if (NS_WARN_IF(aRv.Failed())) { return; } - nsAutoCString spec; - aRv = uriClone->GetSpec(spec); + aRv = uriClone->GetSpec(aRequestURL); if (NS_WARN_IF(aRv.Failed())) { return; } - CopyUTF8toUTF16(spec, aRequestURL); // Get the fragment from nsIURI. aRv = uri->GetRef(aURLfragment); @@ -182,55 +166,55 @@ void GetRequestURLFromChrome(const nsAString& aInput, nsAString& aRequestURL, } } already_AddRefed<URL> ParseURLFromWorker(nsIGlobalObject* aGlobal, - const nsAString& aInput, + const nsACString& aInput, ErrorResult& aRv) { WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(worker); worker->AssertIsOnWorkerThread(); - NS_ConvertUTF8toUTF16 baseURL(worker->GetLocationInfo().mHref); + const auto& baseURL = worker->GetLocationInfo().mHref; RefPtr<URL> url = URL::Constructor(aGlobal, aInput, baseURL, aRv); if (NS_WARN_IF(aRv.Failed())) { - aRv.ThrowTypeError<MSG_INVALID_URL>(NS_ConvertUTF16toUTF8(aInput)); + aRv.ThrowTypeError<MSG_INVALID_URL>(aInput); } return url.forget(); } -void GetRequestURLFromWorker(nsIGlobalObject* aGlobal, const nsAString& aInput, - nsAString& aRequestURL, nsACString& aURLfragment, +void GetRequestURLFromWorker(nsIGlobalObject* aGlobal, const nsACString& aInput, + nsACString& aRequestURL, nsACString& aURLfragment, ErrorResult& aRv) { RefPtr<URL> url = ParseURLFromWorker(aGlobal, aInput, aRv); if (aRv.Failed()) { return; } - nsString username; + nsCString username; url->GetUsername(username); - nsString password; + nsCString password; url->GetPassword(password); if (!username.IsEmpty() || !password.IsEmpty()) { - aRv.ThrowTypeError<MSG_URL_HAS_CREDENTIALS>(NS_ConvertUTF16toUTF8(aInput)); + aRv.ThrowTypeError<MSG_URL_HAS_CREDENTIALS>(aInput); return; } // Get the fragment from URL. - nsAutoString fragment; + nsAutoCString fragment; url->GetHash(fragment); // Note: URL::GetHash() includes the "#" and we want the fragment with out // the hash symbol. if (!fragment.IsEmpty()) { - CopyUTF16toUTF8(Substring(fragment, 1), aURLfragment); + aURLfragment = Substring(fragment, 1); } - url->SetHash(u""_ns); + url->SetHash(""_ns); url->GetHref(aRequestURL); } class ReferrerSameOriginChecker final : public WorkerMainThreadRunnable { public: ReferrerSameOriginChecker(WorkerPrivate* aWorkerPrivate, - const nsAString& aReferrerURL, nsresult& aResult) + const nsACString& aReferrerURL, nsresult& aResult) : WorkerMainThreadRunnable(aWorkerPrivate, "Fetch :: Referrer same origin check"_ns), mReferrerURL(aReferrerURL), @@ -241,8 +225,7 @@ class ReferrerSameOriginChecker final : public WorkerMainThreadRunnable { bool MainThreadRun() override { nsCOMPtr<nsIURI> uri; if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), mReferrerURL))) { - nsCOMPtr<nsIPrincipal> principal = mWorkerPrivate->GetPrincipal(); - if (principal) { + if (nsCOMPtr<nsIPrincipal> principal = mWorkerPrivate->GetPrincipal()) { mResult = principal->CheckMayLoad(uri, /* allowIfInheritsPrincipal */ false); } @@ -251,7 +234,7 @@ class ReferrerSameOriginChecker final : public WorkerMainThreadRunnable { } private: - const nsString mReferrerURL; + const nsCString mReferrerURL; nsresult& mResult; }; @@ -259,19 +242,18 @@ class ReferrerSameOriginChecker final : public WorkerMainThreadRunnable { /*static*/ SafeRefPtr<Request> Request::Constructor(const GlobalObject& aGlobal, - const RequestOrUSVString& aInput, + const RequestOrUTF8String& aInput, const RequestInit& aInit, ErrorResult& aRv) { nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); - return Constructor(global, aGlobal.Context(), aInput, aInit, aRv); + return Constructor(global, aGlobal.Context(), aInput, aInit, + aGlobal.CallerType(), aRv); } /*static*/ -SafeRefPtr<Request> Request::Constructor(nsIGlobalObject* aGlobal, - JSContext* aCx, - const RequestOrUSVString& aInput, - const RequestInit& aInit, - ErrorResult& aRv) { +SafeRefPtr<Request> Request::Constructor( + nsIGlobalObject* aGlobal, JSContext* aCx, const RequestOrUTF8String& aInput, + const RequestInit& aInit, CallerType aCallerType, ErrorResult& aRv) { bool hasCopiedBody = false; SafeRefPtr<InternalRequest> request; @@ -301,11 +283,10 @@ SafeRefPtr<Request> Request::Constructor(nsIGlobalObject* aGlobal, request = inputReq->GetInternalRequest(); signal = inputReq->GetOrCreateSignal(); } else { - // aInput is USVString. + // aInput is UTF8String. // We need to get url before we create a InternalRequest. - nsAutoString input; - input.Assign(aInput.GetAsUSVString()); - nsAutoString requestURL; + const nsACString& input = aInput.GetAsUTF8String(); + nsAutoCString requestURL; nsCString fragment; if (NS_IsMainThread()) { nsCOMPtr<nsPIDOMWindowInner> inner(do_QueryInterface(aGlobal)); @@ -322,8 +303,7 @@ SafeRefPtr<Request> Request::Constructor(nsIGlobalObject* aGlobal, if (aRv.Failed()) { return nullptr; } - request = MakeSafeRefPtr<InternalRequest>(NS_ConvertUTF16toUTF8(requestURL), - fragment); + request = MakeSafeRefPtr<InternalRequest>(requestURL, fragment); } request = request->GetRequestConstructorCopy(aGlobal, aRv); if (NS_WARN_IF(aRv.Failed())) { @@ -346,12 +326,17 @@ SafeRefPtr<Request> Request::Constructor(nsIGlobalObject* aGlobal, if (aInit.mCache.WasPassed()) { cache.emplace(aInit.mCache.Value()); } - if (aInput.IsUSVString()) { + if (aInput.IsUTF8String()) { if (mode.isNothing()) { mode.emplace(RequestMode::Cors); } if (credentials.isNothing()) { - credentials.emplace(RequestCredentials::Same_origin); + if (aCallerType == CallerType::System && + StaticPrefs::network_fetch_systemDefaultsToOmittingCredentials()) { + credentials.emplace(RequestCredentials::Omit); + } else { + credentials.emplace(RequestCredentials::Same_origin); + } } if (cache.isNothing()) { cache.emplace(RequestCache::Default); @@ -363,16 +348,15 @@ SafeRefPtr<Request> Request::Constructor(nsIGlobalObject* aGlobal, } if (aInit.IsAnyMemberPresent()) { - request->SetReferrer( - NS_LITERAL_STRING_FROM_CSTRING(kFETCH_CLIENT_REFERRER_STR)); + request->SetReferrer(nsLiteralCString(kFETCH_CLIENT_REFERRER_STR)); request->SetReferrerPolicy(ReferrerPolicy::_empty); } if (aInit.mReferrer.WasPassed()) { - const nsString& referrer = aInit.mReferrer.Value(); + const nsCString& referrer = aInit.mReferrer.Value(); if (referrer.IsEmpty()) { - request->SetReferrer(u""_ns); + request->SetReferrer(""_ns); } else { - nsAutoString referrerURL; + nsAutoCString referrerURL; if (NS_IsMainThread()) { nsCOMPtr<nsPIDOMWindowInner> inner(do_QueryInterface(aGlobal)); Document* doc = inner ? inner->GetExtantDoc() : nullptr; @@ -385,13 +369,10 @@ SafeRefPtr<Request> Request::Constructor(nsIGlobalObject* aGlobal, uri = ParseURLFromChrome(referrer, aRv); } if (NS_WARN_IF(aRv.Failed())) { - aRv.ThrowTypeError<MSG_INVALID_REFERRER_URL>( - NS_ConvertUTF16toUTF8(referrer)); + aRv.ThrowTypeError<MSG_INVALID_REFERRER_URL>(referrer); return nullptr; } - nsAutoCString spec; - uri->GetSpec(spec); - CopyUTF8toUTF16(spec, referrerURL); + uri->GetSpec(referrerURL); if (!referrerURL.EqualsLiteral(kFETCH_CLIENT_REFERRER_STR)) { nsCOMPtr<nsIPrincipal> principal = aGlobal->PrincipalOrNull(); if (principal) { @@ -406,8 +387,7 @@ SafeRefPtr<Request> Request::Constructor(nsIGlobalObject* aGlobal, } else { RefPtr<URL> url = ParseURLFromWorker(aGlobal, referrer, aRv); if (NS_WARN_IF(aRv.Failed())) { - aRv.ThrowTypeError<MSG_INVALID_REFERRER_URL>( - NS_ConvertUTF16toUTF8(referrer)); + aRv.ThrowTypeError<MSG_INVALID_REFERRER_URL>(referrer); return nullptr; } url->GetHref(referrerURL); @@ -516,6 +496,10 @@ SafeRefPtr<Request> Request::Constructor(nsIGlobalObject* aGlobal, request->SetIntegrity(aInit.mIntegrity.Value()); } + if (aInit.mKeepalive.WasPassed()) { + request->SetKeepalive(aInit.mKeepalive.Value()); + } + if (aInit.mMozErrors.WasPassed() && aInit.mMozErrors.Value()) { request->SetMozErrors(); } diff --git a/dom/fetch/Request.h b/dom/fetch/Request.h index ee8deffe5a..d4d10a2deb 100644 --- a/dom/fetch/Request.h +++ b/dom/fetch/Request.h @@ -21,7 +21,7 @@ namespace mozilla::dom { class Headers; class InternalHeaders; -class RequestOrUSVString; +class RequestOrUTF8String; class Request final : public FetchBody<Request>, public nsWrapperCache { NS_DECL_ISUPPORTS_INHERITED @@ -37,12 +37,7 @@ class Request final : public FetchBody<Request>, public nsWrapperCache { return Request_Binding::Wrap(aCx, this, aGivenProto); } - void GetUrl(nsAString& aUrl) const { - nsAutoCString url; - mRequest->GetURL(url); - CopyUTF8toUTF16(url, aUrl); - } - + void GetUrl(nsACString& aUrl) const { mRequest->GetURL(aUrl); } void GetMethod(nsCString& aMethod) const { aMethod = mRequest->mMethod; } RequestMode Mode() const { return mRequest->mMode; } @@ -59,6 +54,8 @@ class Request final : public FetchBody<Request>, public nsWrapperCache { aIntegrity = mRequest->GetIntegrity(); } + bool Keepalive() const { return mRequest->GetKeepalive(); } + bool MozErrors() const { return mRequest->MozErrors(); } RequestDestination Destination() const { return mRequest->Destination(); } @@ -72,7 +69,7 @@ class Request final : public FetchBody<Request>, public nsWrapperCache { return mRequest->IsContentPolicyTypeOverridden(); } - void GetReferrer(nsAString& aReferrer) const { + void GetReferrer(nsACString& aReferrer) const { mRequest->GetReferrer(aReferrer); } @@ -103,14 +100,15 @@ class Request final : public FetchBody<Request>, public nsWrapperCache { const nsAString& BodyLocalPath() const { return mRequest->BodyLocalPath(); } static SafeRefPtr<Request> Constructor(const GlobalObject& aGlobal, - const RequestOrUSVString& aInput, + const RequestOrUTF8String& aInput, const RequestInit& aInit, ErrorResult& rv); static SafeRefPtr<Request> Constructor(nsIGlobalObject* aGlobal, JSContext* aCx, - const RequestOrUSVString& aInput, + const RequestOrUTF8String& aInput, const RequestInit& aInit, + const CallerType aCallerType, ErrorResult& rv); nsIGlobalObject* GetParentObject() const { return mOwner; } diff --git a/dom/fetch/Response.cpp b/dom/fetch/Response.cpp index 9e4cdf84ce..be3decc514 100644 --- a/dom/fetch/Response.cpp +++ b/dom/fetch/Response.cpp @@ -84,53 +84,42 @@ already_AddRefed<Response> Response::Error(const GlobalObject& aGlobal) { /* static */ already_AddRefed<Response> Response::Redirect(const GlobalObject& aGlobal, - const nsAString& aUrl, + const nsACString& aUrl, uint16_t aStatus, ErrorResult& aRv) { - nsAutoString parsedURL; + nsAutoCString parsedURL; if (NS_IsMainThread()) { nsIURI* baseURI = nullptr; nsCOMPtr<nsPIDOMWindowInner> inner( do_QueryInterface(aGlobal.GetAsSupports())); - Document* doc = inner ? inner->GetExtantDoc() : nullptr; - if (doc) { + if (Document* doc = inner ? inner->GetExtantDoc() : nullptr) { baseURI = doc->GetBaseURI(); } - // Don't use NS_ConvertUTF16toUTF8 because that doesn't let us handle OOM. - nsAutoCString url; - if (!AppendUTF16toUTF8(aUrl, url, fallible)) { - aRv.Throw(NS_ERROR_OUT_OF_MEMORY); - return nullptr; - } - nsCOMPtr<nsIURI> resolvedURI; - nsresult rv = NS_NewURI(getter_AddRefs(resolvedURI), url, nullptr, baseURI); + nsresult rv = + NS_NewURI(getter_AddRefs(resolvedURI), aUrl, nullptr, baseURI); if (NS_WARN_IF(NS_FAILED(rv))) { - aRv.ThrowTypeError<MSG_INVALID_URL>(url); + aRv.ThrowTypeError<MSG_INVALID_URL>(aUrl); return nullptr; } - nsAutoCString spec; - rv = resolvedURI->GetSpec(spec); + rv = resolvedURI->GetSpec(parsedURL); if (NS_WARN_IF(NS_FAILED(rv))) { - aRv.ThrowTypeError<MSG_INVALID_URL>(url); + aRv.ThrowTypeError<MSG_INVALID_URL>(aUrl); return nullptr; } - - CopyUTF8toUTF16(spec, parsedURL); } else { WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(worker); worker->AssertIsOnWorkerThread(); - NS_ConvertUTF8toUTF16 baseURL(worker->GetLocationInfo().mHref); + const auto& baseURL = worker->GetLocationInfo().mHref; RefPtr<URL> url = URL::Constructor(aGlobal.GetAsSupports(), aUrl, baseURL, aRv); if (aRv.Failed()) { return nullptr; } - url->GetHref(parsedURL); } @@ -152,8 +141,7 @@ already_AddRefed<Response> Response::Redirect(const GlobalObject& aGlobal, return nullptr; } - r->GetInternalHeaders()->Set("Location"_ns, NS_ConvertUTF16toUTF8(parsedURL), - aRv); + r->GetInternalHeaders()->Set("Location"_ns, parsedURL, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } diff --git a/dom/fetch/Response.h b/dom/fetch/Response.h index 1aa0e56c31..14c0e63bb2 100644 --- a/dom/fetch/Response.h +++ b/dom/fetch/Response.h @@ -43,9 +43,7 @@ class Response final : public FetchBody<Response>, public nsWrapperCache { } ResponseType Type() const { return mInternalResponse->Type(); } - void GetUrl(nsAString& aUrl) const { - CopyUTF8toUTF16(mInternalResponse->GetURL(), aUrl); - } + void GetUrl(nsACString& aUrl) const { aUrl = mInternalResponse->GetURL(); } bool Redirected() const { return mInternalResponse->IsRedirected(); } uint16_t Status() const { return mInternalResponse->GetStatus(); } @@ -101,7 +99,7 @@ class Response final : public FetchBody<Response>, public nsWrapperCache { static already_AddRefed<Response> Error(const GlobalObject& aGlobal); static already_AddRefed<Response> Redirect(const GlobalObject& aGlobal, - const nsAString& aUrl, + const nsACString& aUrl, uint16_t aStatus, ErrorResult& aRv); diff --git a/dom/fetch/tests/browser.toml b/dom/fetch/tests/browser.toml index e449f9572f..f0788db856 100644 --- a/dom/fetch/tests/browser.toml +++ b/dom/fetch/tests/browser.toml @@ -2,6 +2,11 @@ ["browser_blobFromFile.js"] +["browser_default_credentialless_fetch.js"] +support-files = [ + "store_header.sjs", +] + ["browser_origin_trial_coep_credentialless_cache.js"] support-files = [ "open_credentialless_document.sjs", diff --git a/dom/fetch/tests/browser_default_credentialless_fetch.js b/dom/fetch/tests/browser_default_credentialless_fetch.js new file mode 100644 index 0000000000..1c7e820d5f --- /dev/null +++ b/dom/fetch/tests/browser_default_credentialless_fetch.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const RESOURCE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "store_header.sjs"; + +add_task(async function test_fetch_defaults_to_credentialless() { + // Ensure cookie is set up: + let expiry = Date.now() / 1000 + 24 * 60 * 60; + Services.cookies.add( + "example.com", + "/", + "foo", + "bar", + false, + false, + false, + expiry, + {}, + Ci.nsICookie.SAMESITE_NONE, + Ci.nsICookie.SCHEME_HTTPS + ); + + // Explicitly send cookie header by using `same-origin` in the init dict, to + // ensure cookies are stored correctly and can be sent. + await fetch(RESOURCE_URL + "?checkheader", { credentials: "same-origin" }); + + Assert.equal( + await fetch(RESOURCE_URL + "?getstate").then(r => r.text()), + "hasCookie", + "Should have cookie when explicitly passing credentials info in 'checkheader' request." + ); + + // Check the default behaviour. + await fetch(RESOURCE_URL + "?checkheader"); + Assert.equal( + await fetch(RESOURCE_URL + "?getstate").then(r => r.text()), + "noCookie", + "Should not have cookie in the default case (no explicit credentials mode) for chrome privileged requests." + ); +}); diff --git a/dom/file/ipc/RemoteLazyInputStreamThread.cpp b/dom/file/ipc/RemoteLazyInputStreamThread.cpp index 5883c44d4c..67f670ac4a 100644 --- a/dom/file/ipc/RemoteLazyInputStreamThread.cpp +++ b/dom/file/ipc/RemoteLazyInputStreamThread.cpp @@ -151,7 +151,7 @@ RemoteLazyInputStreamThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable, if (RLISThreadIsInOrBeyondShutdown()) { // nsIEventTarget::Dispatch must leak the runnable if the dispatch fails. - (void)runnable.forget(); + Unused << runnable.forget(); return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; } diff --git a/dom/file/uri/BlobURLProtocolHandler.cpp b/dom/file/uri/BlobURLProtocolHandler.cpp index 978e88cc8b..9c27ece1d1 100644 --- a/dom/file/uri/BlobURLProtocolHandler.cpp +++ b/dom/file/uri/BlobURLProtocolHandler.cpp @@ -16,7 +16,6 @@ #include "mozilla/dom/IPCBlobUtils.h" #include "mozilla/dom/MediaSource.h" #include "mozilla/ipc/IPCStreamUtils.h" -#include "mozilla/glean/GleanMetrics.h" #include "mozilla/AppShutdown.h" #include "mozilla/BasePrincipal.h" #include "mozilla/LoadInfo.h" @@ -777,7 +776,6 @@ bool BlobURLProtocolHandler::GetDataEntry( if (StaticPrefs::privacy_partition_bloburl_per_partition_key() && !aPartitionKey.IsEmpty() && !info->mPartitionKey.IsEmpty() && !aPartitionKey.Equals(info->mPartitionKey)) { - mozilla::glean::bloburl::resolve_stopped.Add(); nsAutoString localizedMsg; AutoTArray<nsString, 1> param; CopyUTF8toUTF16(aUri, *param.AppendElement()); diff --git a/dom/fs/api/FileSystemManager.cpp b/dom/fs/api/FileSystemManager.cpp index 099739f8a1..5bcc49c615 100644 --- a/dom/fs/api/FileSystemManager.cpp +++ b/dom/fs/api/FileSystemManager.cpp @@ -100,7 +100,7 @@ void FileSystemManager::BeginRequest( MOZ_ASSERT(mGlobal); // Check if we're allowed to use storage - if (mGlobal->GetStorageAccess() < StorageAccess::eSessionScoped) { + if (mGlobal->GetStorageAccess() <= StorageAccess::ePrivateBrowsing) { aFailure(NS_ERROR_DOM_SECURITY_ERR); return; } diff --git a/dom/fs/api/FileSystemWritableFileStream.cpp b/dom/fs/api/FileSystemWritableFileStream.cpp index ab133b2707..8e0abe3ee6 100644 --- a/dom/fs/api/FileSystemWritableFileStream.cpp +++ b/dom/fs/api/FileSystemWritableFileStream.cpp @@ -86,7 +86,7 @@ class WritableFileStreamUnderlyingSinkAlgorithms final FileSystemWritableFileStream& aStream) : mStream(&aStream) {} - already_AddRefed<Promise> WriteCallback( + already_AddRefed<Promise> WriteCallbackImpl( JSContext* aCx, JS::Handle<JS::Value> aChunk, WritableStreamDefaultController& aController, ErrorResult& aRv) override; @@ -180,8 +180,10 @@ class FileSystemWritableFileStream::CloseHandler { * @brief Transition from initial to open state. In initial state * */ - void Open() { + void Open(std::function<void()>&& aCallback) { MOZ_ASSERT(State::Initial == mState); + + mShutdownBlocker->SetCallback(std::move(aCallback)); mShutdownBlocker->Block(); mState = State::Open; @@ -193,6 +195,7 @@ class FileSystemWritableFileStream::CloseHandler { */ void Close() { mShutdownBlocker->Unblock(); + mShutdownBlocker = nullptr; mState = State::Closed; mClosePromiseHolder.ResolveIfExists(true, __func__); } @@ -329,7 +332,17 @@ FileSystemWritableFileStream::Create( autoClose.release(); stream->mWorkerRef = std::move(workerRef); - stream->mCloseHandler->Open(); + + // The close handler passes this callback to `FileSystemShutdownBlocker` + // which has separate handling for debug and release builds. Basically, + // there's no content process shutdown blocking in release builds, so the + // callback might be executed in debug builds only. + stream->mCloseHandler->Open([stream]() { + if (stream->IsOpen()) { + // We don't need the promise, we just begin the closing process. + Unused << stream->BeginAbort(); + } + }); // Step 9: Return stream. return stream; @@ -479,23 +492,27 @@ already_AddRefed<Promise> FileSystemWritableFileStream::Write( ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams data; if (!data.Init(aCx, aChunk)) { aError.StealExceptionFromJSContext(aCx); + // XXX(krosylight): The Streams spec does not provide a way to catch errors + // thrown from the write algorithm, and the File System spec does not try + // catching them either. This is unfortunate, as per the spec the type error + // from write() must immediately return a rejected promise without any file + // handle closure. For now we handle it here manually. See also: + // - https://github.com/whatwg/streams/issues/636 + // - https://github.com/whatwg/fs/issues/153 + if (IsOpen()) { + (void)BeginAbort(); + } return nullptr; } // Step 2. Let p be a new promise. - RefPtr<Promise> promise = Promise::Create(GetParentObject(), aError); - if (aError.Failed()) { - return nullptr; - } - - RefPtr<Promise> innerPromise = Promise::Create(GetParentObject(), aError); - if (aError.Failed()) { - return nullptr; - } + RefPtr<Promise> promise = Promise::CreateInfallible(GetParentObject()); RefPtr<Command> command = CreateCommand(); // Step 3.3. + // XXX: This should ideally be also handled by the streams but we don't + // currently have the hook. See https://github.com/whatwg/streams/issues/620. Write(data)->Then( GetCurrentSerialEventTarget(), __func__, [self = RefPtr{this}, command, @@ -961,7 +978,7 @@ NS_IMPL_CYCLE_COLLECTION_INHERITED(WritableFileStreamUnderlyingSinkAlgorithms, // Step 3 of // https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream already_AddRefed<Promise> -WritableFileStreamUnderlyingSinkAlgorithms::WriteCallback( +WritableFileStreamUnderlyingSinkAlgorithms::WriteCallbackImpl( JSContext* aCx, JS::Handle<JS::Value> aChunk, WritableStreamDefaultController& aController, ErrorResult& aRv) { return mStream->Write(aCx, aChunk, aRv); diff --git a/dom/fs/child/FileSystemShutdownBlocker.cpp b/dom/fs/child/FileSystemShutdownBlocker.cpp index bddbf18c6b..e6301e52ea 100644 --- a/dom/fs/child/FileSystemShutdownBlocker.cpp +++ b/dom/fs/child/FileSystemShutdownBlocker.cpp @@ -36,6 +36,8 @@ class FileSystemWritableBlocker : public FileSystemShutdownBlocker { public: FileSystemWritableBlocker() : mName(CreateBlockerName()) {} + void SetCallback(std::function<void()>&& aCallback) override; + NS_DECL_ISUPPORTS_INHERITED NS_DECL_NSIASYNCSHUTDOWNBLOCKER @@ -50,8 +52,13 @@ class FileSystemWritableBlocker : public FileSystemShutdownBlocker { private: const nsString mName; + std::function<void()> mCallback; }; +void FileSystemWritableBlocker::SetCallback(std::function<void()>&& aCallback) { + mCallback = std::move(aCallback); +} + NS_IMPL_ISUPPORTS_INHERITED(FileSystemWritableBlocker, FileSystemShutdownBlocker, nsIAsyncShutdownBlocker) @@ -112,11 +119,18 @@ FileSystemWritableBlocker::BlockShutdown( nsIAsyncShutdownClient* /* aBarrier */) { MOZ_ASSERT(NS_IsMainThread()); + if (mCallback) { + auto callback = std::move(mCallback); + callback(); + } + return NS_OK; } class FileSystemNullBlocker : public FileSystemShutdownBlocker { public: + void SetCallback(std::function<void()>&& aCallback) override {} + NS_IMETHODIMP Block() override { return NS_OK; } NS_IMETHODIMP Unblock() override { return NS_OK; } diff --git a/dom/fs/include/fs/FileSystemShutdownBlocker.h b/dom/fs/include/fs/FileSystemShutdownBlocker.h index f11d263f4e..4932e2889e 100644 --- a/dom/fs/include/fs/FileSystemShutdownBlocker.h +++ b/dom/fs/include/fs/FileSystemShutdownBlocker.h @@ -17,6 +17,8 @@ class FileSystemShutdownBlocker : public nsIAsyncShutdownBlocker { public: static already_AddRefed<FileSystemShutdownBlocker> CreateForWritable(); + virtual void SetCallback(std::function<void()>&& aCallback) = 0; + NS_DECL_ISUPPORTS NS_DECL_NSIASYNCSHUTDOWNBLOCKER diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp index c65bf01508..a898f1afb6 100644 --- a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.cpp @@ -1233,7 +1233,7 @@ void FileSystemDatabaseManagerVersion001::DecreaseCachedQuotaUsage( } nsresult FileSystemDatabaseManagerVersion001::UpdateCachedQuotaUsage( - const FileId& aFileId, Usage aOldUsage, Usage aNewUsage) { + const FileId& aFileId, Usage aOldUsage, Usage aNewUsage) const { quota::QuotaManager* quotaManager = quota::QuotaManager::Get(); MOZ_ASSERT(quotaManager); diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h index 333c5af6c2..64ecfa5b01 100644 --- a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion001.h @@ -130,7 +130,7 @@ class FileSystemDatabaseManagerVersion001 : public FileSystemDatabaseManager { void DecreaseCachedQuotaUsage(int64_t aDelta); nsresult UpdateCachedQuotaUsage(const FileId& aFileId, Usage aOldUsage, - Usage aNewUsage); + Usage aNewUsage) const; nsresult ClearDestinationIfNotLocked( const FileSystemConnection& aConnection, diff --git a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp index 3543346ff0..a6c61b0e3e 100644 --- a/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp +++ b/dom/fs/parent/datamodel/FileSystemDatabaseManagerVersion002.cpp @@ -463,6 +463,9 @@ nsresult FileSystemDatabaseManagerVersion002::GetFile( if (mainFileId) { QM_TRY_UNWRAP(aFile, mFileManager->CreateFileFrom(aFileId, mainFileId.value())); + int64_t fileSize = 0; + QM_TRY(QM_TO_RESULT(aFile->GetFileSize(&fileSize))); + UpdateCachedQuotaUsage(aFileId, 0, fileSize); } else { // LockShared/EnsureTemporaryFileId has provided a brand new fileId. QM_TRY_UNWRAP(aFile, mFileManager->GetOrCreateFile(aFileId)); diff --git a/dom/fs/test/common/test_writableFileStream.js b/dom/fs/test/common/test_writableFileStream.js index 016c53bf3b..5b6caa0d20 100644 --- a/dom/fs/test/common/test_writableFileStream.js +++ b/dom/fs/test/common/test_writableFileStream.js @@ -2,6 +2,7 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ const allowCreate = { create: true }; +const denyCreate = { create: false }; exported_symbols.test0 = async function () { let root = await navigator.storage.getDirectory(); @@ -145,6 +146,111 @@ exported_symbols.bug1825018 = async function () { } }; +exported_symbols.usageTest = async function () { + const bufferSize = 1024; + const keepData = { keepExistingData: true }; + const fromEmpty = { keepExistingData: false }; + + let root = await navigator.storage.getDirectory(); + Assert.ok(root, "Can we access the root directory?"); + + const baseUsage = await Utils.getCachedOriginUsage(); + Assert.ok(true, "Usage " + baseUsage); + // Create a file. + { + const fileHandle = await root.getFileHandle("usagetest.txt", allowCreate); + Assert.ok(!!fileHandle, "Can we get file handle?"); + + const writable = await fileHandle.createWritable(fromEmpty); + Assert.ok(!!writable, "Can we create writable file stream?"); + + const buffer = new ArrayBuffer(bufferSize); + Assert.ok(!!buffer, "Can we create array buffer?"); + + const result = await writable.write(buffer); + Assert.equal(result, undefined, "Can we write entire buffer?"); + + await writable.close(); + } + + { + const fileUsage = await Utils.getCachedOriginUsage(); + Assert.ok(true, "Usage " + fileUsage); + Assert.ok(fileUsage >= baseUsage + bufferSize); + + const fileHandle = await root.getFileHandle("usagetest.txt", denyCreate); + Assert.ok(!!fileHandle, "Can we get file handle?"); + + { + const usageNow = await Utils.getCachedOriginUsage(); + Assert.equal(usageNow, fileUsage); + } + + const writableA = await fileHandle.createWritable(keepData); + Assert.ok(!!writableA, "Can we create writable file stream?"); + + { + const usageNow = await Utils.getCachedOriginUsage(); + Assert.ok(true, "Usage " + usageNow.usage); + Assert.equal(usageNow, fileUsage + bufferSize); + } + + const writableB = await fileHandle.createWritable(keepData); + Assert.ok(!!writableB, "Can we create writable file stream?"); + + { + const usageNow = await Utils.getCachedOriginUsage(); + Assert.equal(usageNow, fileUsage + 2 * bufferSize); + } + + const writableC = await fileHandle.createWritable(keepData); + Assert.ok(!!writableC, "Can we create writable file stream?"); + + { + const usageNow = await Utils.getCachedOriginUsage(); + Assert.equal(usageNow, fileUsage + 3 * bufferSize); + } + + const writableD = await fileHandle.createWritable(fromEmpty); + Assert.ok(!!writableD, "Can we create writable file stream?"); + + { + const usageNow = await Utils.getCachedOriginUsage(); + // We did not keep existing data for this writable + Assert.equal(usageNow, fileUsage + 3 * bufferSize); + } + + await writableA.abort(); + + { + const usageNow = await Utils.getCachedOriginUsage(); + Assert.equal(usageNow, fileUsage + 2 * bufferSize); + } + + await writableB.close(); + + { + const usageNow = await Utils.getCachedOriginUsage(); + Assert.equal(usageNow, fileUsage + bufferSize); + } + + await writableC.abort(); + + { + const usageNow = await Utils.getCachedOriginUsage(); + Assert.equal(usageNow, fileUsage); + } + + await writableD.close(); + + { + const usageNow = await Utils.getCachedOriginUsage(); + // Buffer was overwritten with nothing. + Assert.equal(usageNow, fileUsage - bufferSize); + } + } +}; + for (const [key, value] of Object.entries(exported_symbols)) { Object.defineProperty(value, "name", { value: key, diff --git a/dom/fs/test/crashtests/1874334-2.html b/dom/fs/test/crashtests/1874334-2.html new file mode 100644 index 0000000000..3d30d7496f --- /dev/null +++ b/dom/fs/test/crashtests/1874334-2.html @@ -0,0 +1,8 @@ +<script> +document.addEventListener("DOMContentLoaded", async () => { + await self.navigator.serviceWorker.register("sw1874334-2.js?318", { }) + await self.navigator.serviceWorker.register("sw1874334-2.js?363", { }) + await self.navigator.serviceWorker.register("sw1874334-2.js?87", { }) + await self.fetch("missing", {"headers": []}) +}) +</script> diff --git a/dom/fs/test/crashtests/1874334.html b/dom/fs/test/crashtests/1874334.html new file mode 100644 index 0000000000..590025a60f --- /dev/null +++ b/dom/fs/test/crashtests/1874334.html @@ -0,0 +1,16 @@ +<script> + +async function stuff() { + let arr = new ArrayBuffer(500000); + let blob = new Blob([arr]); + let file = new File([blob], "", { }); + + let dir = await self.navigator.storage.getDirectory(); + let handle = await dir.getFileHandle("514600c6-596b-4676-ab0c-3e6f1e86759f", {"create": true}); + let wfs = await handle.createWritable({"keepExistingData": true}); + + await file.stream().pipeTo(wfs); +}; + +document.addEventListener("DOMContentLoaded", stuff); +</script> diff --git a/dom/fs/test/crashtests/crashtests.list b/dom/fs/test/crashtests/crashtests.list index a083cb80b2..c5130c4232 100644 --- a/dom/fs/test/crashtests/crashtests.list +++ b/dom/fs/test/crashtests/crashtests.list @@ -1,6 +1,7 @@ # StorageManager isn't enabled on Android defaults skip-if(Android) pref(dom.fs.enabled,true) pref(dom.fs.writable_file_stream.enabled,true) +# Most of the issues here are reproducible only with --verify load 1798773.html load 1800470.html load 1809759.html @@ -8,3 +9,5 @@ load 1816710.html load 1841702.html HTTP load 1844619.html HTTP load 1858820.html +load 1874334.html +HTTP load 1874334-2.html diff --git a/dom/fs/test/crashtests/sw1874334-2.js b/dom/fs/test/crashtests/sw1874334-2.js new file mode 100644 index 0000000000..59d82ebaaa --- /dev/null +++ b/dom/fs/test/crashtests/sw1874334-2.js @@ -0,0 +1,12 @@ +(async () => { + let arr = new ArrayBuffer(43109) + let blob = new Blob([arr, arr, arr, arr, arr]) + let req = new Request("missing", {"headers": []}) + let dir = await self.navigator.storage.getDirectory() + let file = new File([blob, arr], "", { }) + let handle = await dir.getFileHandle("514600c6-596b-4676-ab0c-3e6f1e86759f", {"create": true}) + let wfs = await handle.createWritable({"keepExistingData": true}) + await handle.createWritable({"keepExistingData": true}) + try { await req.json(arr, {"headers": []}) } catch (e) {} + try { await file.stream().pipeTo(wfs, { }) } catch (e) {} +})() diff --git a/dom/html/CustomStateSet.cpp b/dom/html/CustomStateSet.cpp index 4e24551f5e..bb3621b195 100644 --- a/dom/html/CustomStateSet.cpp +++ b/dom/html/CustomStateSet.cpp @@ -38,32 +38,39 @@ void CustomStateSet::Clear(ErrorResult& aRv) { return; } - mTarget->EnsureCustomStates().Clear(); - InvalidateStyleFromCustomStateSetChange(); -} - -void CustomStateSet::InvalidateStyleFromCustomStateSetChange() const { - Document* doc = mTarget->OwnerDoc(); - - PresShell* presShell = doc->GetPresShell(); - if (!presShell) { + nsTArray<RefPtr<nsAtom>>& states = mTarget->EnsureCustomStates(); + Document* doc = mTarget->GetComposedDoc(); + PresShell* presShell = doc ? doc->GetPresShell() : nullptr; + if (presShell) { + presShell->CustomStatesWillChange(*mTarget); + // Iterate over each state to ensure each one is invalidated. + while (!states.IsEmpty()) { + RefPtr<nsAtom> atom = states.PopLastElement(); + presShell->CustomStateChanged(*mTarget, atom); + } return; } - // TODO: make this more efficient? - presShell->DestroyFramesForAndRestyle(mTarget); + states.Clear(); } bool CustomStateSet::Delete(const nsAString& aState, ErrorResult& aRv) { - if (!CustomStateSet_Binding::SetlikeHelpers::Delete(this, aState, aRv) || - aRv.Failed()) { + CustomStateSet_Binding::SetlikeHelpers::Delete(this, aState, aRv); + if (aRv.Failed()) { return false; } RefPtr<nsAtom> atom = NS_AtomizeMainThread(aState); + Document* doc = mTarget->GetComposedDoc(); + PresShell* presShell = doc ? doc->GetPresShell() : nullptr; + if (presShell) { + presShell->CustomStatesWillChange(*mTarget); + } + bool deleted = mTarget->EnsureCustomStates().RemoveElement(atom); - if (deleted) { - InvalidateStyleFromCustomStateSetChange(); + + if (presShell) { + presShell->CustomStateChanged(*mTarget, atom); } return deleted; } @@ -74,9 +81,23 @@ void CustomStateSet::Add(const nsAString& aState, ErrorResult& aRv) { return; } + nsTArray<RefPtr<nsAtom>>& states = mTarget->EnsureCustomStates(); RefPtr<nsAtom> atom = NS_AtomizeMainThread(aState); - mTarget->EnsureCustomStates().AppendElement(atom); - InvalidateStyleFromCustomStateSetChange(); + if (states.Contains(atom)) { + return; + } + + Document* doc = mTarget->GetComposedDoc(); + PresShell* presShell = doc ? doc->GetPresShell() : nullptr; + if (presShell) { + presShell->CustomStatesWillChange(*mTarget); + } + + states.AppendElement(atom); + + if (presShell) { + presShell->CustomStateChanged(*mTarget, atom); + } } } // namespace mozilla::dom diff --git a/dom/html/CustomStateSet.h b/dom/html/CustomStateSet.h index 095974b5f4..a28a22011e 100644 --- a/dom/html/CustomStateSet.h +++ b/dom/html/CustomStateSet.h @@ -35,8 +35,6 @@ class CustomStateSet final : public nsISupports, public nsWrapperCache { static MOZ_CAN_RUN_SCRIPT_BOUNDARY already_AddRefed<CustomStateSet> Constructor(const GlobalObject& aGlobal, ErrorResult& aRv); - void InvalidateStyleFromCustomStateSetChange() const; - MOZ_CAN_RUN_SCRIPT void Clear(ErrorResult& aRv); /** diff --git a/dom/html/HTMLAnchorElement.h b/dom/html/HTMLAnchorElement.h index 4d89c6a75b..88669549e3 100644 --- a/dom/html/HTMLAnchorElement.h +++ b/dom/html/HTMLAnchorElement.h @@ -68,11 +68,11 @@ class HTMLAnchorElement final : public nsGenericHTMLElement, // WebIDL API - void GetHref(nsAString& aValue) const { + void GetHref(nsACString& aValue) const { GetURIAttr(nsGkAtoms::href, nullptr, aValue); } - void SetHref(const nsAString& aValue, mozilla::ErrorResult& rv) { - SetHTMLAttr(nsGkAtoms::href, aValue, rv); + void SetHref(const nsACString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::href, NS_ConvertUTF8toUTF16(aValue), aRv); } void GetTarget(nsAString& aValue) const; void SetTarget(const nsAString& aValue, mozilla::ErrorResult& rv) { @@ -180,8 +180,6 @@ class HTMLAnchorElement final : public nsGenericHTMLElement, void SetShape(const nsAString& aValue, mozilla::ErrorResult& rv) { SetHTMLAttr(nsGkAtoms::shape, aValue, rv); } - void Stringify(nsAString& aResult) const { GetHref(aResult); } - void ToString(nsAString& aSource) const { GetHref(aSource); } void NodeInfoChanged(Document* aOldDoc) final { ClearHasPendingLinkUpdate(); diff --git a/dom/html/HTMLAreaElement.cpp b/dom/html/HTMLAreaElement.cpp index e07150cc06..fc47069ead 100644 --- a/dom/html/HTMLAreaElement.cpp +++ b/dom/html/HTMLAreaElement.cpp @@ -92,8 +92,6 @@ void HTMLAreaElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); } -void HTMLAreaElement::ToString(nsAString& aSource) { GetHref(aSource); } - already_AddRefed<nsIURI> HTMLAreaElement::GetHrefURI() const { if (nsCOMPtr<nsIURI> uri = GetCachedURI()) { return uri.forget(); diff --git a/dom/html/HTMLAreaElement.h b/dom/html/HTMLAreaElement.h index 3effb1a87e..6e9744ae62 100644 --- a/dom/html/HTMLAreaElement.h +++ b/dom/html/HTMLAreaElement.h @@ -64,12 +64,11 @@ class HTMLAreaElement final : public nsGenericHTMLElement, public Link { SetHTMLAttr(nsGkAtoms::shape, aShape, aError); } - // argument type nsAString for nsContextMenuInfo - void GetHref(nsAString& aValue) { + void GetHref(nsACString& aValue) { GetURIAttr(nsGkAtoms::href, nullptr, aValue); } - void SetHref(const nsAString& aHref, ErrorResult& aError) { - SetHTMLAttr(nsGkAtoms::href, aHref, aError); + void SetHref(const nsACString& aHref, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::href, NS_ConvertUTF8toUTF16(aHref), aError); } void GetTarget(DOMString& aValue); @@ -85,13 +84,11 @@ class HTMLAreaElement final : public nsGenericHTMLElement, public Link { } void GetPing(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::ping, aValue); } - void SetPing(const nsAString& aPing, ErrorResult& aError) { SetHTMLAttr(nsGkAtoms::ping, aPing, aError); } void GetRel(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::rel, aValue); } - void SetRel(const nsAString& aRel, ErrorResult& aError) { SetHTMLAttr(nsGkAtoms::rel, aRel, aError); } @@ -141,9 +138,6 @@ class HTMLAreaElement final : public nsGenericHTMLElement, public Link { SetHTMLBoolAttr(nsGkAtoms::nohref, aValue, aError); } - void ToString(nsAString& aSource); - void Stringify(nsAString& aResult) { GetHref(aResult); } - void NodeInfoChanged(Document* aOldDoc) final { ClearHasPendingLinkUpdate(); nsGenericHTMLElement::NodeInfoChanged(aOldDoc); diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp index d5a65a1755..958601616b 100644 --- a/dom/html/HTMLInputElement.cpp +++ b/dom/html/HTMLInputElement.cpp @@ -5921,7 +5921,7 @@ nsresult HTMLInputElement::SetDefaultValueAsValue() { void HTMLInputElement::SetAutoDirectionality(bool aNotify, const nsAString* aKnownValue) { if (!IsAutoDirectionalityAssociated()) { - return SetDirectionality(GetParentDirectionality(this), aNotify); + return SetDirectionality(Directionality::Ltr, aNotify); } nsAutoString value; if (!aKnownValue) { diff --git a/dom/html/HTMLMarqueeElement.cpp b/dom/html/HTMLMarqueeElement.cpp index 9719f83ea3..8721086064 100644 --- a/dom/html/HTMLMarqueeElement.cpp +++ b/dom/html/HTMLMarqueeElement.cpp @@ -34,11 +34,6 @@ static const nsAttrValue::EnumTable kDirectionTable[] = { // Default direction value is "left". static const nsAttrValue::EnumTable* kDefaultDirection = &kDirectionTable[0]; -bool HTMLMarqueeElement::IsEventAttributeNameInternal(nsAtom* aName) { - return nsContentUtils::IsEventAttributeName( - aName, EventNameType_HTML | EventNameType_HTMLMarqueeOnly); -} - JSObject* HTMLMarqueeElement::WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { return dom::HTMLMarqueeElement_Binding::Wrap(aCx, this, aGivenProto); @@ -112,19 +107,6 @@ bool HTMLMarqueeElement::ParseAttribute(int32_t aNamespaceID, aMaybeScriptedPrincipal, aResult); } -void HTMLMarqueeElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, - const nsAttrValue* aValue, - const nsAttrValue* aOldValue, - nsIPrincipal* aMaybeScriptedPrincipal, - bool aNotify) { - if (IsInComposedDoc() && aNameSpaceID == kNameSpaceID_None && - aName == nsGkAtoms::direction) { - NotifyUAWidgetSetupOrChange(); - } - return nsGenericHTMLElement::AfterSetAttr( - aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); -} - void HTMLMarqueeElement::MapAttributesIntoRule( MappedDeclarationsBuilder& aBuilder) { nsGenericHTMLElement::MapImageMarginAttributeInto(aBuilder); diff --git a/dom/html/HTMLMarqueeElement.h b/dom/html/HTMLMarqueeElement.h index f93f6b4944..2abebdcd9d 100644 --- a/dom/html/HTMLMarqueeElement.h +++ b/dom/html/HTMLMarqueeElement.h @@ -25,8 +25,6 @@ class HTMLMarqueeElement final : public nsGenericHTMLElement { static const int kDefaultScrollAmount = 6; static const int kDefaultScrollDelayMS = 85; - bool IsEventAttributeNameInternal(nsAtom* aName) override; - void GetBehavior(nsAString& aValue); void SetBehavior(const nsAString& aValue, ErrorResult& aError) { SetHTMLAttr(nsGkAtoms::behavior, aValue, aError); @@ -104,10 +102,6 @@ class HTMLMarqueeElement final : public nsGenericHTMLElement { const nsAString& aValue, nsIPrincipal* aMaybeScriptedPrincipal, nsAttrValue& aResult) override; - void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, - const nsAttrValue* aValue, const nsAttrValue* aOldValue, - nsIPrincipal* aMaybeScriptedPrincipal, - bool aNotify) override; NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; diff --git a/dom/html/HTMLMediaElement.cpp b/dom/html/HTMLMediaElement.cpp index 4ea52ccdf6..877f3ec3ba 100644 --- a/dom/html/HTMLMediaElement.cpp +++ b/dom/html/HTMLMediaElement.cpp @@ -454,7 +454,7 @@ class HTMLMediaElement::MediaControlKeyListener final void HandleMediaKey(MediaControlKey aKey) override { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(IsStarted()); - MEDIACONTROL_LOG("HandleEvent '%s'", ToMediaControlKeyStr(aKey)); + MEDIACONTROL_LOG("HandleEvent '%s'", GetEnumString(aKey).get()); if (aKey == MediaControlKey::Play) { Owner()->Play(); } else if (aKey == MediaControlKey::Pause) { @@ -565,6 +565,10 @@ class HTMLMediaElement::MediaStreamTrackListener explicit MediaStreamTrackListener(HTMLMediaElement* aElement) : mElement(aElement) {} + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaStreamTrackListener, + DOMMediaStream::TrackListener) + void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override { if (!mElement) { return; @@ -678,16 +682,26 @@ class HTMLMediaElement::MediaStreamTrackListener } protected: - const WeakPtr<HTMLMediaElement> mElement; + virtual ~MediaStreamTrackListener() = default; + RefPtr<HTMLMediaElement> mElement; }; +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLMediaElement::MediaStreamTrackListener, + DOMMediaStream::TrackListener, mElement) +NS_IMPL_ADDREF_INHERITED(HTMLMediaElement::MediaStreamTrackListener, + DOMMediaStream::TrackListener) +NS_IMPL_RELEASE_INHERITED(HTMLMediaElement::MediaStreamTrackListener, + DOMMediaStream::TrackListener) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + HTMLMediaElement::MediaStreamTrackListener) +NS_INTERFACE_MAP_END_INHERITING(DOMMediaStream::TrackListener) + /** * Helper class that manages audio and video outputs for all enabled tracks in a * media element. It also manages calculating the current time when playing a * MediaStream. */ -class HTMLMediaElement::MediaStreamRenderer - : public DOMMediaStream::TrackListener { +class HTMLMediaElement::MediaStreamRenderer { public: NS_INLINE_DECL_REFCOUNTING(MediaStreamRenderer) @@ -1973,6 +1987,7 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLMediaElement) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLMediaElement, nsGenericHTMLElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStreamWindowCapturer) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaSource) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSrcMediaSource) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSrcStream) @@ -1988,6 +2003,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLMediaElement, NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextTrackManager) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioTrackList) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVideoTrackList) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaStreamTrackListener) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaKeys) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIncomingMediaKeys) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectedVideoStreamTrack) @@ -2022,6 +2038,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLMediaElement, tmp->mMediaStreamTrackListener.get()); } } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mStreamWindowCapturer) NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcStream) NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcAttrStream) NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaSource) @@ -2040,6 +2057,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLMediaElement, NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextTrackManager) NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioTrackList) NS_IMPL_CYCLE_COLLECTION_UNLINK(mVideoTrackList) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaStreamTrackListener) NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaKeys) NS_IMPL_CYCLE_COLLECTION_UNLINK(mIncomingMediaKeys) NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectedVideoStreamTrack) @@ -5270,7 +5288,7 @@ void HTMLMediaElement::SetupSrcMediaStreamPlayback(DOMMediaStream* aStream) { NotifyMediaStreamTrackAdded(track); } - mMediaStreamTrackListener = MakeUnique<MediaStreamTrackListener>(this); + mMediaStreamTrackListener = new MediaStreamTrackListener(this); mSrcStream->RegisterTrackListener(mMediaStreamTrackListener.get()); ChangeNetworkState(NETWORK_IDLE); @@ -7347,7 +7365,9 @@ void HTMLMediaElement::AudioCaptureTrackChange(bool aCapture) { CaptureStreamInternal(StreamCaptureBehavior::CONTINUE_WHEN_ENDED, StreamCaptureType::CAPTURE_AUDIO, mtg); mStreamWindowCapturer = - MakeUnique<MediaStreamWindowCapturer>(stream, window->WindowID()); + new MediaStreamWindowCapturer(stream, window->WindowID()); + mStreamWindowCapturer->mStream->RegisterTrackListener( + mStreamWindowCapturer); } else if (!aCapture && mStreamWindowCapturer) { for (size_t i = 0; i < mOutputStreams.Length(); i++) { if (mOutputStreams[i].mStream == mStreamWindowCapturer->mStream) { @@ -7361,6 +7381,9 @@ void HTMLMediaElement::AudioCaptureTrackChange(bool aCapture) { break; } } + + mStreamWindowCapturer->mStream->UnregisterTrackListener( + mStreamWindowCapturer); mStreamWindowCapturer = nullptr; if (mOutputStreams.IsEmpty()) { mTracksCaptured = nullptr; diff --git a/dom/html/HTMLMediaElement.h b/dom/html/HTMLMediaElement.h index eef276b43d..6c159a7971 100644 --- a/dom/html/HTMLMediaElement.h +++ b/dom/html/HTMLMediaElement.h @@ -1455,7 +1455,7 @@ class HTMLMediaElement : public nsGenericHTMLElement, // Holds a reference to the stream connecting this stream to the window // capture sink. - UniquePtr<MediaStreamWindowCapturer> mStreamWindowCapturer; + RefPtr<MediaStreamWindowCapturer> mStreamWindowCapturer; // Holds references to the DOM wrappers for the MediaStreams that we're // writing to. @@ -1770,7 +1770,7 @@ class HTMLMediaElement : public nsGenericHTMLElement, RefPtr<VideoTrackList> mVideoTrackList; - UniquePtr<MediaStreamTrackListener> mMediaStreamTrackListener; + RefPtr<MediaStreamTrackListener> mMediaStreamTrackListener; // The principal guarding mVideoFrameContainer access when playing a // MediaStream. diff --git a/dom/html/TextControlState.cpp b/dom/html/TextControlState.cpp index 11f7619699..cbc10452ca 100644 --- a/dom/html/TextControlState.cpp +++ b/dom/html/TextControlState.cpp @@ -2877,8 +2877,7 @@ bool TextControlState::SetValueWithoutTextEditor( // We can't just early-return here, because OnValueChanged below still need to // be called. - if (!mValue.Equals(aHandlingSetValue.GetSettingValue()) || - !StaticPrefs::dom_input_skip_cursor_move_for_same_value_set()) { + if (!mValue.Equals(aHandlingSetValue.GetSettingValue())) { bool handleSettingValue = true; // If `SetValue()` call is nested, `GetSettingValue()` result will be // modified. So, we need to store input event data value before diff --git a/dom/html/nsGenericHTMLElement.cpp b/dom/html/nsGenericHTMLElement.cpp index 1014ba2a25..1deaf719d3 100644 --- a/dom/html/nsGenericHTMLElement.cpp +++ b/dom/html/nsGenericHTMLElement.cpp @@ -646,7 +646,6 @@ already_AddRefed<nsIURI> nsGenericHTMLElement::GetHrefURIForAnchors() const { // We use the nsAttrValue's copy of the URI string to avoid copying. nsCOMPtr<nsIURI> uri; GetURIAttr(nsGkAtoms::href, nullptr, getter_AddRefs(uri)); - return uri.forget(); } @@ -1661,34 +1660,50 @@ uint32_t nsGenericHTMLElement::GetDimensionAttrAsUnsignedInt( void nsGenericHTMLElement::GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, nsAString& aResult) const { nsCOMPtr<nsIURI> uri; - bool hadAttr = GetURIAttr(aAttr, aBaseAttr, getter_AddRefs(uri)); - if (!hadAttr) { + const nsAttrValue* attr = GetURIAttr(aAttr, aBaseAttr, getter_AddRefs(uri)); + if (!attr) { aResult.Truncate(); return; } - if (!uri) { // Just return the attr value - GetAttr(aAttr, aResult); + attr->ToString(aResult); return; } - nsAutoCString spec; uri->GetSpec(spec); CopyUTF8toUTF16(spec, aResult); } -bool nsGenericHTMLElement::GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, - nsIURI** aURI) const { +void nsGenericHTMLElement::GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, + nsACString& aResult) const { + nsCOMPtr<nsIURI> uri; + const nsAttrValue* attr = GetURIAttr(aAttr, aBaseAttr, getter_AddRefs(uri)); + if (!attr) { + aResult.Truncate(); + return; + } + if (!uri) { + // Just return the attr value + nsAutoString value; + attr->ToString(value); + CopyUTF16toUTF8(value, aResult); + return; + } + uri->GetSpec(aResult); +} + +const nsAttrValue* nsGenericHTMLElement::GetURIAttr(nsAtom* aAttr, + nsAtom* aBaseAttr, + nsIURI** aURI) const { *aURI = nullptr; const nsAttrValue* attr = mAttrs.GetAttr(aAttr); if (!attr) { - return false; + return nullptr; } nsCOMPtr<nsIURI> baseURI = GetBaseURI(); - if (aBaseAttr) { nsAutoString baseAttrValue; if (GetAttr(aBaseAttr, baseAttrValue)) { @@ -1696,7 +1711,7 @@ bool nsGenericHTMLElement::GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, nsresult rv = nsContentUtils::NewURIWithDocumentCharset( getter_AddRefs(baseAttrURI), baseAttrValue, OwnerDoc(), baseURI); if (NS_FAILED(rv)) { - return true; + return attr; } baseURI.swap(baseAttrURI); } @@ -1706,7 +1721,7 @@ bool nsGenericHTMLElement::GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, // return true, and *aURI will be null. nsContentUtils::NewURIWithDocumentCharset(aURI, attr->GetStringValue(), OwnerDoc(), baseURI); - return true; + return attr; } bool nsGenericHTMLElement::IsLabelable() const { diff --git a/dom/html/nsGenericHTMLElement.h b/dom/html/nsGenericHTMLElement.h index 227e052fdf..86f87e8795 100644 --- a/dom/html/nsGenericHTMLElement.h +++ b/dom/html/nsGenericHTMLElement.h @@ -60,7 +60,6 @@ class nsGenericHTMLElement : public nsGenericHTMLElementBase { : nsGenericHTMLElementBase(std::move(aNodeInfo)) { NS_ASSERTION(mNodeInfo->NamespaceID() == kNameSpaceID_XHTML, "Unexpected namespace"); - AddStatesSilently(mozilla::dom::ElementState::LTR); } NS_INLINE_DECL_REFCOUNTING_INHERITED(nsGenericHTMLElement, @@ -641,6 +640,7 @@ class nsGenericHTMLElement : public nsGenericHTMLElementBase { * @param aResult result value [out] */ void GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, nsAString& aResult) const; + void GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, nsACString& aResult) const; /** * Gets the absolute URI values of an attribute, by resolving any relative @@ -648,7 +648,8 @@ class nsGenericHTMLElement : public nsGenericHTMLElementBase { * isn't a relative URI, the substring is returned as is. Only works for * attributes in null namespace. */ - bool GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, nsIURI** aURI) const; + const nsAttrValue* GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, + nsIURI** aURI) const; bool IsHidden() const { return HasAttr(nsGkAtoms::hidden); } @@ -767,6 +768,9 @@ class nsGenericHTMLElement : public nsGenericHTMLElementBase { void GetHTMLURIAttr(nsAtom* aName, nsAString& aResult) const { GetURIAttr(aName, nullptr, aResult); } + void GetHTMLURIAttr(nsAtom* aName, nsACString& aResult) const { + GetURIAttr(aName, nullptr, aResult); + } void SetHTMLAttr(nsAtom* aName, const nsAString& aValue) { SetAttr(kNameSpaceID_None, aName, aValue, true); diff --git a/dom/html/test/browser_ImageDocument_svg_zoom.js b/dom/html/test/browser_ImageDocument_svg_zoom.js index f0df2282a3..0bf0b98065 100644 --- a/dom/html/test/browser_ImageDocument_svg_zoom.js +++ b/dom/html/test/browser_ImageDocument_svg_zoom.js @@ -13,7 +13,7 @@ function test_once() { `${rect.width}x${rect.height}, ${content.innerWidth}x${content.innerHeight}` ); is( - Math.round(rect.height), + Math.trunc(rect.height), content.innerHeight, "Should fill the viewport and not overflow" ); diff --git a/dom/html/test/browser_bug436200.js b/dom/html/test/browser_bug436200.js index 7e739c02ad..ca11a3e994 100644 --- a/dom/html/test/browser_bug436200.js +++ b/dom/html/test/browser_bug436200.js @@ -8,12 +8,6 @@ const kTestPage = "https://example.org/browser/dom/html/test/bug436200.html"; async function run_test(shouldShowPrompt, msg) { let promptShown = false; - function tabModalObserver(subject) { - promptShown = true; - subject.querySelector(".tabmodalprompt-button0").click(); - } - Services.obs.addObserver(tabModalObserver, "tabmodal-dialog-loaded"); - function commonDialogObserver(subject) { let dialog = subject.Dialog; promptShown = true; @@ -27,7 +21,6 @@ async function run_test(shouldShowPrompt, msg) { let form = content.document.getElementById("test_form"); form.submit(); }); - Services.obs.removeObserver(tabModalObserver, "tabmodal-dialog-loaded"); Services.obs.removeObserver(commonDialogObserver, "common-dialog-loaded"); is(promptShown, shouldShowPrompt, msg); diff --git a/dom/html/test/forms/test_input_password_click_show_password_button.html b/dom/html/test/forms/test_input_password_click_show_password_button.html index 76f4e066f5..5a6b24e82c 100644 --- a/dom/html/test/forms/test_input_password_click_show_password_button.html +++ b/dom/html/test/forms/test_input_password_click_show_password_button.html @@ -37,16 +37,16 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=502258 element.type = "text"; await new Promise(resolve => requestAnimationFrame(resolve)); var typeTextSnapshot = await snapshotWindow(window); - results = compareSnapshots(baseSnapshot, typeTextSnapshot, true); - ok(results[0], aId + ": type=text should render the same as type=password that is showing the password"); + info(aId + ": type=text should render the same as type=password that is showing the password"); + assertSnapshots(baseSnapshot, typeTextSnapshot, true, null, aId + ": base", aId + ": type=text"); // Re-setting value shouldn't change anything. // eslint-disable-next-line no-self-assign element.value = element.value; var tmpSnapshot = await snapshotWindow(window); - results = compareSnapshots(baseSnapshot, tmpSnapshot, true); - ok(results[0], aId + ": re-setting the value should change nothing"); + info(aId + ": re-setting the value should change nothing"); + assertSnapshots(baseSnapshot, tmpSnapshot, true, null, aId + ": base", aId + ": tmp"); } async function reset_show_password(aId, concealedSnapshot) { @@ -54,8 +54,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=502258 element.type = "password"; await new Promise(resolve => requestAnimationFrame(resolve)); var typePasswordSnapshot = await snapshotWindow(window); - results = compareSnapshots(concealedSnapshot, typePasswordSnapshot, true); - ok(results[0], aId + ": changing the type attribute should conceal the password again"); + info(aId + ": changing the type attribute should conceal the password again"); + assertSnapshots(concealedSnapshot, typePasswordSnapshot, true, null, aId + ": concealed", aId + ": type=password"); } async function runTest() { diff --git a/dom/html/test/test_document-element-inserted.html b/dom/html/test/test_document-element-inserted.html index 6d7e8695ce..59228b393e 100644 --- a/dom/html/test/test_document-element-inserted.html +++ b/dom/html/test/test_document-element-inserted.html @@ -28,7 +28,7 @@ var observe = function(doc){ var media = document.getElementById('media'); var tests = [ - "../../../media/test/short-video.ogv", + "../../../media/test/vp9.webm", "../../../media/test/sound.ogg", "../../content/test/image.png" ] diff --git a/dom/indexedDB/ActorsChild.cpp b/dom/indexedDB/ActorsChild.cpp index a6f3ef7789..1a122ea3c1 100644 --- a/dom/indexedDB/ActorsChild.cpp +++ b/dom/indexedDB/ActorsChild.cpp @@ -1429,7 +1429,7 @@ mozilla::ipc::IPCResult BackgroundTransactionChild::RecvComplete( PBackgroundIDBRequestChild* BackgroundTransactionChild::AllocPBackgroundIDBRequestChild( - const RequestParams& aParams) { + const int64_t& aRequestId, const RequestParams& aParams) { MOZ_CRASH( "PBackgroundIDBRequestChild actors should be manually " "constructed!"); @@ -1445,7 +1445,7 @@ bool BackgroundTransactionChild::DeallocPBackgroundIDBRequestChild( PBackgroundIDBCursorChild* BackgroundTransactionChild::AllocPBackgroundIDBCursorChild( - const OpenCursorParams& aParams) { + const int64_t& aRequestId, const OpenCursorParams& aParams) { AssertIsOnOwningThread(); MOZ_CRASH("PBackgroundIDBCursorChild actors should be manually constructed!"); @@ -1547,7 +1547,7 @@ mozilla::ipc::IPCResult BackgroundVersionChangeTransactionChild::RecvComplete( PBackgroundIDBRequestChild* BackgroundVersionChangeTransactionChild::AllocPBackgroundIDBRequestChild( - const RequestParams& aParams) { + const int64_t& aRequestId, const RequestParams& aParams) { MOZ_CRASH( "PBackgroundIDBRequestChild actors should be manually " "constructed!"); @@ -1563,7 +1563,7 @@ bool BackgroundVersionChangeTransactionChild::DeallocPBackgroundIDBRequestChild( PBackgroundIDBCursorChild* BackgroundVersionChangeTransactionChild::AllocPBackgroundIDBCursorChild( - const OpenCursorParams& aParams) { + const int64_t& aRequestId, const OpenCursorParams& aParams) { AssertIsOnOwningThread(); MOZ_CRASH("PBackgroundIDBCursorChild actors should be manually constructed!"); @@ -2211,7 +2211,7 @@ BackgroundCursorChild<CursorType>::SafeRefPtrFromThis() { template <IDBCursorType CursorType> void BackgroundCursorChild<CursorType>::SendContinueInternal( - const CursorRequestParams& aParams, + const int64_t aRequestId, const CursorRequestParams& aParams, const CursorData<CursorType>& aCurrentData) { AssertIsOnOwningThread(); MOZ_ASSERT(mRequest); @@ -2394,7 +2394,7 @@ void BackgroundCursorChild<CursorType>::SendContinueInternal( // handling model disallow this? } else { MOZ_ALWAYS_TRUE(PBackgroundIDBCursorChild::SendContinue( - params, currentKey, currentObjectStoreKey)); + aRequestId, params, currentKey, currentObjectStoreKey)); } } diff --git a/dom/indexedDB/ActorsChild.h b/dom/indexedDB/ActorsChild.h index e3e204be0b..194ed28878 100644 --- a/dom/indexedDB/ActorsChild.h +++ b/dom/indexedDB/ActorsChild.h @@ -347,12 +347,12 @@ class BackgroundTransactionChild final : public BackgroundTransactionBase, mozilla::ipc::IPCResult RecvComplete(nsresult aResult); PBackgroundIDBRequestChild* AllocPBackgroundIDBRequestChild( - const RequestParams& aParams); + const int64_t& aRequestId, const RequestParams& aParams); bool DeallocPBackgroundIDBRequestChild(PBackgroundIDBRequestChild* aActor); PBackgroundIDBCursorChild* AllocPBackgroundIDBCursorChild( - const OpenCursorParams& aParams); + const int64_t& aRequestId, const OpenCursorParams& aParams); bool DeallocPBackgroundIDBCursorChild(PBackgroundIDBCursorChild* aActor); }; @@ -393,12 +393,12 @@ class BackgroundVersionChangeTransactionChild final mozilla::ipc::IPCResult RecvComplete(nsresult aResult); PBackgroundIDBRequestChild* AllocPBackgroundIDBRequestChild( - const RequestParams& aParams); + const int64_t& aRequestId, const RequestParams& aParams); bool DeallocPBackgroundIDBRequestChild(PBackgroundIDBRequestChild* aActor); PBackgroundIDBCursorChild* AllocPBackgroundIDBCursorChild( - const OpenCursorParams& aParams); + const int64_t& aRequestId, const OpenCursorParams& aParams); bool DeallocPBackgroundIDBCursorChild(PBackgroundIDBCursorChild* aActor); }; @@ -548,7 +548,8 @@ class BackgroundCursorChild final : public BackgroundCursorChildBase { BackgroundCursorChild(NotNull<IDBRequest*> aRequest, SourceType* aSource, Direction aDirection); - void SendContinueInternal(const CursorRequestParams& aParams, + void SendContinueInternal(const int64_t aRequestId, + const CursorRequestParams& aParams, const CursorData<CursorType>& aCurrentData); void InvalidateCachedResponses(); @@ -594,7 +595,8 @@ class BackgroundCursorChild final : public BackgroundCursorChildBase { mozilla::ipc::IPCResult RecvResponse(CursorResponse&& aResponse) override; // Force callers to use SendContinueInternal. - bool SendContinue(const CursorRequestParams& aParams, const Key& aCurrentKey, + bool SendContinue(const int64_t& aRequestId, + const CursorRequestParams& aParams, const Key& aCurrentKey, const Key& aCurrentObjectStoreKey) = delete; bool SendDeleteMe() = delete; diff --git a/dom/indexedDB/ActorsParent.cpp b/dom/indexedDB/ActorsParent.cpp index 47360094ca..6609ea5b81 100644 --- a/dom/indexedDB/ActorsParent.cpp +++ b/dom/indexedDB/ActorsParent.cpp @@ -94,6 +94,7 @@ #include "mozilla/dom/FileBlobImpl.h" #include "mozilla/dom/FlippedOnce.h" #include "mozilla/dom/IDBCursorBinding.h" +#include "mozilla/dom/IDBFactory.h" #include "mozilla/dom/IPCBlob.h" #include "mozilla/dom/IPCBlobUtils.h" #include "mozilla/dom/IndexedDatabase.h" @@ -122,6 +123,7 @@ #include "mozilla/dom/quota/CheckedUnsafePtr.h" #include "mozilla/dom/quota/Client.h" #include "mozilla/dom/quota/ClientImpl.h" +#include "mozilla/dom/quota/DebugOnlyMacro.h" #include "mozilla/dom/quota/DirectoryLock.h" #include "mozilla/dom/quota/DecryptingInputStream_impl.h" #include "mozilla/dom/quota/EncryptingOutputStream_impl.h" @@ -1973,6 +1975,10 @@ class TransactionDatabaseOperationBase : public DatabaseOperationBase { }; InitializedOnce<const NotNull<SafeRefPtr<TransactionBase>>> mTransaction; + // Unique request id within the context of the transaction, allocated by the + // transaction in the content process starting from 0. Values less than 0 are + // impossible and forbidden. Used to support the explicit commit() request. + const int64_t mRequestId; InternalState mInternalState = InternalState::Initial; bool mWaitingForContinue = false; const bool mTransactionIsAborted; @@ -2034,10 +2040,11 @@ class TransactionDatabaseOperationBase : public DatabaseOperationBase { virtual void Cleanup(); protected: - explicit TransactionDatabaseOperationBase( - SafeRefPtr<TransactionBase> aTransaction); + TransactionDatabaseOperationBase(SafeRefPtr<TransactionBase> aTransaction, + int64_t aRequestId); TransactionDatabaseOperationBase(SafeRefPtr<TransactionBase> aTransaction, + const int64_t aRequestId, uint64_t aLoggingSerialNumber); ~TransactionDatabaseOperationBase() override; @@ -2141,6 +2148,14 @@ class Factory final : public PBackgroundIDBFactoryParent, bool DeallocPBackgroundIDBFactoryRequestParent( PBackgroundIDBFactoryRequestParent* aActor) override; + + mozilla::ipc::IPCResult RecvGetDatabases( + const PersistenceType& aPersistenceType, + const PrincipalInfo& aPrincipalInfo, + GetDatabasesResolver&& aResolve) override; + + private: + Maybe<ContentParentId> GetContentParentId() const; }; class WaitForTransactionsHelper final : public Runnable { @@ -2395,11 +2410,13 @@ class Database final already_AddRefed<PBackgroundIDBTransactionParent> AllocPBackgroundIDBTransactionParent( - const nsTArray<nsString>& aObjectStoreNames, const Mode& aMode) override; + const nsTArray<nsString>& aObjectStoreNames, const Mode& aMode, + const Durability& aDurability) override; mozilla::ipc::IPCResult RecvPBackgroundIDBTransactionConstructor( PBackgroundIDBTransactionParent* aActor, - nsTArray<nsString>&& aObjectStoreNames, const Mode& aMode) override; + nsTArray<nsString>&& aObjectStoreNames, const Mode& aMode, + const Durability& aDurability) override; mozilla::ipc::IPCResult RecvDeleteMe() override; @@ -2418,6 +2435,7 @@ class Database::StartTransactionOp final private: explicit StartTransactionOp(SafeRefPtr<TransactionBase> aTransaction) : TransactionDatabaseOperationBase(std::move(aTransaction), + /* aRequestId */ 0, /* aLoggingSerialNumber */ 0) {} ~StartTransactionOp() override = default; @@ -2591,6 +2609,7 @@ class TransactionBase : public AtomicSafeRefCounted<TransactionBase> { protected: using Mode = IDBTransaction::Mode; + using Durability = IDBTransaction::Durability; private: const SafeRefPtr<Database> mDatabase; @@ -2602,6 +2621,7 @@ class TransactionBase : public AtomicSafeRefCounted<TransactionBase> { uint64_t mActiveRequestCount; Atomic<bool> mInvalidatedOnAnyThread; const Mode mMode; + const Durability mDurability; // TODO: See bug 1883045 FlippedOnce<false> mInitialized; FlippedOnce<false> mHasBeenActiveOnConnectionThread; FlippedOnce<false> mActorDestroyed; @@ -2661,6 +2681,8 @@ class TransactionBase : public AtomicSafeRefCounted<TransactionBase> { Mode GetMode() const { return mMode; } + Durability GetDurability() const { return mDurability; } + const Database& GetDatabase() const { MOZ_ASSERT(mDatabase); @@ -2723,7 +2745,8 @@ class TransactionBase : public AtomicSafeRefCounted<TransactionBase> { virtual ~TransactionBase(); protected: - TransactionBase(SafeRefPtr<Database> aDatabase, Mode aMode); + TransactionBase(SafeRefPtr<Database> aDatabase, Mode aMode, + Durability aDurability); void NoteActorDestroyed() { AssertIsOnBackgroundThread(); @@ -2765,7 +2788,8 @@ class TransactionBase : public AtomicSafeRefCounted<TransactionBase> { CommitOrAbort(); } - PBackgroundIDBRequestParent* AllocRequest(RequestParams&& aParams, + PBackgroundIDBRequestParent* AllocRequest(const int64_t aRequestId, + RequestParams&& aParams, bool aTrustParams); bool StartRequest(PBackgroundIDBRequestParent* aActor); @@ -2775,7 +2799,7 @@ class TransactionBase : public AtomicSafeRefCounted<TransactionBase> { already_AddRefed<PBackgroundIDBCursorParent> AllocCursor( const OpenCursorParams& aParams, bool aTrustParams); - bool StartCursor(PBackgroundIDBCursorParent* aActor, + bool StartCursor(PBackgroundIDBCursorParent* aActor, const int64_t aRequestId, const OpenCursorParams& aParams); virtual void UpdateMetadata(nsresult aResult) {} @@ -2858,26 +2882,27 @@ class NormalTransaction final : public TransactionBase, mozilla::ipc::IPCResult RecvAbort(const nsresult& aResultCode) override; PBackgroundIDBRequestParent* AllocPBackgroundIDBRequestParent( - const RequestParams& aParams) override; + const int64_t& aRequestId, const RequestParams& aParams) override; mozilla::ipc::IPCResult RecvPBackgroundIDBRequestConstructor( - PBackgroundIDBRequestParent* aActor, + PBackgroundIDBRequestParent* aActor, const int64_t& aRequestId, const RequestParams& aParams) override; bool DeallocPBackgroundIDBRequestParent( PBackgroundIDBRequestParent* aActor) override; already_AddRefed<PBackgroundIDBCursorParent> AllocPBackgroundIDBCursorParent( - const OpenCursorParams& aParams) override; + const int64_t& aRequestId, const OpenCursorParams& aParams) override; mozilla::ipc::IPCResult RecvPBackgroundIDBCursorConstructor( - PBackgroundIDBCursorParent* aActor, + PBackgroundIDBCursorParent* aActor, const int64_t& aRequestId, const OpenCursorParams& aParams) override; public: // This constructor is only called by Database. NormalTransaction( SafeRefPtr<Database> aDatabase, TransactionBase::Mode aMode, + TransactionBase::Durability aDurability, nsTArray<SafeRefPtr<FullObjectStoreMetadata>>&& aObjectStores); MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(NormalTransaction, TransactionBase) @@ -2950,26 +2975,25 @@ class VersionChangeTransaction final const IndexOrObjectStoreId& aIndexId, const nsAString& aName) override; PBackgroundIDBRequestParent* AllocPBackgroundIDBRequestParent( - const RequestParams& aParams) override; + const int64_t& aRequestId, const RequestParams& aParams) override; mozilla::ipc::IPCResult RecvPBackgroundIDBRequestConstructor( - PBackgroundIDBRequestParent* aActor, + PBackgroundIDBRequestParent* aActor, const int64_t& aRequestId, const RequestParams& aParams) override; bool DeallocPBackgroundIDBRequestParent( PBackgroundIDBRequestParent* aActor) override; already_AddRefed<PBackgroundIDBCursorParent> AllocPBackgroundIDBCursorParent( - const OpenCursorParams& aParams) override; + const int64_t& aRequestId, const OpenCursorParams& aParams) override; mozilla::ipc::IPCResult RecvPBackgroundIDBCursorConstructor( - PBackgroundIDBCursorParent* aActor, + PBackgroundIDBCursorParent* aActor, const int64_t& aRequestId, const OpenCursorParams& aParams) override; }; class FactoryOp : public DatabaseOperationBase, - public PBackgroundIDBFactoryRequestParent, public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> { public: struct MaybeBlockedDatabaseInfo final { @@ -3008,9 +3032,20 @@ class FactoryOp // Waiting for directory open allowed on the PBackground thread. The next // step is either SendingResults if directory lock failed to acquire, or - // DatabaseOpenPending if directory lock is acquired. + // DirectoryWorkOpen if the factory operation is not tied up to a specific + // database, or DatabaseOpenPending otherwise. DirectoryOpenPending, + // Waiting to do/doing directory work on the QuotaManager IO thread. Its + // next step is DirectoryWorkDone if directory work was successful or + // SendingResults if directory work failed. + DirectoryWorkOpen, + + // Checking if database work can be started. If the database is not blocked + // by other factory operations then the next step is DatabaseWorkOpen. + // Otherwise the next step is DatabaseOpenPending. + DirectoryWorkDone, + // Waiting for database open allowed on the PBackground thread. The next // step is DatabaseWorkOpen. DatabaseOpenPending, @@ -3060,14 +3095,18 @@ class FactoryOp // Must be released on the main thread! RefPtr<DirectoryLock> mDirectoryLock; - RefPtr<FactoryOp> mDelayedOp; + nsTArray<NotNull<RefPtr<FactoryOp>>> mBlocking; + nsTArray<NotNull<RefPtr<FactoryOp>>> mBlockedOn; + nsTArray<MaybeBlockedDatabaseInfo> mMaybeBlockedDatabases; - const CommonFactoryRequestParams mCommonParams; + const PrincipalInfo mPrincipalInfo; OriginMetadata mOriginMetadata; - nsCString mDatabaseId; - nsString mDatabaseFilePath; + Maybe<nsString> mDatabaseName; + Maybe<nsCString> mDatabaseId; + Maybe<nsString> mDatabaseFilePath; int64_t mDirectoryLockId; + const PersistenceType mPersistenceType; State mState; bool mWaitingForPermissionRetry; bool mEnforcingQuota; @@ -3081,17 +3120,23 @@ class FactoryOp return mOriginMetadata.mOrigin; } + const Maybe<nsString>& DatabaseNameRef() const { + AssertIsOnOwningThread(); + + return mDatabaseName; + } + bool DatabaseFilePathIsKnown() const { AssertIsOnOwningThread(); - return !mDatabaseFilePath.IsEmpty(); + return mDatabaseFilePath.isSome(); } const nsAString& DatabaseFilePath() const { AssertIsOnOwningThread(); - MOZ_ASSERT(!mDatabaseFilePath.IsEmpty()); + MOZ_ASSERT(mDatabaseFilePath); - return mDatabaseFilePath; + return mDatabaseFilePath.ref(); } void NoteDatabaseBlocked(Database* aDatabase); @@ -3109,7 +3154,9 @@ class FactoryOp protected: FactoryOp(SafeRefPtr<Factory> aFactory, const Maybe<ContentParentId>& aContentParentId, - const CommonFactoryRequestParams& aCommonParams, bool aDeleting); + const PersistenceType aPersistenceType, + const PrincipalInfo& aPrincipalInfo, + const Maybe<nsString>& aDatabaseName, bool aDeleting); ~FactoryOp() override { // Normally this would be out-of-line since it is a virtual function but @@ -3122,6 +3169,8 @@ class FactoryOp nsresult DirectoryOpen(); + nsresult DirectoryWorkDone(); + nsresult SendToIOThread(); void WaitForTransactions(); @@ -3136,6 +3185,8 @@ class FactoryOp const Maybe<uint64_t>& aNewVersion); // Methods that subclasses must implement. + virtual nsresult DoDirectoryWork() = 0; + virtual nsresult DatabaseOpen() = 0; virtual nsresult DoDatabaseWork() = 0; @@ -3157,17 +3208,56 @@ class FactoryOp void DirectoryLockFailed(); - // IPDL methods. - void ActorDestroy(ActorDestroyReason aWhy) override; - virtual void SendBlockedNotification() = 0; private: // Test whether this FactoryOp needs to wait for the given op. bool MustWaitFor(const FactoryOp& aExistingOp); + + void AddBlockingOp(FactoryOp& aOp) { + AssertIsOnOwningThread(); + + mBlocking.AppendElement(WrapNotNull(&aOp)); + } + + void AddBlockedOnOp(FactoryOp& aOp) { + AssertIsOnOwningThread(); + + mBlockedOn.AppendElement(WrapNotNull(&aOp)); + } + + void MaybeUnblock(FactoryOp& aOp) { + AssertIsOnOwningThread(); + + mBlockedOn.RemoveElement(&aOp); + if (mBlockedOn.IsEmpty()) { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(this)); + } + } +}; + +class FactoryRequestOp : public FactoryOp, + public PBackgroundIDBFactoryRequestParent { + protected: + const CommonFactoryRequestParams mCommonParams; + + FactoryRequestOp(SafeRefPtr<Factory> aFactory, + const Maybe<ContentParentId>& aContentParentId, + const CommonFactoryRequestParams& aCommonParams, + bool aDeleting) + : FactoryOp(std::move(aFactory), aContentParentId, + aCommonParams.metadata().persistenceType(), + aCommonParams.principalInfo(), + Some(aCommonParams.metadata().name()), aDeleting), + mCommonParams(aCommonParams) {} + + nsresult DoDirectoryWork() override; + + // IPDL methods. + void ActorDestroy(ActorDestroyReason aWhy) override; }; -class OpenDatabaseOp final : public FactoryOp { +class OpenDatabaseOp final : public FactoryRequestOp { friend class Database; friend class VersionChangeTransaction; @@ -3249,7 +3339,7 @@ class OpenDatabaseOp::VersionChangeOp final explicit VersionChangeOp(OpenDatabaseOp* aOpenDatabaseOp) : TransactionDatabaseOperationBase( aOpenDatabaseOp->mVersionChangeTransaction.clonePtr(), - aOpenDatabaseOp->LoggingSerialNumber()), + /* aRequestId */ 0, aOpenDatabaseOp->LoggingSerialNumber()), mOpenDatabaseOp(aOpenDatabaseOp), mRequestedVersion(aOpenDatabaseOp->mRequestedVersion), mPreviousVersion( @@ -3269,7 +3359,7 @@ class OpenDatabaseOp::VersionChangeOp final void Cleanup() override; }; -class DeleteDatabaseOp final : public FactoryOp { +class DeleteDatabaseOp final : public FactoryRequestOp { class VersionChangeOp; nsString mDatabaseDirectoryPath; @@ -3280,8 +3370,8 @@ class DeleteDatabaseOp final : public FactoryOp { DeleteDatabaseOp(SafeRefPtr<Factory> aFactory, const Maybe<ContentParentId>& aContentParentId, const CommonFactoryRequestParams& aParams) - : FactoryOp(std::move(aFactory), aContentParentId, aParams, - /* aDeleting */ true), + : FactoryRequestOp(std::move(aFactory), aContentParentId, aParams, + /* aDeleting */ true), mPreviousVersion(0) {} private: @@ -3327,6 +3417,43 @@ class DeleteDatabaseOp::VersionChangeOp final : public DatabaseOperationBase { NS_DECL_NSIRUNNABLE }; +class GetDatabasesOp final : public FactoryOp { + nsTHashMap<nsStringHashKey, DatabaseMetadata> mDatabaseMetadataTable; + nsTArray<DatabaseMetadata> mDatabaseMetadataArray; + Factory::GetDatabasesResolver mResolver; + + public: + GetDatabasesOp(SafeRefPtr<Factory> aFactory, + const Maybe<ContentParentId>& aContentParentId, + const PersistenceType aPersistenceType, + const PrincipalInfo& aPrincipalInfo, + Factory::GetDatabasesResolver&& aResolver) + : FactoryOp(std::move(aFactory), aContentParentId, aPersistenceType, + aPrincipalInfo, Nothing(), /* aDeleting */ false), + mResolver(std::move(aResolver)) {} + + private: + ~GetDatabasesOp() override = default; + + nsresult DatabasesNotAvailable(); + + nsresult DoDirectoryWork() override; + + nsresult DatabaseOpen() override; + + nsresult DoDatabaseWork() override; + + nsresult BeginVersionChange() override; + + bool AreActorsAlive() override; + + void SendBlockedNotification() override; + + nsresult DispatchToWorkThread() override; + + void SendResults() override; +}; + class VersionChangeTransactionOp : public TransactionDatabaseOperationBase { public: void Cleanup() override; @@ -3334,7 +3461,8 @@ class VersionChangeTransactionOp : public TransactionDatabaseOperationBase { protected: explicit VersionChangeTransactionOp( SafeRefPtr<VersionChangeTransaction> aTransaction) - : TransactionDatabaseOperationBase(std::move(aTransaction)) {} + : TransactionDatabaseOperationBase(std::move(aTransaction), + /* aRequestId */ 0) {} ~VersionChangeTransactionOp() override = default; @@ -3519,8 +3647,9 @@ class NormalTransactionOp : public TransactionDatabaseOperationBase, void Cleanup() override; protected: - explicit NormalTransactionOp(SafeRefPtr<TransactionBase> aTransaction) - : TransactionDatabaseOperationBase(std::move(aTransaction)) + NormalTransactionOp(SafeRefPtr<TransactionBase> aTransaction, + const int64_t aRequestId) + : TransactionDatabaseOperationBase(std::move(aTransaction), aRequestId) #ifdef DEBUG , mResponseSent(false) @@ -3628,7 +3757,7 @@ class ObjectStoreAddOrPutRequestOp final : public NormalTransactionOp { }; class SCInputStream; - const ObjectStoreAddPutParams mParams; + ObjectStoreAddPutParams mParams; Maybe<UniqueIndexTable> mUniqueIndexTable; // This must be non-const so that we can update the mNextAutoIncrementId field @@ -3647,6 +3776,7 @@ class ObjectStoreAddOrPutRequestOp final : public NormalTransactionOp { private: // Only created by TransactionBase. ObjectStoreAddOrPutRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const int64_t aRequestId, RequestParams&& aParams); ~ObjectStoreAddOrPutRequestOp() override = default; @@ -3883,6 +4013,7 @@ class ObjectStoreGetRequestOp final : public NormalTransactionOp { private: // Only created by TransactionBase. ObjectStoreGetRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const int64_t aRequestId, const RequestParams& aParams, bool aGetAll); ~ObjectStoreGetRequestOp() override = default; @@ -3912,6 +4043,7 @@ class ObjectStoreGetKeyRequestOp final : public NormalTransactionOp { private: // Only created by TransactionBase. ObjectStoreGetKeyRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const int64_t aRequestId, const RequestParams& aParams, bool aGetAll); ~ObjectStoreGetKeyRequestOp() override = default; @@ -3930,6 +4062,7 @@ class ObjectStoreDeleteRequestOp final : public NormalTransactionOp { private: ObjectStoreDeleteRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const int64_t aRequestId, const ObjectStoreDeleteParams& aParams); ~ObjectStoreDeleteRequestOp() override = default; @@ -3951,6 +4084,7 @@ class ObjectStoreClearRequestOp final : public NormalTransactionOp { private: ObjectStoreClearRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const int64_t aRequestId, const ObjectStoreClearParams& aParams); ~ObjectStoreClearRequestOp() override = default; @@ -3971,8 +4105,10 @@ class ObjectStoreCountRequestOp final : public NormalTransactionOp { private: ObjectStoreCountRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const int64_t aRequestId, const ObjectStoreCountParams& aParams) - : NormalTransactionOp(std::move(aTransaction)), mParams(aParams) {} + : NormalTransactionOp(std::move(aTransaction), aRequestId), + mParams(aParams) {} ~ObjectStoreCountRequestOp() override = default; @@ -3990,8 +4126,8 @@ class IndexRequestOpBase : public NormalTransactionOp { protected: IndexRequestOpBase(SafeRefPtr<TransactionBase> aTransaction, - const RequestParams& aParams) - : NormalTransactionOp(std::move(aTransaction)), + const int64_t aRequestId, const RequestParams& aParams) + : NormalTransactionOp(std::move(aTransaction), aRequestId), mMetadata(IndexMetadataForParams(Transaction(), aParams)) {} ~IndexRequestOpBase() override = default; @@ -4014,7 +4150,8 @@ class IndexGetRequestOp final : public IndexRequestOpBase { private: // Only created by TransactionBase. IndexGetRequestOp(SafeRefPtr<TransactionBase> aTransaction, - const RequestParams& aParams, bool aGetAll); + const int64_t aRequestId, const RequestParams& aParams, + bool aGetAll); ~IndexGetRequestOp() override = default; @@ -4034,7 +4171,8 @@ class IndexGetKeyRequestOp final : public IndexRequestOpBase { private: // Only created by TransactionBase. IndexGetKeyRequestOp(SafeRefPtr<TransactionBase> aTransaction, - const RequestParams& aParams, bool aGetAll); + const int64_t aRequestId, const RequestParams& aParams, + bool aGetAll); ~IndexGetKeyRequestOp() override = default; @@ -4052,8 +4190,8 @@ class IndexCountRequestOp final : public IndexRequestOpBase { private: // Only created by TransactionBase. IndexCountRequestOp(SafeRefPtr<TransactionBase> aTransaction, - const RequestParams& aParams) - : IndexRequestOpBase(std::move(aTransaction), aParams), + const int64_t aRequestId, const RequestParams& aParams) + : IndexRequestOpBase(std::move(aTransaction), aRequestId, aParams), mParams(aParams.get_IndexCountParams()) {} ~IndexCountRequestOp() override = default; @@ -4153,7 +4291,8 @@ class CursorBase : public PBackgroundIDBCursorParent { ~CursorBase() override { MOZ_ASSERT(!mObjectStoreMetadata); } private: - virtual bool Start(const OpenCursorParams& aParams) = 0; + virtual bool Start(const int64_t aRequestId, + const OpenCursorParams& aParams) = 0; }; class IndexCursorBase : public CursorBase { @@ -4311,7 +4450,7 @@ class Cursor final LazyInitializedOnce<const typename Base::ContinueQueries> mContinueQueries; // Only called by TransactionBase. - bool Start(const OpenCursorParams& aParams) final; + bool Start(const int64_t aRequestId, const OpenCursorParams& aParams) final; void SendResponseInternal(CursorResponse& aResponse, const FilesArrayT<CursorType>& aFiles); @@ -4325,8 +4464,8 @@ class Cursor final mozilla::ipc::IPCResult RecvDeleteMe() override; mozilla::ipc::IPCResult RecvContinue( - const CursorRequestParams& aParams, const Key& aCurrentKey, - const Key& aCurrentObjectStoreKey) override; + const int64_t& aRequestId, const CursorRequestParams& aParams, + const Key& aCurrentKey, const Key& aCurrentObjectStoreKey) override; public: Cursor(SafeRefPtr<TransactionBase> aTransaction, @@ -4373,8 +4512,9 @@ class Cursor<CursorType>::CursorOpBase #endif protected: - explicit CursorOpBase(Cursor* aCursor) - : TransactionDatabaseOperationBase(aCursor->mTransaction.clonePtr()), + explicit CursorOpBase(Cursor* aCursor, const int64_t aRequestId) + : TransactionDatabaseOperationBase(aCursor->mTransaction.clonePtr(), + /* aRequestId */ aRequestId), mCursor(aCursor) #ifdef DEBUG , @@ -4552,9 +4692,10 @@ class Cursor<CursorType>::OpenOp final : public CursorOpBase { using CursorOpBase::mResponse; // Only created by Cursor. - OpenOp(Cursor* const aCursor, + OpenOp(Cursor* const aCursor, const int64_t aRequestId, const Maybe<SerializedKeyRange>& aOptionalKeyRange) - : CursorOpBase(aCursor), mOptionalKeyRange(aOptionalKeyRange) {} + : CursorOpBase(aCursor, aRequestId), + mOptionalKeyRange(aOptionalKeyRange) {} // Reference counted. ~OpenOp() override = default; @@ -4572,9 +4713,9 @@ class Cursor<CursorType>::ContinueOp final const CursorRequestParams mParams; // Only created by Cursor. - ContinueOp(Cursor* const aCursor, CursorRequestParams aParams, - CursorPosition<CursorType> aPosition) - : CursorOpBase(aCursor), + ContinueOp(Cursor* const aCursor, int64_t aRequestId, + CursorRequestParams aParams, CursorPosition<CursorType> aPosition) + : CursorOpBase(aCursor, aRequestId), mParams(std::move(aParams)), mCurrentPosition{std::move(aPosition)} { MOZ_ASSERT(mParams.type() != CursorRequestParams::T__None); @@ -4691,6 +4832,8 @@ class DatabaseLoggingInfo final { }; class QuotaClient final : public mozilla::dom::quota::Client { + friend class GetDatabasesOp; + static QuotaClient* sInstance; nsCOMPtr<nsIEventTarget> mBackgroundThread; @@ -4824,8 +4967,9 @@ class QuotaClient final : public mozilla::dom::quota::Client { // checks those unfinished deletion and clean them up after that. template <ObsoleteFilenamesHandling ObsoleteFilenames = ObsoleteFilenamesHandling::Omit> - Result<GetDatabaseFilenamesResult<ObsoleteFilenames>, nsresult> - GetDatabaseFilenames(nsIFile& aDirectory, const AtomicBool& aCanceled); + Result<GetDatabaseFilenamesResult<ObsoleteFilenames>, + nsresult> static GetDatabaseFilenames(nsIFile& aDirectory, + const AtomicBool& aCanceled); nsresult GetUsageForOriginInternal(PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata, @@ -4866,6 +5010,9 @@ class DeleteFilesRunnable final : public Runnable { RefPtr<DirectoryLock> mDirectoryLock; nsTArray<int64_t> mFileIds; State mState; + DEBUGONLY(bool mDEBUGCountsAsPending = false); + + static uint64_t sPendingRunnables; public: DeleteFilesRunnable(SafeRefPtr<DatabaseFileManager> aFileManager, @@ -4873,8 +5020,14 @@ class DeleteFilesRunnable final : public Runnable { void RunImmediately(); + static bool IsDeletionPending() { return sPendingRunnables > 0; } + private: +#ifdef DEBUG + ~DeleteFilesRunnable(); +#else ~DeleteFilesRunnable() = default; +#endif void Open(); @@ -4953,6 +5106,7 @@ class Maintenance final : public Runnable { PRTime mStartTime; RefPtr<UniversalDirectoryLock> mPendingDirectoryLock; RefPtr<UniversalDirectoryLock> mDirectoryLock; + nsTArray<nsCOMPtr<nsIRunnable>> mCompleteCallbacks; nsTArray<DirectoryInfo> mDirectoryInfos; nsTHashMap<nsStringHashKey, DatabaseMaintenance*> mDatabaseMaintenances; nsresult mResultCode; @@ -4994,6 +5148,8 @@ class Maintenance final : public Runnable { void UnregisterDatabaseMaintenance(DatabaseMaintenance* aDatabaseMaintenance); + bool HasDatabaseMaintenances() const { return mDatabaseMaintenances.Count(); } + RefPtr<DatabaseMaintenance> GetDatabaseMaintenance( const nsAString& aDatabasePath) const { AssertIsOnBackgroundThread(); @@ -5001,6 +5157,13 @@ class Maintenance final : public Runnable { return mDatabaseMaintenances.Get(aDatabasePath); } + void WaitForCompletion(nsIRunnable* aCallback) { + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDatabaseMaintenances.Count()); + + mCompleteCallbacks.AppendElement(aCallback); + } + void Stringify(nsACString& aResult) const; private: @@ -6410,9 +6573,9 @@ class DeserializeIndexValueHelper final : public Runnable { value.setUndefined(); ErrorResult rv; - IDBObjectStore::AppendIndexUpdateInfo(mIndexID, mKeyPath, mMultiEntry, - mLocale, jsapi.cx(), value, - &mUpdateInfoArray, &rv); + IDBObjectStore::AppendIndexUpdateInfo( + mIndexID, mKeyPath, mMultiEntry, &mUpdateInfoArray, + /* aAutoIncrementedObjectStoreKeyPath */ VoidString(), &rv); return rv.Failed() ? rv.StealNSResult() : NS_OK; } #endif @@ -6450,9 +6613,9 @@ class DeserializeIndexValueHelper final : public Runnable { [this](const nsresult rv) { OperationCompleted(rv); }); ErrorResult errorResult; - IDBObjectStore::AppendIndexUpdateInfo(mIndexID, mKeyPath, mMultiEntry, - mLocale, cx, value, &mUpdateInfoArray, - &errorResult); + IDBObjectStore::AppendIndexUpdateInfo( + mIndexID, mKeyPath, mMultiEntry, mLocale, cx, value, &mUpdateInfoArray, + /* aAutoIncrementedObjectStoreKeyPath */ VoidString(), &errorResult); QM_TRY(OkIf(!errorResult.Failed()), NS_OK, ([this, &errorResult](const NotOk) { OperationCompleted(errorResult.StealNSResult()); @@ -9111,20 +9274,9 @@ Factory::AllocPBackgroundIDBFactoryRequestParent( return nullptr; } - Maybe<ContentParentId> contentParentId; - - uint64_t childID = BackgroundParent::GetChildID(Manager()); - if (childID) { - // If childID is not zero we are dealing with an other-process actor. We - // want to initialize OpenDatabaseOp/DeleteDatabaseOp here with the ID - // (and later also Database) in that case, so Database::IsOwnedByProcess - // can find Databases belonging to a particular content process when - // QuotaClient::AbortOperationsForProcess is called which is currently used - // to abort operations for content processes only. - contentParentId = Some(ContentParentId(childID)); - } + Maybe<ContentParentId> contentParentId = GetContentParentId(); - auto actor = [&]() -> RefPtr<FactoryOp> { + auto actor = [&]() -> RefPtr<FactoryRequestOp> { if (aParams.type() == FactoryRequestParams::TOpenDatabaseRequestParams) { return MakeRefPtr<OpenDatabaseOp>(SafeRefPtrFromThis(), contentParentId, *commonParams); @@ -9151,7 +9303,7 @@ mozilla::ipc::IPCResult Factory::RecvPBackgroundIDBFactoryRequestConstructor( MOZ_ASSERT(aParams.type() != FactoryRequestParams::T__None); MOZ_ASSERT(!QuotaClient::IsShuttingDownOnBackgroundThread()); - auto* op = static_cast<FactoryOp*>(aActor); + auto* op = static_cast<FactoryRequestOp*>(aActor); MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(op)); return IPC_OK(); @@ -9163,10 +9315,69 @@ bool Factory::DeallocPBackgroundIDBFactoryRequestParent( MOZ_ASSERT(aActor); // Transfer ownership back from IPDL. - RefPtr<FactoryOp> op = dont_AddRef(static_cast<FactoryOp*>(aActor)); + RefPtr<FactoryRequestOp> op = + dont_AddRef(static_cast<FactoryRequestOp*>(aActor)); return true; } +mozilla::ipc::IPCResult Factory::RecvGetDatabases( + const PersistenceType& aPersistenceType, + const PrincipalInfo& aPrincipalInfo, GetDatabasesResolver&& aResolve) { + AssertIsOnBackgroundThread(); + + auto ResolveGetDatabasesAndReturn = [&aResolve](const nsresult rv) { + aResolve(rv); + return IPC_OK(); + }; + + QM_TRY(MOZ_TO_RESULT(!QuotaClient::IsShuttingDownOnBackgroundThread()), + ResolveGetDatabasesAndReturn); + + QM_TRY(MOZ_TO_RESULT(IsValidPersistenceType(aPersistenceType)), + QM_IPC_FAIL(this)); + + QM_TRY(MOZ_TO_RESULT(QuotaManager::IsPrincipalInfoValid(aPrincipalInfo)), + QM_IPC_FAIL(this)); + + MOZ_ASSERT(aPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo || + aPrincipalInfo.type() == PrincipalInfo::TContentPrincipalInfo); + + PersistenceType persistenceType = + IDBFactory::GetPersistenceType(aPrincipalInfo); + + QM_TRY(MOZ_TO_RESULT(aPersistenceType == persistenceType), QM_IPC_FAIL(this)); + + Maybe<ContentParentId> contentParentId = GetContentParentId(); + + auto op = MakeRefPtr<GetDatabasesOp>(SafeRefPtrFromThis(), contentParentId, + aPersistenceType, aPrincipalInfo, + std::move(aResolve)); + + gFactoryOps->AppendElement(op); + + // Balanced in CleanupMetadata() which is/must always called by SendResults(). + IncreaseBusyCount(); + + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(op)); + + return IPC_OK(); +} + +Maybe<ContentParentId> Factory::GetContentParentId() const { + uint64_t childID = BackgroundParent::GetChildID(Manager()); + if (childID) { + // If childID is not zero we are dealing with an other-process actor. We + // want to initialize OpenDatabaseOp/DeleteDatabaseOp here with the ID + // (and later also Database) in that case, so Database::IsOwnedByProcess + // can find Databases belonging to a particular content process when + // QuotaClient::AbortOperationsForProcess is called which is currently used + // to abort operations for content processes only. + return Some(ContentParentId(childID)); + } + + return Nothing(); +} + /******************************************************************************* * WaitForTransactionsHelper ******************************************************************************/ @@ -9608,7 +9819,8 @@ bool Database::DeallocPBackgroundIDBDatabaseFileParent( already_AddRefed<PBackgroundIDBTransactionParent> Database::AllocPBackgroundIDBTransactionParent( - const nsTArray<nsString>& aObjectStoreNames, const Mode& aMode) { + const nsTArray<nsString>& aObjectStoreNames, const Mode& aMode, + const Durability& aDurability) { AssertIsOnBackgroundThread(); // Once a database is closed it must not try to open new transactions. @@ -9628,6 +9840,12 @@ Database::AllocPBackgroundIDBTransactionParent( return nullptr; } + if (NS_AUUF_OR_WARN_IF(aDurability != IDBTransaction::Durability::Default && + aDurability != IDBTransaction::Durability::Strict && + aDurability != IDBTransaction::Durability::Relaxed)) { + return nullptr; + } + const ObjectStoreTable& objectStores = mMetadata->mObjectStores; const uint32_t nameCount = aObjectStoreNames.Length(); @@ -9670,13 +9888,15 @@ Database::AllocPBackgroundIDBTransactionParent( nullptr); return MakeSafeRefPtr<NormalTransaction>(SafeRefPtrFromThis(), aMode, + aDurability, std::move(objectStoreMetadatas)) .forget(); } mozilla::ipc::IPCResult Database::RecvPBackgroundIDBTransactionConstructor( PBackgroundIDBTransactionParent* aActor, - nsTArray<nsString>&& aObjectStoreNames, const Mode& aMode) { + nsTArray<nsString>&& aObjectStoreNames, const Mode& aMode, + const Durability& aDurability) { // TODO: See bug 1883045 AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(!aObjectStoreNames.IsEmpty()); @@ -9684,6 +9904,9 @@ mozilla::ipc::IPCResult Database::RecvPBackgroundIDBTransactionConstructor( aMode == IDBTransaction::Mode::ReadWrite || aMode == IDBTransaction::Mode::ReadWriteFlush || aMode == IDBTransaction::Mode::Cleanup); + MOZ_ASSERT(aDurability == IDBTransaction::Durability::Default || + aDurability == IDBTransaction::Durability::Strict || + aDurability == IDBTransaction::Durability::Relaxed); MOZ_ASSERT(!mClosed); if (IsInvalidated()) { @@ -9815,7 +10038,8 @@ void Database::StartTransactionOp::Cleanup() { * TransactionBase ******************************************************************************/ -TransactionBase::TransactionBase(SafeRefPtr<Database> aDatabase, Mode aMode) +TransactionBase::TransactionBase(SafeRefPtr<Database> aDatabase, Mode aMode, + Durability aDurability) : mDatabase(std::move(aDatabase)), mDatabaseId(mDatabase->Id()), mLoggingSerialNumber( @@ -9823,6 +10047,7 @@ TransactionBase::TransactionBase(SafeRefPtr<Database> aDatabase, Mode aMode) mActiveRequestCount(0), mInvalidatedOnAnyThread(false), mMode(aMode), + mDurability(aDurability), mResultCode(NS_OK) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mDatabase); @@ -9899,14 +10124,13 @@ void TransactionBase::CommitOrAbort() { return; } - // In case of a failed request that was started after committing was - // initiated, abort (cf. - // https://w3c.github.io/IndexedDB/#async-execute-request step 5.3 vs. 5.4). - // Note this can only happen here when we are committing explicitly, otherwise - // the decision is made by the child. + // In case of a failed request and explicitly committed transaction, abort + // (cf. https://w3c.github.io/IndexedDB/#async-execute-request step 5.3 + // vs. 5.4). It's worth emphasizing this can only happen here when we are + // committing explicitly, otherwise the decision is made by the child. if (NS_SUCCEEDED(mResultCode) && mLastFailedRequest && *mLastRequestBeforeCommit && - *mLastFailedRequest >= **mLastRequestBeforeCommit) { + *mLastFailedRequest == **mLastRequestBeforeCommit) { mResultCode = NS_ERROR_DOM_INDEXEDDB_ABORT_ERR; } @@ -10360,7 +10584,7 @@ void TransactionBase::Invalidate() { } PBackgroundIDBRequestParent* TransactionBase::AllocRequest( - RequestParams&& aParams, bool aTrustParams) { + const int64_t aRequestId, RequestParams&& aParams, bool aTrustParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != RequestParams::T__None); @@ -10382,67 +10606,77 @@ PBackgroundIDBRequestParent* TransactionBase::AllocRequest( switch (aParams.type()) { case RequestParams::TObjectStoreAddParams: case RequestParams::TObjectStorePutParams: - actor = new ObjectStoreAddOrPutRequestOp(SafeRefPtrFromThis(), + actor = new ObjectStoreAddOrPutRequestOp(SafeRefPtrFromThis(), aRequestId, std::move(aParams)); break; case RequestParams::TObjectStoreGetParams: - actor = new ObjectStoreGetRequestOp(SafeRefPtrFromThis(), aParams, - /* aGetAll */ false); + actor = + new ObjectStoreGetRequestOp(SafeRefPtrFromThis(), aRequestId, aParams, + /* aGetAll */ false); break; case RequestParams::TObjectStoreGetAllParams: - actor = new ObjectStoreGetRequestOp(SafeRefPtrFromThis(), aParams, - /* aGetAll */ true); + actor = + new ObjectStoreGetRequestOp(SafeRefPtrFromThis(), aRequestId, aParams, + /* aGetAll */ true); break; case RequestParams::TObjectStoreGetKeyParams: - actor = new ObjectStoreGetKeyRequestOp(SafeRefPtrFromThis(), aParams, + actor = new ObjectStoreGetKeyRequestOp(SafeRefPtrFromThis(), aRequestId, + aParams, /* aGetAll */ false); break; case RequestParams::TObjectStoreGetAllKeysParams: - actor = new ObjectStoreGetKeyRequestOp(SafeRefPtrFromThis(), aParams, + actor = new ObjectStoreGetKeyRequestOp(SafeRefPtrFromThis(), aRequestId, + aParams, /* aGetAll */ true); break; case RequestParams::TObjectStoreDeleteParams: - actor = new ObjectStoreDeleteRequestOp( - SafeRefPtrFromThis(), aParams.get_ObjectStoreDeleteParams()); + actor = + new ObjectStoreDeleteRequestOp(SafeRefPtrFromThis(), aRequestId, + aParams.get_ObjectStoreDeleteParams()); break; case RequestParams::TObjectStoreClearParams: - actor = new ObjectStoreClearRequestOp( - SafeRefPtrFromThis(), aParams.get_ObjectStoreClearParams()); + actor = + new ObjectStoreClearRequestOp(SafeRefPtrFromThis(), aRequestId, + aParams.get_ObjectStoreClearParams()); break; case RequestParams::TObjectStoreCountParams: - actor = new ObjectStoreCountRequestOp( - SafeRefPtrFromThis(), aParams.get_ObjectStoreCountParams()); + actor = + new ObjectStoreCountRequestOp(SafeRefPtrFromThis(), aRequestId, + aParams.get_ObjectStoreCountParams()); break; case RequestParams::TIndexGetParams: - actor = new IndexGetRequestOp(SafeRefPtrFromThis(), aParams, + actor = new IndexGetRequestOp(SafeRefPtrFromThis(), aRequestId, aParams, /* aGetAll */ false); break; case RequestParams::TIndexGetKeyParams: - actor = new IndexGetKeyRequestOp(SafeRefPtrFromThis(), aParams, - /* aGetAll */ false); + actor = + new IndexGetKeyRequestOp(SafeRefPtrFromThis(), aRequestId, aParams, + /* aGetAll */ false); break; case RequestParams::TIndexGetAllParams: - actor = new IndexGetRequestOp(SafeRefPtrFromThis(), aParams, + actor = new IndexGetRequestOp(SafeRefPtrFromThis(), aRequestId, aParams, /* aGetAll */ true); break; case RequestParams::TIndexGetAllKeysParams: - actor = new IndexGetKeyRequestOp(SafeRefPtrFromThis(), aParams, - /* aGetAll */ true); + actor = + new IndexGetKeyRequestOp(SafeRefPtrFromThis(), aRequestId, aParams, + /* aGetAll */ true); break; case RequestParams::TIndexCountParams: - actor = new IndexCountRequestOp(SafeRefPtrFromThis(), aParams); + actor = + new IndexCountRequestOp(SafeRefPtrFromThis(), aRequestId, aParams); break; default: @@ -10552,6 +10786,7 @@ already_AddRefed<PBackgroundIDBCursorParent> TransactionBase::AllocCursor( } bool TransactionBase::StartCursor(PBackgroundIDBCursorParent* const aActor, + const int64_t aRequestId, const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); @@ -10559,7 +10794,7 @@ bool TransactionBase::StartCursor(PBackgroundIDBCursorParent* const aActor, auto* const op = static_cast<CursorBase*>(aActor); - if (NS_WARN_IF(!op->Start(aParams))) { + if (NS_WARN_IF(!op->Start(aRequestId, aParams))) { return false; } @@ -10572,8 +10807,9 @@ bool TransactionBase::StartCursor(PBackgroundIDBCursorParent* const aActor, NormalTransaction::NormalTransaction( SafeRefPtr<Database> aDatabase, TransactionBase::Mode aMode, + TransactionBase::Durability aDurability, nsTArray<SafeRefPtr<FullObjectStoreMetadata>>&& aObjectStores) - : TransactionBase(std::move(aDatabase), aMode), + : TransactionBase(std::move(aDatabase), aMode, aDurability), mObjectStores{std::move(aObjectStores)} { AssertIsOnBackgroundThread(); MOZ_ASSERT(!mObjectStores.IsEmpty()); @@ -10638,16 +10874,18 @@ mozilla::ipc::IPCResult NormalTransaction::RecvAbort( PBackgroundIDBRequestParent* NormalTransaction::AllocPBackgroundIDBRequestParent( - const RequestParams& aParams) { + const int64_t& aRequestId, const RequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != RequestParams::T__None); - return AllocRequest(std::move(const_cast<RequestParams&>(aParams)), + return AllocRequest(aRequestId, + std::move(const_cast<RequestParams&>(aParams)), IsSameProcessActor()); } mozilla::ipc::IPCResult NormalTransaction::RecvPBackgroundIDBRequestConstructor( - PBackgroundIDBRequestParent* const aActor, const RequestParams& aParams) { + PBackgroundIDBRequestParent* const aActor, const int64_t& aRequestId, + const RequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != RequestParams::T__None); @@ -10668,19 +10906,20 @@ bool NormalTransaction::DeallocPBackgroundIDBRequestParent( already_AddRefed<PBackgroundIDBCursorParent> NormalTransaction::AllocPBackgroundIDBCursorParent( - const OpenCursorParams& aParams) { + const int64_t& aRequestId, const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); return AllocCursor(aParams, IsSameProcessActor()); } mozilla::ipc::IPCResult NormalTransaction::RecvPBackgroundIDBCursorConstructor( - PBackgroundIDBCursorParent* const aActor, const OpenCursorParams& aParams) { + PBackgroundIDBCursorParent* const aActor, const int64_t& aRequestId, + const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); - if (!StartCursor(aActor, aParams)) { + if (!StartCursor(aActor, aRequestId, aParams)) { return IPC_FAIL(this, "StartCursor failed!"); } return IPC_OK(); @@ -10693,7 +10932,9 @@ mozilla::ipc::IPCResult NormalTransaction::RecvPBackgroundIDBCursorConstructor( VersionChangeTransaction::VersionChangeTransaction( OpenDatabaseOp* aOpenDatabaseOp) : TransactionBase(aOpenDatabaseOp->mDatabase.clonePtr(), - IDBTransaction::Mode::VersionChange), + IDBTransaction::Mode::VersionChange, + // VersionChange must not change durability. + IDBTransaction::Durability::Default), // Not used. mOpenDatabaseOp(aOpenDatabaseOp) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aOpenDatabaseOp); @@ -10755,7 +10996,7 @@ void VersionChangeTransaction::UpdateMetadata(nsresult aResult) { AssertIsOnBackgroundThread(); MOZ_ASSERT(mOpenDatabaseOp); MOZ_ASSERT(!!mActorWasAlive == !!mOpenDatabaseOp->mDatabase); - MOZ_ASSERT_IF(mActorWasAlive, !mOpenDatabaseOp->mDatabaseId.IsEmpty()); + MOZ_ASSERT_IF(mActorWasAlive, !mOpenDatabaseOp->mDatabaseId.ref().IsEmpty()); if (IsActorDestroyed() || !mActorWasAlive) { return; @@ -11224,17 +11465,19 @@ mozilla::ipc::IPCResult VersionChangeTransaction::RecvRenameIndex( PBackgroundIDBRequestParent* VersionChangeTransaction::AllocPBackgroundIDBRequestParent( - const RequestParams& aParams) { + const int64_t& aRequestId, const RequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != RequestParams::T__None); - return AllocRequest(std::move(const_cast<RequestParams&>(aParams)), + return AllocRequest(aRequestId, + std::move(const_cast<RequestParams&>(aParams)), IsSameProcessActor()); } mozilla::ipc::IPCResult VersionChangeTransaction::RecvPBackgroundIDBRequestConstructor( - PBackgroundIDBRequestParent* aActor, const RequestParams& aParams) { + PBackgroundIDBRequestParent* aActor, const int64_t& aRequestId, + const RequestParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != RequestParams::T__None); @@ -11255,7 +11498,7 @@ bool VersionChangeTransaction::DeallocPBackgroundIDBRequestParent( already_AddRefed<PBackgroundIDBCursorParent> VersionChangeTransaction::AllocPBackgroundIDBCursorParent( - const OpenCursorParams& aParams) { + const int64_t& aRequestId, const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); return AllocCursor(aParams, IsSameProcessActor()); @@ -11263,12 +11506,13 @@ VersionChangeTransaction::AllocPBackgroundIDBCursorParent( mozilla::ipc::IPCResult VersionChangeTransaction::RecvPBackgroundIDBCursorConstructor( - PBackgroundIDBCursorParent* aActor, const OpenCursorParams& aParams) { + PBackgroundIDBCursorParent* aActor, const int64_t& aRequestId, + const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aActor); MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); - if (!StartCursor(aActor, aParams)) { + if (!StartCursor(aActor, aRequestId, aParams)) { return IPC_FAIL(this, "StartCursor failed!"); } return IPC_OK(); @@ -11417,7 +11661,8 @@ bool Cursor<CursorType>::VerifyRequestParams( } template <IDBCursorType CursorType> -bool Cursor<CursorType>::Start(const OpenCursorParams& aParams) { +bool Cursor<CursorType>::Start(const int64_t aRequestId, + const OpenCursorParams& aParams) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() == ToOpenCursorParamsType(CursorType)); MOZ_ASSERT(this->mObjectStoreMetadata); @@ -11429,7 +11674,7 @@ bool Cursor<CursorType>::Start(const OpenCursorParams& aParams) { const Maybe<SerializedKeyRange>& optionalKeyRange = GetCommonOpenCursorParams(aParams).optionalKeyRange(); - const RefPtr<OpenOp> openOp = new OpenOp(this, optionalKeyRange); + const RefPtr<OpenOp> openOp = new OpenOp(this, aRequestId, optionalKeyRange); if (NS_WARN_IF(!openOp->Init(*mTransaction))) { openOp->Cleanup(); @@ -11553,8 +11798,8 @@ mozilla::ipc::IPCResult Cursor<CursorType>::RecvDeleteMe() { template <IDBCursorType CursorType> mozilla::ipc::IPCResult Cursor<CursorType>::RecvContinue( - const CursorRequestParams& aParams, const Key& aCurrentKey, - const Key& aCurrentObjectStoreKey) { + const int64_t& aRequestId, const CursorRequestParams& aParams, + const Key& aCurrentKey, const Key& aCurrentObjectStoreKey) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aParams.type() != CursorRequestParams::T__None); MOZ_ASSERT(this->mObjectStoreMetadata); @@ -11604,7 +11849,7 @@ mozilla::ipc::IPCResult Cursor<CursorType>::RecvContinue( } const RefPtr<ContinueOp> continueOp = - new ContinueOp(this, aParams, std::move(position)); + new ContinueOp(this, aRequestId, aParams, std::move(position)); if (NS_WARN_IF(!continueOp->Init(*mTransaction))) { continueOp->Cleanup(); return IPC_FAIL(this, "ContinueOp initialization failed!"); @@ -11626,15 +11871,18 @@ DatabaseFileManager::DatabaseFileManager( PersistenceType aPersistenceType, const quota::OriginMetadata& aOriginMetadata, const nsAString& aDatabaseName, const nsCString& aDatabaseID, - bool aEnforcingQuota, bool aIsInPrivateBrowsingMode) + const nsAString& aDatabaseFilePath, bool aEnforcingQuota, + bool aIsInPrivateBrowsingMode) : mPersistenceType(aPersistenceType), mOriginMetadata(aOriginMetadata), mDatabaseName(aDatabaseName), mDatabaseID(aDatabaseID), + mDatabaseFilePath(aDatabaseFilePath), mCipherKeyManager( aIsInPrivateBrowsingMode ? new IndexedDBCipherKeyManager("IndexedDBCipherKeyManager") : nullptr), + mDatabaseVersion(0), mEnforcingQuota(aEnforcingQuota), mIsInPrivateBrowsingMode(aIsInPrivateBrowsingMode) {} @@ -12488,6 +12736,17 @@ void QuotaClient::StopIdleMaintenance() { void QuotaClient::InitiateShutdown() { AssertIsOnBackgroundThread(); + MOZ_ASSERT(IsShuttingDownOnBackgroundThread()); + + if (mDeleteTimer) { + // QuotaClient::AsyncDeleteFile will not schedule new timers beyond + // shutdown. And we expect all critical (PBM) deletions to have been + // triggered before this point via ClearPrivateRepository (w/out using + // DeleteFilesRunnable at all). + mDeleteTimer->Cancel(); + mDeleteTimer = nullptr; + mPendingDeleteInfos.Clear(); + } AbortAllOperations(); } @@ -12495,7 +12754,7 @@ void QuotaClient::InitiateShutdown() { bool QuotaClient::IsShutdownCompleted() const { return (!gFactoryOps || gFactoryOps->IsEmpty()) && (!gLiveDatabaseHashtable || !gLiveDatabaseHashtable->Count()) && - !mCurrentMaintenance; + !mCurrentMaintenance && !DeleteFilesRunnable::IsDeletionPending(); } void QuotaClient::ForceKillActors() { @@ -12575,17 +12834,20 @@ void QuotaClient::FinalizeShutdown() { mMaintenanceThreadPool->Shutdown(); mMaintenanceThreadPool = nullptr; } - - if (mDeleteTimer) { - MOZ_ALWAYS_SUCCEEDS(mDeleteTimer->Cancel()); - mDeleteTimer = nullptr; - } } void QuotaClient::DeleteTimerCallback(nsITimer* aTimer, void* aClosure) { AssertIsOnBackgroundThread(); MOZ_ASSERT(aTimer); + // Even though we do not schedule new timers after shutdown has started, + // an already existing one might fire afterwards (actually we think it + // shouldn't, but there is no reason to enforce this invariant). We can + // just ignore it, the cleanup work is done in InitiateShutdown. + if (NS_WARN_IF(IsShuttingDownOnBackgroundThread())) { + return; + } + auto* const self = static_cast<QuotaClient*>(aClosure); MOZ_ASSERT(self); MOZ_ASSERT(self->mDeleteTimer); @@ -12728,6 +12990,8 @@ void QuotaClient::ProcessMaintenanceQueue() { * DeleteFilesRunnable ******************************************************************************/ +uint64_t DeleteFilesRunnable::sPendingRunnables = 0; + DeleteFilesRunnable::DeleteFilesRunnable( SafeRefPtr<DatabaseFileManager> aFileManager, nsTArray<int64_t>&& aFileIds) : Runnable("dom::indexeddb::DeleteFilesRunnable"), @@ -12736,6 +13000,12 @@ DeleteFilesRunnable::DeleteFilesRunnable( mFileIds(std::move(aFileIds)), mState(State_Initial) {} +#ifdef DEBUG +DeleteFilesRunnable::~DeleteFilesRunnable() { + MOZ_ASSERT(!mDEBUGCountsAsPending); +} +#endif + void DeleteFilesRunnable::RunImmediately() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State_Initial); @@ -12747,6 +13017,10 @@ void DeleteFilesRunnable::Open() { AssertIsOnBackgroundThread(); MOZ_ASSERT(mState == State_Initial); + MOZ_ASSERT(!mDEBUGCountsAsPending); + sPendingRunnables++; + DEBUGONLY(mDEBUGCountsAsPending = true); + QuotaManager* const quotaManager = QuotaManager::Get(); if (NS_WARN_IF(!quotaManager)) { Finish(); @@ -12800,6 +13074,9 @@ void DeleteFilesRunnable::UnblockOpen() { MOZ_ASSERT(mState == State_UnblockingOpen); mDirectoryLock = nullptr; + MOZ_ASSERT(mDEBUGCountsAsPending); + sPendingRunnables--; + DEBUGONLY(mDEBUGCountsAsPending = false); mState = State_Completed; } @@ -12892,6 +13169,11 @@ void Maintenance::UnregisterDatabaseMaintenance( return; } + for (const auto& completeCallback : mCompleteCallbacks) { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(completeCallback)); + } + mCompleteCallbacks.Clear(); + mState = State::Finishing; Finish(); } @@ -13289,6 +13571,10 @@ nsresult Maintenance::BeginDatabaseMaintenance() { for (uint32_t index = gFactoryOps->Length(); index > 0; index--) { CheckedUnsafePtr<FactoryOp>& existingOp = (*gFactoryOps)[index - 1]; + if (existingOp->DatabaseNameRef().isNothing()) { + return false; + } + if (!existingOp->DatabaseFilePathIsKnown()) { continue; } @@ -14448,14 +14734,17 @@ void DatabaseOperationBase::AutoSetProgressHandler::Unregister() { FactoryOp::FactoryOp(SafeRefPtr<Factory> aFactory, const Maybe<ContentParentId>& aContentParentId, - const CommonFactoryRequestParams& aCommonParams, - bool aDeleting) + const PersistenceType aPersistenceType, + const PrincipalInfo& aPrincipalInfo, + const Maybe<nsString>& aDatabaseName, bool aDeleting) : DatabaseOperationBase(aFactory->GetLoggingInfo()->Id(), aFactory->GetLoggingInfo()->NextRequestSN()), mFactory(std::move(aFactory)), mContentParentId(aContentParentId), - mCommonParams(aCommonParams), + mPrincipalInfo(aPrincipalInfo), + mDatabaseName(aDatabaseName), mDirectoryLockId(-1), + mPersistenceType(aPersistenceType), mState(State::Initial), mWaitingForPermissionRetry(false), mEnforcingQuota(true), @@ -14506,7 +14795,7 @@ void FactoryOp::NoteDatabaseClosed(Database* const aDatabase) { } DatabaseActorInfo* info; - MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(mDatabaseId, &info)); + MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(mDatabaseId.ref(), &info)); MOZ_ASSERT(info->mWaitingFactoryOp == this); if (AreActorsAlive()) { @@ -14547,6 +14836,14 @@ void FactoryOp::StringifyState(nsACString& aResult) const { aResult.AppendLiteral("DirectoryOpenPending"); return; + case State::DirectoryWorkOpen: + aResult.AppendLiteral("DirectoryWorkOpen"); + return; + + case State::DirectoryWorkDone: + aResult.AppendLiteral("DirectoryWorkDone"); + return; + case State::DatabaseOpenPending: aResult.AppendLiteral("DatabaseOpenPending"); return; @@ -14588,8 +14885,7 @@ void FactoryOp::Stringify(nsACString& aResult) const { AssertIsOnOwningThread(); aResult.AppendLiteral("PersistenceType:"); - aResult.Append( - PersistenceTypeToString(mCommonParams.metadata().persistenceType())); + aResult.Append(PersistenceTypeToString(mPersistenceType)); aResult.Append(kQuotaGenericDelimiter); aResult.AppendLiteral("Origin:"); @@ -14617,32 +14913,25 @@ nsresult FactoryOp::Open() { QuotaManager* const quotaManager = QuotaManager::Get(); MOZ_ASSERT(quotaManager); - const DatabaseMetadata& metadata = mCommonParams.metadata(); - - const PersistenceType persistenceType = metadata.persistenceType(); - - const PrincipalInfo& principalInfo = mCommonParams.principalInfo(); - - QM_TRY_UNWRAP(auto principalMetadata, - quotaManager->GetInfoFromValidatedPrincipalInfo(principalInfo)); + QM_TRY_UNWRAP( + auto principalMetadata, + quotaManager->GetInfoFromValidatedPrincipalInfo(mPrincipalInfo)); - mOriginMetadata = {std::move(principalMetadata), persistenceType}; + mOriginMetadata = {std::move(principalMetadata), mPersistenceType}; - if (principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { - MOZ_ASSERT(mCommonParams.metadata().persistenceType() == - PERSISTENCE_TYPE_PERSISTENT); + if (mPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + MOZ_ASSERT(mPersistenceType == PERSISTENCE_TYPE_PERSISTENT); mEnforcingQuota = false; - } else if (principalInfo.type() == PrincipalInfo::TContentPrincipalInfo) { + } else if (mPrincipalInfo.type() == PrincipalInfo::TContentPrincipalInfo) { const ContentPrincipalInfo& contentPrincipalInfo = - principalInfo.get_ContentPrincipalInfo(); + mPrincipalInfo.get_ContentPrincipalInfo(); MOZ_ASSERT_IF( QuotaManager::IsOriginInternal(contentPrincipalInfo.originNoSuffix()), - mCommonParams.metadata().persistenceType() == - PERSISTENCE_TYPE_PERSISTENT); + mPersistenceType == PERSISTENCE_TYPE_PERSISTENT); - mEnforcingQuota = persistenceType != PERSISTENCE_TYPE_PERSISTENT; + mEnforcingQuota = mPersistenceType != PERSISTENCE_TYPE_PERSISTENT; if (mOriginMetadata.mIsPrivate) { if (StaticPrefs::dom_indexedDB_privateBrowsing_enabled()) { @@ -14663,31 +14952,39 @@ nsresult FactoryOp::Open() { MOZ_ASSERT(false); } - QuotaManager::GetStorageId(persistenceType, mOriginMetadata.mOrigin, - Client::IDB, mDatabaseId); + if (mDatabaseName.isSome()) { + nsCString databaseId; - mDatabaseId.Append('*'); - mDatabaseId.Append(NS_ConvertUTF16toUTF8(metadata.name())); + QuotaManager::GetStorageId(mPersistenceType, mOriginMetadata.mOrigin, + Client::IDB, databaseId); - // Need to get database file path before opening the directory. - // XXX: For what reason? - QM_TRY_UNWRAP( - mDatabaseFilePath, - ([this, metadata, quotaManager]() -> mozilla::Result<nsString, nsresult> { - QM_TRY_INSPECT(const auto& dbFile, - quotaManager->GetOriginDirectory(mOriginMetadata)); + databaseId.Append('*'); + databaseId.Append(NS_ConvertUTF16toUTF8(mDatabaseName.ref())); - QM_TRY(MOZ_TO_RESULT(dbFile->Append( - NS_LITERAL_STRING_FROM_CSTRING(IDB_DIRECTORY_NAME)))); + mDatabaseId = Some(std::move(databaseId)); - QM_TRY(MOZ_TO_RESULT( - dbFile->Append(GetDatabaseFilenameBase(metadata.name(), - mOriginMetadata.mIsPrivate) + - kSQLiteSuffix))); + // Need to get database file path before opening the directory. + // XXX: For what reason? + QM_TRY_UNWRAP( + auto databaseFilePath, + ([this, quotaManager]() -> mozilla::Result<nsString, nsresult> { + QM_TRY_INSPECT(const auto& dbFile, + quotaManager->GetOriginDirectory(mOriginMetadata)); - QM_TRY_RETURN( - MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, dbFile, GetPath)); - }())); + QM_TRY(MOZ_TO_RESULT(dbFile->Append( + NS_LITERAL_STRING_FROM_CSTRING(IDB_DIRECTORY_NAME)))); + + QM_TRY(MOZ_TO_RESULT(dbFile->Append( + GetDatabaseFilenameBase(mDatabaseName.ref(), + mOriginMetadata.mIsPrivate) + + kSQLiteSuffix))); + + QM_TRY_RETURN( + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, dbFile, GetPath)); + }())); + + mDatabaseFilePath = Some(std::move(databaseFilePath)); + } // Open directory mState = State::DirectoryOpenPending; @@ -14711,47 +15008,77 @@ nsresult FactoryOp::DirectoryOpen() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::DirectoryOpenPending); MOZ_ASSERT(mDirectoryLock); - MOZ_ASSERT(!mDatabaseFilePath.IsEmpty()); + + if (mDatabaseName.isNothing()) { + QuotaManager* const quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + // Must set this before dispatching otherwise we will race with the IO + // thread. + mState = State::DirectoryWorkOpen; + + QM_TRY(MOZ_TO_RESULT( + quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL)), + NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, IDB_REPORT_INTERNAL_ERR_LAMBDA); + + return NS_OK; + } + + mState = State::DirectoryWorkDone; + MOZ_ALWAYS_SUCCEEDS(Run()); + + return NS_OK; +} + +nsresult FactoryOp::DirectoryWorkDone() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DirectoryWorkDone); + MOZ_ASSERT(mDirectoryLock); MOZ_ASSERT(gFactoryOps); // See if this FactoryOp needs to wait. - const bool delayed = - std::any_of( - gFactoryOps->rbegin(), gFactoryOps->rend(), - [foundThis = false, &self = *this](const auto& existingOp) mutable { - if (existingOp == &self) { - foundThis = true; - return false; - } + const bool blocked = [&self = *this] { + bool foundThis = false; + bool blocked = false; - if (foundThis && self.MustWaitFor(*existingOp)) { - // Only one op can be delayed. - MOZ_ASSERT(!existingOp->mDelayedOp); - existingOp->mDelayedOp = &self; - return true; - } + for (const auto& existingOp : Reversed(*gFactoryOps)) { + if (existingOp == &self) { + foundThis = true; + continue; + } - return false; - }) || - [&self = *this] { - QuotaClient* quotaClient = QuotaClient::GetInstance(); - MOZ_ASSERT(quotaClient); - - if (RefPtr<Maintenance> currentMaintenance = - quotaClient->GetCurrentMaintenance()) { - if (RefPtr<DatabaseMaintenance> databaseMaintenance = - currentMaintenance->GetDatabaseMaintenance( - self.mDatabaseFilePath)) { - databaseMaintenance->WaitForCompletion(&self); - return true; - } + if (foundThis && self.MustWaitFor(*existingOp)) { + existingOp->AddBlockingOp(self); + self.AddBlockedOnOp(*existingOp); + blocked = true; + } + } + + return blocked; + }() || [&self = *this] { + QuotaClient* quotaClient = QuotaClient::GetInstance(); + MOZ_ASSERT(quotaClient); + + if (RefPtr<Maintenance> currentMaintenance = + quotaClient->GetCurrentMaintenance()) { + if (self.mDatabaseName.isSome()) { + if (RefPtr<DatabaseMaintenance> databaseMaintenance = + currentMaintenance->GetDatabaseMaintenance( + self.mDatabaseFilePath.ref())) { + databaseMaintenance->WaitForCompletion(&self); + return true; } + } else if (currentMaintenance->HasDatabaseMaintenances()) { + currentMaintenance->WaitForCompletion(&self); + return true; + } + } - return false; - }(); + return false; + }(); mState = State::DatabaseOpenPending; - if (!delayed) { + if (!blocked) { QM_TRY(MOZ_TO_RESULT(DatabaseOpen())); } @@ -14785,22 +15112,23 @@ void FactoryOp::WaitForTransactions() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::BeginVersionChange || mState == State::WaitingForOtherDatabasesToClose); - MOZ_ASSERT(!mDatabaseId.IsEmpty()); + MOZ_ASSERT(!mDatabaseId.ref().IsEmpty()); MOZ_ASSERT(!IsActorDestroyed()); mState = State::WaitingForTransactionsToComplete; RefPtr<WaitForTransactionsHelper> helper = - new WaitForTransactionsHelper(mDatabaseId, this); + new WaitForTransactionsHelper(mDatabaseId.ref(), this); helper->WaitForTransactions(); } void FactoryOp::CleanupMetadata() { AssertIsOnOwningThread(); - if (mDelayedOp) { - MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(mDelayedOp.forget())); + for (const NotNull<RefPtr<FactoryOp>>& blockingOp : mBlocking) { + blockingOp->MaybeUnblock(*this); } + mBlocking.Clear(); MOZ_ASSERT(gFactoryOps); gFactoryOps->RemoveElement(this); @@ -14868,12 +15196,23 @@ nsresult FactoryOp::SendVersionChangeMessages( bool FactoryOp::MustWaitFor(const FactoryOp& aExistingOp) { AssertIsOnOwningThread(); - // Things for the same persistence type, the same origin and the same - // database must wait. - return aExistingOp.mCommonParams.metadata().persistenceType() == - mCommonParams.metadata().persistenceType() && - aExistingOp.mOriginMetadata.mOrigin == mOriginMetadata.mOrigin && - aExistingOp.mDatabaseId == mDatabaseId; + // If the persistence types don't overlap, the op can proceed. + if (aExistingOp.mPersistenceType != mPersistenceType) { + return false; + } + + // If the origins don't overlap, the op can proceed. + if (aExistingOp.mOriginMetadata.mOrigin != mOriginMetadata.mOrigin) { + return false; + } + + // If the database ids don't overlap, the op can proceed. + if (!aExistingOp.mDatabaseId.isNothing() && !mDatabaseId.isNothing() && + aExistingOp.mDatabaseId.ref() != mDatabaseId.ref()) { + return false; + } + + return true; } // Run() assumes that the caller holds a strong reference to the object that @@ -14905,6 +15244,14 @@ FactoryOp::Run() { QM_WARNONLY_TRY(MOZ_TO_RESULT(Open()), handleError); break; + case State::DirectoryWorkOpen: + QM_WARNONLY_TRY(MOZ_TO_RESULT(DoDirectoryWork()), handleError); + break; + + case State::DirectoryWorkDone: + QM_WARNONLY_TRY(MOZ_TO_RESULT(DirectoryWorkDone()), handleError); + break; + case State::DatabaseOpenPending: QM_WARNONLY_TRY(MOZ_TO_RESULT(DatabaseOpen()), handleError); break; @@ -14971,7 +15318,11 @@ void FactoryOp::DirectoryLockFailed() { MOZ_ALWAYS_SUCCEEDS(Run()); } -void FactoryOp::ActorDestroy(ActorDestroyReason aWhy) { +nsresult FactoryRequestOp::DoDirectoryWork() { + MOZ_CRASH("Not implemented because this should be unreachable."); +} + +void FactoryRequestOp::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnBackgroundThread(); NoteActorDestroyed(); @@ -14980,8 +15331,8 @@ void FactoryOp::ActorDestroy(ActorDestroyReason aWhy) { OpenDatabaseOp::OpenDatabaseOp(SafeRefPtr<Factory> aFactory, const Maybe<ContentParentId>& aContentParentId, const CommonFactoryRequestParams& aParams) - : FactoryOp(std::move(aFactory), aContentParentId, aParams, - /* aDeleting */ false), + : FactoryRequestOp(std::move(aFactory), aContentParentId, aParams, + /* aDeleting */ false), mMetadata(MakeSafeRefPtr<FullDatabaseMetadata>(aParams.metadata())), mRequestedVersion(aParams.metadata().version()), mVersionChangeOp(nullptr), @@ -14990,7 +15341,7 @@ OpenDatabaseOp::OpenDatabaseOp(SafeRefPtr<Factory> aFactory, void OpenDatabaseOp::ActorDestroy(ActorDestroyReason aWhy) { AssertIsOnOwningThread(); - FactoryOp::ActorDestroy(aWhy); + FactoryRequestOp::ActorDestroy(aWhy); if (mVersionChangeOp) { mVersionChangeOp->NoteActorDestroyed(); @@ -15097,7 +15448,7 @@ nsresult OpenDatabaseOp::DoDatabaseWork() { const auto& databaseFilePath, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, dbFile, GetPath)); - MOZ_ASSERT(databaseFilePath == mDatabaseFilePath); + MOZ_ASSERT(databaseFilePath == mDatabaseFilePath.ref()); } #endif @@ -15114,8 +15465,8 @@ nsresult OpenDatabaseOp::DoDatabaseWork() { if (!fileManager) { fileManager = MakeSafeRefPtr<DatabaseFileManager>( - persistenceType, mOriginMetadata, databaseName, mDatabaseId, - mEnforcingQuota, mInPrivateBrowsing); + persistenceType, mOriginMetadata, databaseName, mDatabaseId.ref(), + mDatabaseFilePath.ref(), mEnforcingQuota, mInPrivateBrowsing); } Maybe<const CipherKey> maybeKey = @@ -15555,7 +15906,7 @@ nsresult OpenDatabaseOp::BeginVersionChange() { MOZ_ASSERT(!mDatabase->IsClosed()); DatabaseActorInfo* info; - MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(mDatabaseId, &info)); + MOZ_ALWAYS_TRUE(gLiveDatabaseHashtable->Get(mDatabaseId.ref(), &info)); MOZ_ASSERT(info->mLiveDatabases.Contains(mDatabase.unsafeGetRawPtr())); MOZ_ASSERT(!info->mWaitingFactoryOp); @@ -15699,9 +16050,9 @@ void OpenDatabaseOp::SendResults() { MOZ_ASSERT_IF(!HasFailed(), !mVersionChangeTransaction); DebugOnly<DatabaseActorInfo*> info = nullptr; - MOZ_ASSERT_IF( - gLiveDatabaseHashtable && gLiveDatabaseHashtable->Get(mDatabaseId, &info), - !info->mWaitingFactoryOp); + MOZ_ASSERT_IF(mDatabaseId.isSome() && gLiveDatabaseHashtable && + gLiveDatabaseHashtable->Get(mDatabaseId.ref(), &info), + !info->mWaitingFactoryOp); if (mVersionChangeTransaction) { MOZ_ASSERT(HasFailed()); @@ -15720,6 +16071,8 @@ void OpenDatabaseOp::SendResults() { // need to update the version in our metadata. mMetadata->mCommonMetadata.version() = mRequestedVersion; + mFileManager->UpdateDatabaseVersion(mRequestedVersion); + nsresult rv = EnsureDatabaseActorIsAlive(); if (NS_SUCCEEDED(rv)) { // We successfully opened a database so use its actor as the success @@ -15767,7 +16120,7 @@ void OpenDatabaseOp::SendResults() { &OpenDatabaseOp::ConnectionClosedCallback); RefPtr<WaitForTransactionsHelper> helper = - new WaitForTransactionsHelper(mDatabaseId, callback); + new WaitForTransactionsHelper(mDatabaseId.ref(), callback); helper->WaitForTransactions(); } else { CleanupMetadata(); @@ -15792,7 +16145,7 @@ void OpenDatabaseOp::EnsureDatabaseActor() { mState == State::DatabaseWorkVersionChange || mState == State::SendingResults); MOZ_ASSERT(!HasFailed()); - MOZ_ASSERT(!mDatabaseFilePath.IsEmpty()); + MOZ_ASSERT(mDatabaseFilePath.isSome()); MOZ_ASSERT(!IsActorDestroyed()); if (mDatabase) { @@ -15800,13 +16153,13 @@ void OpenDatabaseOp::EnsureDatabaseActor() { } MOZ_ASSERT(mMetadata->mDatabaseId.IsEmpty()); - mMetadata->mDatabaseId = mDatabaseId; + mMetadata->mDatabaseId = mDatabaseId.ref(); MOZ_ASSERT(mMetadata->mFilePath.IsEmpty()); - mMetadata->mFilePath = mDatabaseFilePath; + mMetadata->mFilePath = mDatabaseFilePath.ref(); DatabaseActorInfo* info; - if (gLiveDatabaseHashtable->Get(mDatabaseId, &info)) { + if (gLiveDatabaseHashtable->Get(mDatabaseId.ref(), &info)) { AssertMetadataConsistency(*info->mMetadata); mMetadata = info->mMetadata.clonePtr(); } @@ -15833,7 +16186,7 @@ void OpenDatabaseOp::EnsureDatabaseActor() { // XXX Maybe use LookupOrInsertWith above, to avoid a second lookup here? info = gLiveDatabaseHashtable ->InsertOrUpdate( - mDatabaseId, + mDatabaseId.ref(), MakeUnique<DatabaseActorInfo>( mMetadata.clonePtr(), WrapNotNullUnchecked(mDatabase.unsafeGetRawPtr()))) @@ -16128,8 +16481,8 @@ void DeleteDatabaseOp::LoadPreviousVersion(nsIFile& aDatabaseFile) { if (!fileManager) { fileManager = MakeSafeRefPtr<DatabaseFileManager>( - persistenceType, mOriginMetadata, databaseName, mDatabaseId, - mEnforcingQuota, mInPrivateBrowsing); + persistenceType, mOriginMetadata, databaseName, mDatabaseId.ref(), + mDatabaseFilePath.ref(), mEnforcingQuota, mInPrivateBrowsing); } const auto maybeKey = @@ -16237,7 +16590,7 @@ nsresult DeleteDatabaseOp::DoDatabaseWork() { const auto& databaseFilePath, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, dbFile, GetPath)); - MOZ_ASSERT(databaseFilePath == mDatabaseFilePath); + MOZ_ASSERT(databaseFilePath == mDatabaseFilePath.ref()); } #endif @@ -16271,7 +16624,7 @@ nsresult DeleteDatabaseOp::BeginVersionChange() { } DatabaseActorInfo* info; - if (gLiveDatabaseHashtable->Get(mDatabaseId, &info)) { + if (gLiveDatabaseHashtable->Get(mDatabaseId.ref(), &info)) { MOZ_ASSERT(!info->mWaitingFactoryOp); nsresult rv = @@ -16345,9 +16698,9 @@ void DeleteDatabaseOp::SendResults() { MOZ_ASSERT(mMaybeBlockedDatabases.IsEmpty()); DebugOnly<DatabaseActorInfo*> info = nullptr; - MOZ_ASSERT_IF( - gLiveDatabaseHashtable && gLiveDatabaseHashtable->Get(mDatabaseId, &info), - !info->mWaitingFactoryOp); + MOZ_ASSERT_IF(mDatabaseId.isSome() && gLiveDatabaseHashtable && + gLiveDatabaseHashtable->Get(mDatabaseId.ref(), &info), + !info->mWaitingFactoryOp); if (!IsActorDestroyed()) { FactoryRequestResponse response; @@ -16428,7 +16781,7 @@ void DeleteDatabaseOp::VersionChangeOp::RunOnOwningThread() { // Inform all the other databases that they are now invalidated. That // should remove the previous metadata from our table. - if (gLiveDatabaseHashtable->Get(deleteOp->mDatabaseId, &info)) { + if (gLiveDatabaseHashtable->Get(deleteOp->mDatabaseId.ref(), &info)) { MOZ_ASSERT(!info->mLiveDatabases.IsEmpty()); MOZ_ASSERT(!info->mWaitingFactoryOp); @@ -16454,7 +16807,7 @@ void DeleteDatabaseOp::VersionChangeOp::RunOnOwningThread() { database->Invalidate(); } - MOZ_ASSERT(!gLiveDatabaseHashtable->Get(deleteOp->mDatabaseId)); + MOZ_ASSERT(!gLiveDatabaseHashtable->Get(deleteOp->mDatabaseId.ref())); } } } @@ -16491,21 +16844,257 @@ nsresult DeleteDatabaseOp::VersionChangeOp::Run() { return NS_OK; } +nsresult GetDatabasesOp::DatabasesNotAvailable() { + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State::DatabaseWorkOpen); + + mState = State::SendingResults; + + QM_TRY(MOZ_TO_RESULT(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL))); + + return NS_OK; +} + +nsresult GetDatabasesOp::DoDirectoryWork() { + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State::DirectoryWorkOpen); + + // This state (DirectoryWorkOpen) runs immediately on the I/O thread, before + // waiting for existing factory operations to complete (at which point + // DoDatabaseWork will be invoked). To match the spec, we must snapshot the + // current state of any databases that are being created (version = 0) or + // upgraded (version >= 1) now. If we only sampled these values in + // DoDatabaseWork, we would only see their post-creation/post-upgrade + // versions, which would be incorrect. + + IndexedDatabaseManager* const idm = IndexedDatabaseManager::Get(); + MOZ_ASSERT(idm); + + const auto& fileManagers = + idm->GetFileManagers(mPersistenceType, mOriginMetadata.mOrigin); + + for (const auto& fileManager : fileManagers) { + auto& metadata = + mDatabaseMetadataTable.LookupOrInsert(fileManager->DatabaseFilePath()); + metadata.name() = fileManager->DatabaseName(); + metadata.version() = fileManager->DatabaseVersion(); + } + + // Must set this before dispatching otherwise we will race with the IO thread. + mState = State::DirectoryWorkDone; + + QM_TRY(MOZ_TO_RESULT(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL))); + + return NS_OK; +} + +nsresult GetDatabasesOp::DatabaseOpen() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::DatabaseOpenPending); + + nsresult rv = SendToIOThread(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult GetDatabasesOp::DoDatabaseWork() { + AssertIsOnIOThread(); + MOZ_ASSERT(mState == State::DatabaseWorkOpen); + + AUTO_PROFILER_LABEL("GetDatabasesOp::DoDatabaseWork", DOM); + + if (NS_WARN_IF(QuotaClient::IsShuttingDownOnNonBackgroundThread()) || + !OperationMayProceed()) { + IDB_REPORT_INTERNAL_ERR(); + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; + } + + QuotaManager* const quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + if (mPersistenceType != PERSISTENCE_TYPE_PERSISTENT) { + QM_TRY(MOZ_TO_RESULT( + quotaManager->EnsureTemporaryStorageIsInitializedInternal())); + } + + { + QM_TRY_INSPECT(const bool& exists, + quotaManager->DoesOriginDirectoryExist(mOriginMetadata)); + if (!exists) { + return DatabasesNotAvailable(); + } + } + + QM_TRY((["aManager, this]() + -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> { + if (mPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + QM_TRY_RETURN( + quotaManager->EnsurePersistentOriginIsInitialized(mOriginMetadata)); + } + + QM_TRY_RETURN(quotaManager->EnsureTemporaryOriginIsInitialized( + mPersistenceType, mOriginMetadata)); + }() + .map([](const auto& res) { return Ok{}; }))); + + { + QM_TRY_INSPECT(const bool& exists, + quotaManager->DoesClientDirectoryExist( + ClientMetadata{mOriginMetadata, Client::IDB})); + if (!exists) { + return DatabasesNotAvailable(); + } + } + + QM_TRY_INSPECT( + const auto& clientDirectory, + (["aManager, this]() + -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> { + if (mPersistenceType == PERSISTENCE_TYPE_PERSISTENT) { + QM_TRY_RETURN(quotaManager->EnsurePersistentClientIsInitialized( + ClientMetadata{mOriginMetadata, Client::IDB})); + } + + QM_TRY_RETURN(quotaManager->EnsureTemporaryClientIsInitialized( + ClientMetadata{mOriginMetadata, Client::IDB})); + }() + .map([](const auto& res) { return res.first; }))); + + QM_TRY_INSPECT( + (const auto& [subdirsToProcess, databaseFilenames]), + QuotaClient::GetDatabaseFilenames(*clientDirectory, + /* aCanceled */ Atomic<bool>{false})); + + for (const auto& databaseFilename : databaseFilenames) { + QM_TRY_INSPECT( + const auto& databaseFile, + CloneFileAndAppend(*clientDirectory, databaseFilename + kSQLiteSuffix)); + + nsString path; + databaseFile->GetPath(path); + + // Use the snapshotted values from DoDirectoryWork which correctly + // snapshotted the state of any pending creations/upgrades. This does mean + // that we need to skip reporting databases that had a version of 0 at that + // time because they were still being created. In the event that any other + // creation or upgrade requests are made after our operation is created, + // this operation will block those, so it's not possible for this set of + // data to get out of sync. The snapshotting (using cached database name + // and version in DatabaseFileManager) also guarantees that we are not + // touching the SQLite database here on the QuotaManager I/O thread which + // is already open on the connection thread. + + auto metadata = mDatabaseMetadataTable.Lookup(path); + if (metadata) { + if (metadata->version() != 0) { + mDatabaseMetadataArray.AppendElement(DatabaseMetadata( + metadata->name(), metadata->version(), mPersistenceType)); + } + + continue; + } + + // Since the database is not already open (there was no DatabaseFileManager + // for snapshotting in DoDirectoryWork which could provide us with the + // database name and version without needing to open the SQLite database), + // it is safe and necessary for us to open the database on this thread and + // retrieve its name and version. We do not need to worry about racing a + // database open because database opens can only be processed on this + // thread and we are performing the steps below synchronously. + + QM_TRY_INSPECT( + const auto& fmDirectory, + CloneFileAndAppend(*clientDirectory, + databaseFilename + kFileManagerDirectoryNameSuffix)); + + QM_TRY_UNWRAP( + const NotNull<nsCOMPtr<mozIStorageConnection>> connection, + CreateStorageConnection(*databaseFile, *fmDirectory, VoidString(), + mOriginMetadata.mOrigin, mDirectoryLockId, + TelemetryIdForFile(databaseFile), Nothing{})); + + { + // Load version information. + QM_TRY_INSPECT(const auto& stmt, + CreateAndExecuteSingleStepStatement< + SingleStepResult::ReturnNullIfNoResult>( + *connection, "SELECT name, version FROM database"_ns)); + + QM_TRY(OkIf(stmt), NS_ERROR_FILE_CORRUPTED); + + QM_TRY_INSPECT( + const auto& databaseName, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, stmt, GetString, 0)); + + QM_TRY_INSPECT(const int64_t& version, + MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 1)); + + mDatabaseMetadataArray.AppendElement( + DatabaseMetadata(databaseName, version, mPersistenceType)); + } + } + + mState = State::SendingResults; + + QM_TRY(MOZ_TO_RESULT(mOwningEventTarget->Dispatch(this, NS_DISPATCH_NORMAL))); + + return NS_OK; +} + +nsresult GetDatabasesOp::BeginVersionChange() { + MOZ_CRASH("Not implemented because this should be unreachable."); +} + +bool GetDatabasesOp::AreActorsAlive() { + MOZ_CRASH("Not implemented because this should be unreachable."); +} + +void GetDatabasesOp::SendBlockedNotification() { + MOZ_CRASH("Not implemented because this should be unreachable."); +} + +nsresult GetDatabasesOp::DispatchToWorkThread() { + MOZ_CRASH("Not implemented because this should be unreachable."); +} + +void GetDatabasesOp::SendResults() { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == State::SendingResults); + +#ifdef DEBUG + NoteActorDestroyed(); +#endif + + mResolver(mDatabaseMetadataArray); + + mDirectoryLock = nullptr; + + CleanupMetadata(); + + FinishSendResults(); +} + TransactionDatabaseOperationBase::TransactionDatabaseOperationBase( - SafeRefPtr<TransactionBase> aTransaction) + SafeRefPtr<TransactionBase> aTransaction, const int64_t aRequestId) : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), aTransaction->GetLoggingInfo()->NextRequestSN()), mTransaction(WrapNotNull(std::move(aTransaction))), + mRequestId(aRequestId), mTransactionIsAborted((*mTransaction)->IsAborted()), mTransactionLoggingSerialNumber((*mTransaction)->LoggingSerialNumber()) { MOZ_ASSERT(LoggingSerialNumber()); } TransactionDatabaseOperationBase::TransactionDatabaseOperationBase( - SafeRefPtr<TransactionBase> aTransaction, uint64_t aLoggingSerialNumber) + SafeRefPtr<TransactionBase> aTransaction, const int64_t aRequestId, + uint64_t aLoggingSerialNumber) : DatabaseOperationBase(aTransaction->GetLoggingInfo()->Id(), aLoggingSerialNumber), mTransaction(WrapNotNull(std::move(aTransaction))), + mRequestId(aRequestId), mTransactionIsAborted((*mTransaction)->IsAborted()), mTransactionLoggingSerialNumber((*mTransaction)->LoggingSerialNumber()) {} @@ -16729,7 +17318,7 @@ void TransactionDatabaseOperationBase::SendPreprocessInfoOrResults( mWaitingForContinue = true; } else { if (mLoggingSerialNumber) { - (*mTransaction)->NoteFinishedRequest(mLoggingSerialNumber, ResultCode()); + (*mTransaction)->NoteFinishedRequest(mRequestId, ResultCode()); } Cleanup(); @@ -18199,8 +18788,9 @@ mozilla::ipc::IPCResult NormalTransactionOp::RecvContinue( } ObjectStoreAddOrPutRequestOp::ObjectStoreAddOrPutRequestOp( - SafeRefPtr<TransactionBase> aTransaction, RequestParams&& aParams) - : NormalTransactionOp(std::move(aTransaction)), + SafeRefPtr<TransactionBase> aTransaction, const int64_t aRequestId, + RequestParams&& aParams) + : NormalTransactionOp(std::move(aTransaction), aRequestId), mParams( std::move(aParams.type() == RequestParams::TObjectStoreAddParams ? aParams.get_ObjectStoreAddParams().commonParams() @@ -18429,6 +19019,11 @@ nsresult ObjectStoreAddOrPutRequestOp::DoDatabaseWork( } QM_TRY(key.SetFromInteger(autoIncrementNum)); + + // Update index keys if primary key is preserved in child. + for (auto& updateInfo : mParams.indexUpdateInfos()) { + updateInfo.value().MaybeUpdateAutoIncrementKey(autoIncrementNum); + } } else if (key.IsFloat()) { double numericKey = key.ToFloat(); numericKey = std::min(numericKey, double(1LL << 53)); @@ -18715,9 +19310,9 @@ ObjectStoreAddOrPutRequestOp::SCInputStream::IsNonBlocking(bool* _retval) { } ObjectStoreGetRequestOp::ObjectStoreGetRequestOp( - SafeRefPtr<TransactionBase> aTransaction, const RequestParams& aParams, - bool aGetAll) - : NormalTransactionOp(std::move(aTransaction)), + SafeRefPtr<TransactionBase> aTransaction, const int64_t aRequestId, + const RequestParams& aParams, bool aGetAll) + : NormalTransactionOp(std::move(aTransaction), aRequestId), mObjectStoreId(aGetAll ? aParams.get_ObjectStoreGetAllParams().objectStoreId() : aParams.get_ObjectStoreGetParams().objectStoreId()), @@ -18887,9 +19482,9 @@ void ObjectStoreGetRequestOp::GetResponse(RequestResponse& aResponse, } ObjectStoreGetKeyRequestOp::ObjectStoreGetKeyRequestOp( - SafeRefPtr<TransactionBase> aTransaction, const RequestParams& aParams, - bool aGetAll) - : NormalTransactionOp(std::move(aTransaction)), + SafeRefPtr<TransactionBase> aTransaction, const int64_t aRequestId, + const RequestParams& aParams, bool aGetAll) + : NormalTransactionOp(std::move(aTransaction), aRequestId), mObjectStoreId( aGetAll ? aParams.get_ObjectStoreGetAllKeysParams().objectStoreId() : aParams.get_ObjectStoreGetKeyParams().objectStoreId()), @@ -18975,9 +19570,9 @@ void ObjectStoreGetKeyRequestOp::GetResponse(RequestResponse& aResponse, } ObjectStoreDeleteRequestOp::ObjectStoreDeleteRequestOp( - SafeRefPtr<TransactionBase> aTransaction, + SafeRefPtr<TransactionBase> aTransaction, const int64_t aRequestId, const ObjectStoreDeleteParams& aParams) - : NormalTransactionOp(std::move(aTransaction)), + : NormalTransactionOp(std::move(aTransaction), aRequestId), mParams(aParams), mObjectStoreMayHaveIndexes(false) { AssertIsOnBackgroundThread(); @@ -19036,9 +19631,9 @@ nsresult ObjectStoreDeleteRequestOp::DoDatabaseWork( } ObjectStoreClearRequestOp::ObjectStoreClearRequestOp( - SafeRefPtr<TransactionBase> aTransaction, + SafeRefPtr<TransactionBase> aTransaction, const int64_t aRequestId, const ObjectStoreClearParams& aParams) - : NormalTransactionOp(std::move(aTransaction)), + : NormalTransactionOp(std::move(aTransaction), aRequestId), mParams(aParams), mObjectStoreMayHaveIndexes(false) { AssertIsOnBackgroundThread(); @@ -19209,8 +19804,9 @@ SafeRefPtr<FullIndexMetadata> IndexRequestOpBase::IndexMetadataForParams( } IndexGetRequestOp::IndexGetRequestOp(SafeRefPtr<TransactionBase> aTransaction, + const int64_t aRequestId, const RequestParams& aParams, bool aGetAll) - : IndexRequestOpBase(std::move(aTransaction), aParams), + : IndexRequestOpBase(std::move(aTransaction), aRequestId, aParams), mDatabase(Transaction().GetDatabasePtr()), mOptionalKeyRange(aGetAll ? aParams.get_IndexGetAllParams().optionalKeyRange() @@ -19339,9 +19935,9 @@ void IndexGetRequestOp::GetResponse(RequestResponse& aResponse, } IndexGetKeyRequestOp::IndexGetKeyRequestOp( - SafeRefPtr<TransactionBase> aTransaction, const RequestParams& aParams, - bool aGetAll) - : IndexRequestOpBase(std::move(aTransaction), aParams), + SafeRefPtr<TransactionBase> aTransaction, const int64_t aRequestId, + const RequestParams& aParams, bool aGetAll) + : IndexRequestOpBase(std::move(aTransaction), aRequestId, aParams), mOptionalKeyRange( aGetAll ? aParams.get_IndexGetAllKeysParams().optionalKeyRange() : Some(aParams.get_IndexGetKeyParams().keyRange())), diff --git a/dom/indexedDB/ActorsParentCommon.cpp b/dom/indexedDB/ActorsParentCommon.cpp index 1b92e15dad..5f25bd8ad6 100644 --- a/dom/indexedDB/ActorsParentCommon.cpp +++ b/dom/indexedDB/ActorsParentCommon.cpp @@ -66,7 +66,7 @@ class nsIFile; namespace mozilla::dom::indexedDB { -static_assert(SNAPPY_VERSION == 0x010109); +static_assert(SNAPPY_VERSION == 0x010200); using mozilla::ipc::IsOnBackgroundThread; diff --git a/dom/indexedDB/DatabaseFileManager.h b/dom/indexedDB/DatabaseFileManager.h index 62d11e95a5..fdfea06ba1 100644 --- a/dom/indexedDB/DatabaseFileManager.h +++ b/dom/indexedDB/DatabaseFileManager.h @@ -31,12 +31,15 @@ class DatabaseFileManager final const quota::OriginMetadata mOriginMetadata; const nsString mDatabaseName; const nsCString mDatabaseID; + const nsString mDatabaseFilePath; RefPtr<IndexedDBCipherKeyManager> mCipherKeyManager; LazyInitializedOnce<const nsString> mDirectoryPath; LazyInitializedOnce<const nsString> mJournalDirectoryPath; + uint64_t mDatabaseVersion; + const bool mEnforcingQuota; const bool mIsInPrivateBrowsingMode; @@ -68,7 +71,8 @@ class DatabaseFileManager final DatabaseFileManager(PersistenceType aPersistenceType, const quota::OriginMetadata& aOriginMetadata, const nsAString& aDatabaseName, - const nsCString& aDatabaseID, bool aEnforcingQuota, + const nsCString& aDatabaseID, + const nsAString& aDatabaseFilePath, bool aEnforcingQuota, bool aIsInPrivateBrowsingMode); PersistenceType Type() const { return mPersistenceType; } @@ -83,6 +87,14 @@ class DatabaseFileManager final const nsCString& DatabaseID() const { return mDatabaseID; } + const nsAString& DatabaseFilePath() const { return mDatabaseFilePath; } + + uint64_t DatabaseVersion() const { return mDatabaseVersion; } + + void UpdateDatabaseVersion(uint64_t aDatabaseVersion) { + mDatabaseVersion = aDatabaseVersion; + } + IndexedDBCipherKeyManager& MutableCipherKeyManagerRef() const { MOZ_ASSERT(mIsInPrivateBrowsingMode); MOZ_ASSERT(mCipherKeyManager); diff --git a/dom/indexedDB/IDBCursor.cpp b/dom/indexedDB/IDBCursor.cpp index 2fb6e98715..65234bf792 100644 --- a/dom/indexedDB/IDBCursor.cpp +++ b/dom/indexedDB/IDBCursor.cpp @@ -417,7 +417,8 @@ void IDBTypedCursor<CursorType>::Continue(JSContext* const aCx, IDB_LOG_STRINGIFY(key)); } - GetTypedBackgroundActorRef().SendContinueInternal(ContinueParams(key), mData); + GetTypedBackgroundActorRef().SendContinueInternal( + mTransaction->NextRequestId(), ContinueParams(key), mData); mContinueCalled = true; } @@ -523,6 +524,7 @@ void IDBTypedCursor<CursorType>::ContinuePrimaryKey( IDB_LOG_STRINGIFY(key), IDB_LOG_STRINGIFY(primaryKey)); GetTypedBackgroundActorRef().SendContinueInternal( + mTransaction->NextRequestId(), ContinuePrimaryKeyParams(key, primaryKey), mData); mContinueCalled = true; @@ -573,8 +575,8 @@ void IDBTypedCursor<CursorType>::Advance(const uint32_t aCount, IDB_LOG_STRINGIFY(mSource), IDB_LOG_STRINGIFY(mDirection), aCount); } - GetTypedBackgroundActorRef().SendContinueInternal(AdvanceParams(aCount), - mData); + GetTypedBackgroundActorRef().SendContinueInternal( + mTransaction->NextRequestId(), AdvanceParams(aCount), mData); mContinueCalled = true; } diff --git a/dom/indexedDB/IDBDatabase.cpp b/dom/indexedDB/IDBDatabase.cpp index 4bbc0fc68a..c1a70e9401 100644 --- a/dom/indexedDB/IDBDatabase.cpp +++ b/dom/indexedDB/IDBDatabase.cpp @@ -20,6 +20,7 @@ #include "MainThreadUtils.h" #include "mozilla/ResultExtensions.h" #include "mozilla/Services.h" +#include "mozilla/dom/IDBTransactionBinding.h" #include "mozilla/storage.h" #include "mozilla/Telemetry.h" #include "mozilla/dom/BindingDeclarations.h" @@ -474,7 +475,8 @@ void IDBDatabase::DeleteObjectStore(const nsAString& aName, ErrorResult& aRv) { RefPtr<IDBTransaction> IDBDatabase::Transaction( JSContext* aCx, const StringOrStringSequence& aStoreNames, - IDBTransactionMode aMode, ErrorResult& aRv) { + IDBTransactionMode aMode, const IDBTransactionOptions& aOptions, + ErrorResult& aRv) { AssertIsOnOwningThread(); if ((aMode == IDBTransactionMode::Readwriteflush || @@ -598,8 +600,26 @@ RefPtr<IDBTransaction> IDBDatabase::Transaction( MOZ_CRASH("Unknown mode!"); } + auto durability = IDBTransaction::Durability::Default; + if (aOptions.IsAnyMemberPresent()) { + switch (aOptions.mDurability) { + case mozilla::dom::IDBTransactionDurability::Default: + durability = IDBTransaction::Durability::Default; + break; + case mozilla::dom::IDBTransactionDurability::Strict: + durability = IDBTransaction::Durability::Strict; + break; + case mozilla::dom::IDBTransactionDurability::Relaxed: + durability = IDBTransaction::Durability::Relaxed; + break; + + default: + MOZ_CRASH("Unknown durability hint!"); + } + } + SafeRefPtr<IDBTransaction> transaction = - IDBTransaction::Create(aCx, this, sortedStoreNames, mode); + IDBTransaction::Create(aCx, this, sortedStoreNames, mode, durability); if (NS_WARN_IF(!transaction)) { IDB_REPORT_INTERNAL_ERR(); MOZ_ASSERT(!NS_IsMainThread(), @@ -617,7 +637,7 @@ RefPtr<IDBTransaction> IDBDatabase::Transaction( IDB_LOG_STRINGIFY(*transaction)); if (!mBackgroundActor->SendPBackgroundIDBTransactionConstructor( - actor, sortedStoreNames, mode)) { + actor, sortedStoreNames, mode, durability)) { IDB_REPORT_INTERNAL_ERR(); aRv.ThrowUnknownError("Failed to create IndexedDB transaction"); return nullptr; diff --git a/dom/indexedDB/IDBDatabase.h b/dom/indexedDB/IDBDatabase.h index 3f7c4aa63c..6d96aaa35d 100644 --- a/dom/indexedDB/IDBDatabase.h +++ b/dom/indexedDB/IDBDatabase.h @@ -166,7 +166,8 @@ class IDBDatabase final : public DOMEventTargetHelper { // This will be called from the DOM. [[nodiscard]] RefPtr<IDBTransaction> Transaction( JSContext* aCx, const StringOrStringSequence& aStoreNames, - IDBTransactionMode aMode, ErrorResult& aRv); + IDBTransactionMode aMode, const IDBTransactionOptions& aOptions, + ErrorResult& aRv); IMPL_EVENT_HANDLER(abort) IMPL_EVENT_HANDLER(close) diff --git a/dom/indexedDB/IDBFactory.cpp b/dom/indexedDB/IDBFactory.cpp index 81dce07d7e..51d4c4df23 100644 --- a/dom/indexedDB/IDBFactory.cpp +++ b/dom/indexedDB/IDBFactory.cpp @@ -15,8 +15,9 @@ #include "mozilla/dom/BindingDeclarations.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/IDBFactoryBinding.h" -#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/Promise.h" #include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/ResultExtensions.h" #include "mozilla/dom/BrowserChild.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/ipc/BackgroundChild.h" @@ -378,6 +379,32 @@ bool IDBFactory::AllowedForPrincipal(nsIPrincipal* aPrincipal, return !aPrincipal->GetIsNullPrincipal(); } +// static +PersistenceType IDBFactory::GetPersistenceType( + const PrincipalInfo& aPrincipalInfo) { + if (aPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + // Chrome privilege always gets persistent storage. + return PERSISTENCE_TYPE_PERSISTENT; + } + + if (aPrincipalInfo.type() == PrincipalInfo::TContentPrincipalInfo) { + nsCString origin = + aPrincipalInfo.get_ContentPrincipalInfo().originNoSuffix(); + + if (QuotaManager::IsOriginInternal(origin)) { + // Internal origins always get persistent storage. + return PERSISTENCE_TYPE_PERSISTENT; + } + + if (aPrincipalInfo.get_ContentPrincipalInfo().attrs().mPrivateBrowsingId > + 0) { + return PERSISTENCE_TYPE_PRIVATE; + } + } + + return PERSISTENCE_TYPE_DEFAULT; +} + void IDBFactory::UpdateActiveTransactionCount(int32_t aDelta) { AssertIsOnOwningThread(); MOZ_DIAGNOSTIC_ASSERT(aDelta > 0 || (mActiveTransactionCount + aDelta) < @@ -443,6 +470,71 @@ RefPtr<IDBOpenDBRequest> IDBFactory::DeleteDatabase( /* aDeleting */ true, aCallerType, aRv); } +already_AddRefed<Promise> IDBFactory::Databases(JSContext* const aCx) { + RefPtr<Promise> promise = Promise::CreateInfallible(GetOwnerGlobal()); + + // Nothing can be done here if we have previously failed to create a + // background actor. + if (mBackgroundActorFailed) { + promise->MaybeReject(NS_ERROR_FAILURE); + return promise.forget(); + } + + PersistenceType persistenceType = GetPersistenceType(*mPrincipalInfo); + + QM_TRY(MOZ_TO_RESULT(EnsureBackgroundActor()), [&promise](const nsresult rv) { + promise->MaybeReject(rv); + return promise.forget(); + }); + + mBackgroundActor->SendGetDatabases(persistenceType, *mPrincipalInfo) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [promise](const PBackgroundIDBFactoryChild::GetDatabasesPromise:: + ResolveOrRejectValue& aValue) { + if (aValue.IsReject()) { + promise->MaybeReject(NS_ERROR_FAILURE); + return; + } + + const GetDatabasesResponse& response = aValue.ResolveValue(); + + switch (response.type()) { + case GetDatabasesResponse::Tnsresult: + promise->MaybeReject(response.get_nsresult()); + + break; + + case GetDatabasesResponse::TArrayOfDatabaseMetadata: { + const auto& array = response.get_ArrayOfDatabaseMetadata(); + + Sequence<IDBDatabaseInfo> databaseInfos; + + for (const auto& databaseMetadata : array) { + IDBDatabaseInfo databaseInfo; + + databaseInfo.mName.Construct(databaseMetadata.name()); + databaseInfo.mVersion.Construct(databaseMetadata.version()); + + if (!databaseInfos.AppendElement(std::move(databaseInfo), + fallible)) { + promise->MaybeRejectWithTypeError("Out of memory"); + return; + } + } + + promise->MaybeResolve(databaseInfos); + + break; + } + default: + MOZ_CRASH("Unknown response type!"); + } + }); + + return promise.forget(); +} + int16_t IDBFactory::Cmp(JSContext* aCx, JS::Handle<JS::Value> aFirst, JS::Handle<JS::Value> aSecond, ErrorResult& aRv) { Key first, second; @@ -512,6 +604,63 @@ RefPtr<IDBOpenDBRequest> IDBFactory::DeleteForPrincipal( /* aDeleting */ true, aGuarantee, aRv); } +nsresult IDBFactory::EnsureBackgroundActor() { + if (mBackgroundActor) { + return NS_OK; + } + + BackgroundChildImpl::ThreadLocal* threadLocal = + BackgroundChildImpl::GetThreadLocalForCurrentThread(); + + UniquePtr<ThreadLocal> newIDBThreadLocal; + ThreadLocal* idbThreadLocal; + + if (threadLocal && threadLocal->mIndexedDBThreadLocal) { + idbThreadLocal = threadLocal->mIndexedDBThreadLocal.get(); + } else { + nsCOMPtr<nsIUUIDGenerator> uuidGen = + do_GetService("@mozilla.org/uuid-generator;1"); + MOZ_ASSERT(uuidGen); + + nsID id{}; + MOZ_ALWAYS_SUCCEEDS(uuidGen->GenerateUUIDInPlace(&id)); + + newIDBThreadLocal = WrapUnique(new ThreadLocal(id)); + idbThreadLocal = newIDBThreadLocal.get(); + } + + PBackgroundChild* backgroundActor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!backgroundActor)) { + return NS_ERROR_FAILURE; + } + + { + BackgroundFactoryChild* actor = new BackgroundFactoryChild(*this); + + mBackgroundActor = static_cast<BackgroundFactoryChild*>( + backgroundActor->SendPBackgroundIDBFactoryConstructor( + actor, idbThreadLocal->GetLoggingInfo(), + IndexedDatabaseManager::GetLocale())); + + if (NS_WARN_IF(!mBackgroundActor)) { + return NS_ERROR_FAILURE; + } + } + + if (newIDBThreadLocal) { + if (!threadLocal) { + threadLocal = BackgroundChildImpl::GetThreadLocalForCurrentThread(); + } + MOZ_ASSERT(threadLocal); + MOZ_ASSERT(!threadLocal->mIndexedDBThreadLocal); + + threadLocal->mIndexedDBThreadLocal = std::move(newIDBThreadLocal); + } + + return NS_OK; +} + RefPtr<IDBOpenDBRequest> IDBFactory::OpenInternal( JSContext* aCx, nsIPrincipal* aPrincipal, const nsAString& aName, const Optional<uint64_t>& aVersion, bool aDeleting, CallerType aCallerType, @@ -590,29 +739,7 @@ RefPtr<IDBOpenDBRequest> IDBFactory::OpenInternal( return nullptr; } - PersistenceType persistenceType; - - bool isPersistent = - principalInfo.type() == PrincipalInfo::TSystemPrincipalInfo; - if (!isPersistent && - principalInfo.type() == PrincipalInfo::TContentPrincipalInfo) { - nsCString origin = - principalInfo.get_ContentPrincipalInfo().originNoSuffix(); - isPersistent = QuotaManager::IsOriginInternal(origin); - } - - const bool isPrivate = - principalInfo.type() == PrincipalInfo::TContentPrincipalInfo && - principalInfo.get_ContentPrincipalInfo().attrs().mPrivateBrowsingId > 0; - - if (isPersistent) { - // Chrome privilege and internal origins always get persistent storage. - persistenceType = PERSISTENCE_TYPE_PERSISTENT; - } else if (isPrivate) { - persistenceType = PERSISTENCE_TYPE_PRIVATE; - } else { - persistenceType = PERSISTENCE_TYPE_DEFAULT; - } + PersistenceType persistenceType = GetPersistenceType(principalInfo); DatabaseMetadata& metadata = commonParams.metadata(); metadata.name() = aName; @@ -627,60 +754,11 @@ RefPtr<IDBOpenDBRequest> IDBFactory::OpenInternal( params = OpenDatabaseRequestParams(commonParams); } - if (!mBackgroundActor) { - BackgroundChildImpl::ThreadLocal* threadLocal = - BackgroundChildImpl::GetThreadLocalForCurrentThread(); - - UniquePtr<ThreadLocal> newIDBThreadLocal; - ThreadLocal* idbThreadLocal; - - if (threadLocal && threadLocal->mIndexedDBThreadLocal) { - idbThreadLocal = threadLocal->mIndexedDBThreadLocal.get(); - } else { - nsCOMPtr<nsIUUIDGenerator> uuidGen = - do_GetService("@mozilla.org/uuid-generator;1"); - MOZ_ASSERT(uuidGen); - - nsID id; - MOZ_ALWAYS_SUCCEEDS(uuidGen->GenerateUUIDInPlace(&id)); - - newIDBThreadLocal = WrapUnique(new ThreadLocal(id)); - idbThreadLocal = newIDBThreadLocal.get(); - } - - PBackgroundChild* backgroundActor = - BackgroundChild::GetOrCreateForCurrentThread(); - if (NS_WARN_IF(!backgroundActor)) { - IDB_REPORT_INTERNAL_ERR(); - aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); - return nullptr; - } - - { - BackgroundFactoryChild* actor = new BackgroundFactoryChild(*this); - - mBackgroundActor = static_cast<BackgroundFactoryChild*>( - backgroundActor->SendPBackgroundIDBFactoryConstructor( - actor, idbThreadLocal->GetLoggingInfo(), - IndexedDatabaseManager::GetLocale())); - - if (NS_WARN_IF(!mBackgroundActor)) { - mBackgroundActorFailed = true; - IDB_REPORT_INTERNAL_ERR(); - aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); - return nullptr; - } - } - - if (newIDBThreadLocal) { - if (!threadLocal) { - threadLocal = BackgroundChildImpl::GetThreadLocalForCurrentThread(); - } - MOZ_ASSERT(threadLocal); - MOZ_ASSERT(!threadLocal->mIndexedDBThreadLocal); - - threadLocal->mIndexedDBThreadLocal = std::move(newIDBThreadLocal); - } + nsresult rv = EnsureBackgroundActor(); + if (NS_WARN_IF(NS_FAILED(rv))) { + IDB_REPORT_INTERNAL_ERR(); + aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); + return nullptr; } RefPtr<IDBOpenDBRequest> request = IDBOpenDBRequest::Create( @@ -704,7 +782,7 @@ RefPtr<IDBOpenDBRequest> IDBFactory::OpenInternal( IDB_LOG_STRINGIFY(aVersion)); } - nsresult rv = InitiateRequest(WrapNotNull(request), params); + rv = InitiateRequest(WrapNotNull(request), params); if (NS_WARN_IF(NS_FAILED(rv))) { IDB_REPORT_INTERNAL_ERR(); aRv.Throw(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); diff --git a/dom/indexedDB/IDBFactory.h b/dom/indexedDB/IDBFactory.h index 488f885ae7..7139b26f9c 100644 --- a/dom/indexedDB/IDBFactory.h +++ b/dom/indexedDB/IDBFactory.h @@ -9,6 +9,7 @@ #include "mozilla/Attributes.h" #include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/quota/PersistenceType.h" #include "mozilla/GlobalTeardownObserver.h" #include "mozilla/UniquePtr.h" #include "nsCOMPtr.h" @@ -59,6 +60,7 @@ class IDBFactory final : public GlobalTeardownObserver, public nsWrapperCache { UniquePtr<PrincipalInfo> mPrincipalInfo; + // TODO: Unused, remove me! nsCOMPtr<nsIGlobalObject> mGlobal; // This will only be set if the factory belongs to a window in a child @@ -96,6 +98,9 @@ class IDBFactory final : public GlobalTeardownObserver, public nsWrapperCache { static bool AllowedForPrincipal(nsIPrincipal* aPrincipal, bool* aIsSystemPrincipal = nullptr); + static quota::PersistenceType GetPersistenceType( + const PrincipalInfo& aPrincipalInfo); + void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(IDBFactory); } nsISerialEventTarget* EventTarget() const { @@ -157,6 +162,8 @@ class IDBFactory final : public GlobalTeardownObserver, public nsWrapperCache { JSContext* aCx, const nsAString& aName, const IDBOpenDBOptions& aOptions, CallerType aCallerType, ErrorResult& aRv); + already_AddRefed<Promise> Databases(JSContext* aCx); + int16_t Cmp(JSContext* aCx, JS::Handle<JS::Value> aFirst, JS::Handle<JS::Value> aSecond, ErrorResult& aRv); @@ -194,6 +201,8 @@ class IDBFactory final : public GlobalTeardownObserver, public nsWrapperCache { static nsresult AllowedForWindowInternal(nsPIDOMWindowInner* aWindow, nsCOMPtr<nsIPrincipal>* aPrincipal); + nsresult EnsureBackgroundActor(); + [[nodiscard]] RefPtr<IDBOpenDBRequest> OpenInternal( JSContext* aCx, nsIPrincipal* aPrincipal, const nsAString& aName, const Optional<uint64_t>& aVersion, bool aDeleting, diff --git a/dom/indexedDB/IDBObjectStore.cpp b/dom/indexedDB/IDBObjectStore.cpp index e6464d2410..728f10b105 100644 --- a/dom/indexedDB/IDBObjectStore.cpp +++ b/dom/indexedDB/IDBObjectStore.cpp @@ -410,13 +410,20 @@ RefPtr<IDBObjectStore> IDBObjectStore::Create( void IDBObjectStore::AppendIndexUpdateInfo( const int64_t aIndexID, const KeyPath& aKeyPath, const bool aMultiEntry, const nsCString& aLocale, JSContext* const aCx, JS::Handle<JS::Value> aVal, - nsTArray<IndexUpdateInfo>* const aUpdateInfoArray, ErrorResult* const aRv) { + nsTArray<IndexUpdateInfo>* const aUpdateInfoArray, + const VoidOrObjectStoreKeyPathString& aAutoIncrementedObjectStoreKeyPath, + ErrorResult* const aRv) { // This precondition holds when `aVal` is the result of a structured clone. js::AutoAssertNoContentJS noContentJS(aCx); + static_assert(std::is_same_v<IDBObjectStore::VoidOrObjectStoreKeyPathString, + KeyPath::VoidOrObjectStoreKeyPathString>, + "Inconsistent types"); + if (!aMultiEntry) { Key key; - *aRv = aKeyPath.ExtractKey(aCx, aVal, key); + *aRv = + aKeyPath.ExtractKey(aCx, aVal, key, aAutoIncrementedObjectStoreKeyPath); // If an index's keyPath doesn't match an object, we ignore that object. if (aRv->ErrorCodeIs(NS_ERROR_DOM_INDEXEDDB_DATA_ERR) || key.IsUnset()) { @@ -625,6 +632,21 @@ void IDBObjectStore::GetAddInfo(JSContext* aCx, ValueWrapper& aValueWrapper, const nsTArray<IndexMetadata>& indexes = mSpec->indexes(); const uint32_t idxCount = indexes.Length(); + const auto& autoIncrementedObjectStoreKeyPath = + [this]() -> const nsAString& { + if (AutoIncrement() && GetKeyPath().IsValid()) { + // By https://w3c.github.io/IndexedDB/#database-interface , + // createObjectStore algorithm, step 8, neither arrays nor empty paths + // are allowed for autoincremented object stores. + // See also KeyPath::IsAllowedForObjectStore. + MOZ_ASSERT(GetKeyPath().IsString()); + MOZ_ASSERT(!GetKeyPath().IsEmpty()); + return GetKeyPath().mStrings[0]; + } + + return VoidString(); + }(); + aUpdateInfoArray.SetCapacity(idxCount); // Pretty good estimate for (uint32_t idxIndex = 0; idxIndex < idxCount; idxIndex++) { @@ -632,7 +654,8 @@ void IDBObjectStore::GetAddInfo(JSContext* aCx, ValueWrapper& aValueWrapper, AppendIndexUpdateInfo(metadata.id(), metadata.keyPath(), metadata.multiEntry(), metadata.locale(), aCx, - aValueWrapper.Value(), &aUpdateInfoArray, &aRv); + aValueWrapper.Value(), &aUpdateInfoArray, + autoIncrementedObjectStoreKeyPath, &aRv); if (NS_WARN_IF(aRv.Failed())) { return; } diff --git a/dom/indexedDB/IDBObjectStore.h b/dom/indexedDB/IDBObjectStore.h index dc79fa3616..99bec23327 100644 --- a/dom/indexedDB/IDBObjectStore.h +++ b/dom/indexedDB/IDBObjectStore.h @@ -48,6 +48,7 @@ class IDBObjectStore final : public nsISupports, public nsWrapperCache { using KeyPath = indexedDB::KeyPath; using ObjectStoreSpec = indexedDB::ObjectStoreSpec; using StructuredCloneReadInfoChild = indexedDB::StructuredCloneReadInfoChild; + using VoidOrObjectStoreKeyPathString = nsAString; // For AddOrPut() and DeleteInternal(). // TODO Consider removing this, and making the functions public? @@ -98,11 +99,12 @@ class IDBObjectStore final : public nsISupports, public nsWrapperCache { [[nodiscard]] static RefPtr<IDBObjectStore> Create( SafeRefPtr<IDBTransaction> aTransaction, ObjectStoreSpec& aSpec); - static void AppendIndexUpdateInfo(int64_t aIndexID, const KeyPath& aKeyPath, - bool aMultiEntry, const nsCString& aLocale, - JSContext* aCx, JS::Handle<JS::Value> aVal, - nsTArray<IndexUpdateInfo>* aUpdateInfoArray, - ErrorResult* aRv); + static void AppendIndexUpdateInfo( + int64_t aIndexID, const KeyPath& aKeyPath, bool aMultiEntry, + const nsCString& aLocale, JSContext* aCx, JS::Handle<JS::Value> aVal, + nsTArray<IndexUpdateInfo>* aUpdateInfoArray, + const VoidOrObjectStoreKeyPathString& aAutoIncrementedObjectStoreKeyPath, + ErrorResult* aRv); static void ClearCloneReadInfo( indexedDB::StructuredCloneReadInfoChild& aReadInfo); diff --git a/dom/indexedDB/IDBTransaction.cpp b/dom/indexedDB/IDBTransaction.cpp index d984bcacdf..b35bea6eff 100644 --- a/dom/indexedDB/IDBTransaction.cpp +++ b/dom/indexedDB/IDBTransaction.cpp @@ -89,8 +89,9 @@ auto IDBTransaction::DoWithTransactionChild(const Func& aFunc) const { IDBTransaction::IDBTransaction(IDBDatabase* const aDatabase, const nsTArray<nsString>& aObjectStoreNames, - const Mode aMode, nsString aFilename, - const uint32_t aLineNo, const uint32_t aColumn, + const Mode aMode, const Durability aDurability, + nsString aFilename, const uint32_t aLineNo, + const uint32_t aColumn, CreatedFromFactoryFunction /*aDummy*/) : DOMEventTargetHelper(aDatabase), mDatabase(aDatabase), @@ -98,12 +99,14 @@ IDBTransaction::IDBTransaction(IDBDatabase* const aDatabase, mLoggingSerialNumber(GetIndexedDBThreadLocal()->NextTransactionSN(aMode)), mNextObjectStoreId(0), mNextIndexId(0), + mNextRequestId(0), mAbortCode(NS_OK), mPendingRequestCount(0), mFilename(std::move(aFilename)), mLineNo(aLineNo), mColumn(aColumn), mMode(aMode), + mDurability(aDurability), mRegistered(false), mNotedActiveTransaction(false) { MOZ_ASSERT(aDatabase); @@ -177,9 +180,11 @@ SafeRefPtr<IDBTransaction> IDBTransaction::CreateVersionChange( nsString filename; uint32_t lineNo, column; aOpenRequest->GetCallerLocation(filename, &lineNo, &column); + // XXX: What should we have as durability hint here? auto transaction = MakeSafeRefPtr<IDBTransaction>( aDatabase, emptyObjectStoreNames, Mode::VersionChange, - std::move(filename), lineNo, column, CreatedFromFactoryFunction{}); + Durability::Default, std::move(filename), lineNo, column, + CreatedFromFactoryFunction{}); transaction->NoteActiveTransaction(); @@ -196,7 +201,8 @@ SafeRefPtr<IDBTransaction> IDBTransaction::CreateVersionChange( // static SafeRefPtr<IDBTransaction> IDBTransaction::Create( JSContext* const aCx, IDBDatabase* const aDatabase, - const nsTArray<nsString>& aObjectStoreNames, const Mode aMode) { + const nsTArray<nsString>& aObjectStoreNames, const Mode aMode, + const Durability aDurability) { MOZ_ASSERT(aDatabase); aDatabase->AssertIsOnOwningThread(); MOZ_ASSERT(!aObjectStoreNames.IsEmpty()); @@ -207,8 +213,8 @@ SafeRefPtr<IDBTransaction> IDBTransaction::Create( uint32_t lineNo, column; IDBRequest::CaptureCaller(aCx, filename, &lineNo, &column); auto transaction = MakeSafeRefPtr<IDBTransaction>( - aDatabase, aObjectStoreNames, aMode, std::move(filename), lineNo, column, - CreatedFromFactoryFunction{}); + aDatabase, aObjectStoreNames, aMode, aDurability, std::move(filename), + lineNo, column, CreatedFromFactoryFunction{}); if (!NS_IsMainThread()) { WorkerPrivate* const workerPrivate = GetCurrentThreadWorkerPrivate(); @@ -287,8 +293,9 @@ BackgroundRequestChild* IDBTransaction::StartRequest( BackgroundRequestChild* const actor = new BackgroundRequestChild(std::move(aRequest)); - DoWithTransactionChild([actor, &aParams](auto& transactionChild) { - transactionChild.SendPBackgroundIDBRequestConstructor(actor, aParams); + DoWithTransactionChild([this, actor, &aParams](auto& transactionChild) { + transactionChild.SendPBackgroundIDBRequestConstructor( + actor, NextRequestId(), aParams); }); // Balanced in BackgroundRequestChild::Recv__delete__(). @@ -302,8 +309,9 @@ void IDBTransaction::OpenCursor(PBackgroundIDBCursorChild& aBackgroundActor, AssertIsOnOwningThread(); MOZ_ASSERT(aParams.type() != OpenCursorParams::T__None); - DoWithTransactionChild([&aBackgroundActor, &aParams](auto& actor) { - actor.SendPBackgroundIDBCursorConstructor(&aBackgroundActor, aParams); + DoWithTransactionChild([this, &aBackgroundActor, &aParams](auto& actor) { + actor.SendPBackgroundIDBCursorConstructor(&aBackgroundActor, + NextRequestId(), aParams); }); // Balanced in BackgroundCursorChild::RecvResponse(). @@ -383,15 +391,16 @@ void IDBTransaction::SendCommit(const bool aAutoCommit) { LoggingSerialNumber(), requestSerialNumber, aAutoCommit ? "automatically" : "explicitly"); - const auto lastRequestSerialNumber = - [this, aAutoCommit, - requestSerialNumber]() -> Maybe<decltype(requestSerialNumber)> { + const int64_t requestId = NextRequestId(); + + const auto lastRequestId = [this, aAutoCommit, + requestId]() -> Maybe<decltype(requestId)> { if (aAutoCommit) { return Nothing(); } - // In case of an explicit commit, we need to note the serial number of the - // last request to check if a request submitted before the commit request + // In case of an explicit commit, we need to note the id of the last + // request to check if a request submitted before the commit request // failed. If we are currently in an event handler for a request on this // transaction, ignore this request. This is used to synchronize the // transaction's committing state with the parent side, to abort the @@ -405,15 +414,13 @@ void IDBTransaction::SendCommit(const bool aAutoCommit) { const bool dispatchingEventForThisTransaction = maybeCurrentTransaction && &maybeCurrentTransaction.ref() == this; - return Some(requestSerialNumber - ? (requestSerialNumber - - (dispatchingEventForThisTransaction ? 0 : 1)) + return Some(requestId + ? (requestId - (dispatchingEventForThisTransaction ? 0 : 1)) : 0); }(); - DoWithTransactionChild([lastRequestSerialNumber](auto& actor) { - actor.SendCommit(lastRequestSerialNumber); - }); + DoWithTransactionChild( + [lastRequestId](auto& actor) { actor.SendCommit(lastRequestId); }); mSentCommitOrAbort.Flip(); } @@ -817,6 +824,12 @@ int64_t IDBTransaction::NextIndexId() { return mNextIndexId++; } +int64_t IDBTransaction::NextRequestId() { + AssertIsOnOwningThread(); + + return mNextRequestId++; +} + void IDBTransaction::InvalidateCursorCaches() { AssertIsOnOwningThread(); @@ -869,6 +882,24 @@ IDBTransactionMode IDBTransaction::GetMode(ErrorResult& aRv) const { } } +IDBTransactionDurability IDBTransaction::GetDurability(ErrorResult& aRv) const { + AssertIsOnOwningThread(); + + switch (mDurability) { + case Durability::Default: + return IDBTransactionDurability::Default; + + case Durability::Strict: + return IDBTransactionDurability::Strict; + + case Durability::Relaxed: + return IDBTransactionDurability::Relaxed; + + default: + MOZ_CRASH("Bad mode!"); + } +} + DOMException* IDBTransaction::GetError() const { AssertIsOnOwningThread(); diff --git a/dom/indexedDB/IDBTransaction.h b/dom/indexedDB/IDBTransaction.h index 65fefddfe5..9feafb3c68 100644 --- a/dom/indexedDB/IDBTransaction.h +++ b/dom/indexedDB/IDBTransaction.h @@ -63,6 +63,15 @@ class IDBTransaction final Invalid }; + enum struct Durability { + Default = 0, + Strict, + Relaxed, + + // Only needed for IPC serialization helper, should never be used in code. + Invalid + }; + enum struct ReadyState { Active, Inactive, Committing, Finished }; private: @@ -90,6 +99,11 @@ class IDBTransaction final int64_t mNextObjectStoreId; int64_t mNextIndexId; + // Request ids are issued starting from 0 and incremented by one as we send + // actor creation messages to the parent process. Used to support the + // explicit commit() request. + int64_t mNextRequestId; + nsresult mAbortCode; ///< The result that caused the transaction to be ///< aborted, or NS_OK if not aborted. ///< NS_ERROR_DOM_INDEXEDDB_ABORT_ERR indicates that the @@ -108,6 +122,7 @@ class IDBTransaction final ReadyState mReadyState = ReadyState::Active; FlippedOnce<false> mStarted; const Mode mMode; + const Durability mDurability; bool mRegistered; ///< Whether mDatabase->RegisterTransaction() has been ///< called (which may not be the case if construction was @@ -130,7 +145,8 @@ class IDBTransaction final [[nodiscard]] static SafeRefPtr<IDBTransaction> Create( JSContext* aCx, IDBDatabase* aDatabase, - const nsTArray<nsString>& aObjectStoreNames, Mode aMode); + const nsTArray<nsString>& aObjectStoreNames, Mode aMode, + Durability aDurability); static Maybe<IDBTransaction&> MaybeCurrent(); @@ -232,6 +248,11 @@ class IDBTransaction final return mMode; } + Durability GetDurability() const { + AssertIsOnOwningThread(); + return mDurability; + } + uint32_t GetPendingRequestCount() const { return mPendingRequestCount; } IDBDatabase* Database() const { @@ -280,6 +301,9 @@ class IDBTransaction final // Only for Mode::VersionChange transactions. int64_t NextIndexId(); + // See the comment for mNextRequestId. + int64_t NextRequestId(); + void InvalidateCursorCaches(); void RegisterCursor(IDBCursor& aCursor); void UnregisterCursor(IDBCursor& aCursor); @@ -299,6 +323,8 @@ class IDBTransaction final IDBTransactionMode GetMode(ErrorResult& aRv) const; + IDBTransactionDurability GetDurability(ErrorResult& aRv) const; + DOMException* GetError() const; [[nodiscard]] RefPtr<IDBObjectStore> ObjectStore(const nsAString& aName, @@ -323,8 +349,8 @@ class IDBTransaction final public: IDBTransaction(IDBDatabase* aDatabase, const nsTArray<nsString>& aObjectStoreNames, Mode aMode, - nsString aFilename, uint32_t aLineNo, uint32_t aColumn, - CreatedFromFactoryFunction aDummy); + Durability aDurability, nsString aFilename, uint32_t aLineNo, + uint32_t aColumn, CreatedFromFactoryFunction aDummy); private: ~IDBTransaction(); diff --git a/dom/indexedDB/IndexedDatabase.cpp b/dom/indexedDB/IndexedDatabase.cpp index a73d9fbf5a..538aea5280 100644 --- a/dom/indexedDB/IndexedDatabase.cpp +++ b/dom/indexedDB/IndexedDatabase.cpp @@ -391,7 +391,8 @@ JSObject* CommonStructuredCloneReadCallback( !StructuredCloneHolder::ReadString(aReader, value)) { return nullptr; } - params->Append(key, value); + params->Append(NS_ConvertUTF16toUTF8(key), + NS_ConvertUTF16toUTF8(value)); } if (!WrapAsJSObject(aCx, params, &result)) { diff --git a/dom/indexedDB/IndexedDatabaseManager.cpp b/dom/indexedDB/IndexedDatabaseManager.cpp index 0558c38826..e63dd793cf 100644 --- a/dom/indexedDB/IndexedDatabaseManager.cpp +++ b/dom/indexedDB/IndexedDatabaseManager.cpp @@ -67,6 +67,13 @@ class FileManagerInfo { [[nodiscard]] SafeRefPtr<DatabaseFileManager> GetFileManager( PersistenceType aPersistenceType, const nsAString& aName) const; + [[nodiscard]] SafeRefPtr<DatabaseFileManager> + GetFileManagerByDatabaseFilePath(PersistenceType aPersistenceType, + const nsAString& aDatabaseFilePath) const; + + const nsTArray<SafeRefPtr<DatabaseFileManager>>& GetFileManagers( + PersistenceType aPersistenceType) const; + void AddFileManager(SafeRefPtr<DatabaseFileManager> aFileManager); bool HasFileManagers() const { @@ -86,18 +93,18 @@ class FileManagerInfo { const nsAString& aName); private: - nsTArray<SafeRefPtr<DatabaseFileManager> >& GetArray( + nsTArray<SafeRefPtr<DatabaseFileManager>>& GetArray( PersistenceType aPersistenceType); - const nsTArray<SafeRefPtr<DatabaseFileManager> >& GetImmutableArray( + const nsTArray<SafeRefPtr<DatabaseFileManager>>& GetImmutableArray( PersistenceType aPersistenceType) const { return const_cast<FileManagerInfo*>(this)->GetArray(aPersistenceType); } - nsTArray<SafeRefPtr<DatabaseFileManager> > mPersistentStorageFileManagers; - nsTArray<SafeRefPtr<DatabaseFileManager> > mTemporaryStorageFileManagers; - nsTArray<SafeRefPtr<DatabaseFileManager> > mDefaultStorageFileManagers; - nsTArray<SafeRefPtr<DatabaseFileManager> > mPrivateStorageFileManagers; + nsTArray<SafeRefPtr<DatabaseFileManager>> mPersistentStorageFileManagers; + nsTArray<SafeRefPtr<DatabaseFileManager>> mTemporaryStorageFileManagers; + nsTArray<SafeRefPtr<DatabaseFileManager>> mDefaultStorageFileManagers; + nsTArray<SafeRefPtr<DatabaseFileManager>> mPrivateStorageFileManagers; }; } // namespace indexedDB @@ -199,6 +206,13 @@ auto DatabaseNameMatchPredicate(const nsAString* const aName) { }; } +auto DatabaseFilePathMatchPredicate(const nsAString* const aDatabaseFilePath) { + MOZ_ASSERT(aDatabaseFilePath); + return [aDatabaseFilePath](const auto& fileManager) { + return fileManager->DatabaseFilePath() == *aDatabaseFilePath; + }; +} + } // namespace IndexedDatabaseManager::IndexedDatabaseManager() : mBackgroundActor(nullptr) { @@ -476,6 +490,35 @@ SafeRefPtr<DatabaseFileManager> IndexedDatabaseManager::GetFileManager( return info->GetFileManager(aPersistenceType, aDatabaseName); } +SafeRefPtr<DatabaseFileManager> +IndexedDatabaseManager::GetFileManagerByDatabaseFilePath( + PersistenceType aPersistenceType, const nsACString& aOrigin, + const nsAString& aDatabaseFilePath) { + AssertIsOnIOThread(); + + FileManagerInfo* info; + if (!mFileManagerInfos.Get(aOrigin, &info)) { + return nullptr; + } + + return info->GetFileManagerByDatabaseFilePath(aPersistenceType, + aDatabaseFilePath); +} + +const nsTArray<SafeRefPtr<DatabaseFileManager>>& +IndexedDatabaseManager::GetFileManagers(PersistenceType aPersistenceType, + const nsACString& aOrigin) { + AssertIsOnIOThread(); + + FileManagerInfo* info; + if (!mFileManagerInfos.Get(aOrigin, &info)) { + static nsTArray<SafeRefPtr<DatabaseFileManager>> emptyArray; + return emptyArray; + } + + return info->GetFileManagers(aPersistenceType); +} + void IndexedDatabaseManager::AddFileManager( SafeRefPtr<DatabaseFileManager> aFileManager) { AssertIsOnIOThread(); @@ -680,11 +723,34 @@ SafeRefPtr<DatabaseFileManager> FileManagerInfo::GetFileManager( return foundIt != end ? foundIt->clonePtr() : nullptr; } +SafeRefPtr<DatabaseFileManager> +FileManagerInfo::GetFileManagerByDatabaseFilePath( + PersistenceType aPersistenceType, + const nsAString& aDatabaseFilePath) const { + AssertIsOnIOThread(); + + const auto& managers = GetImmutableArray(aPersistenceType); + + const auto end = managers.cend(); + const auto foundIt = + std::find_if(managers.cbegin(), end, + DatabaseFilePathMatchPredicate(&aDatabaseFilePath)); + + return foundIt != end ? foundIt->clonePtr() : nullptr; +} + +const nsTArray<SafeRefPtr<DatabaseFileManager>>& +FileManagerInfo::GetFileManagers(PersistenceType aPersistenceType) const { + AssertIsOnIOThread(); + + return GetImmutableArray(aPersistenceType); +} + void FileManagerInfo::AddFileManager( SafeRefPtr<DatabaseFileManager> aFileManager) { AssertIsOnIOThread(); - nsTArray<SafeRefPtr<DatabaseFileManager> >& managers = + nsTArray<SafeRefPtr<DatabaseFileManager>>& managers = GetArray(aFileManager->Type()); NS_ASSERTION(!managers.Contains(aFileManager), "Adding more than once?!"); @@ -718,7 +784,7 @@ void FileManagerInfo::InvalidateAndRemoveFileManagers( PersistenceType aPersistenceType) { AssertIsOnIOThread(); - nsTArray<SafeRefPtr<DatabaseFileManager> >& managers = + nsTArray<SafeRefPtr<DatabaseFileManager>>& managers = GetArray(aPersistenceType); for (uint32_t i = 0; i < managers.Length(); i++) { @@ -743,7 +809,7 @@ void FileManagerInfo::InvalidateAndRemoveFileManager( } } -nsTArray<SafeRefPtr<DatabaseFileManager> >& FileManagerInfo::GetArray( +nsTArray<SafeRefPtr<DatabaseFileManager>>& FileManagerInfo::GetArray( PersistenceType aPersistenceType) { switch (aPersistenceType) { case PERSISTENCE_TYPE_PERSISTENT: diff --git a/dom/indexedDB/IndexedDatabaseManager.h b/dom/indexedDB/IndexedDatabaseManager.h index 0766143dd6..3d0f467368 100644 --- a/dom/indexedDB/IndexedDatabaseManager.h +++ b/dom/indexedDB/IndexedDatabaseManager.h @@ -99,6 +99,14 @@ class IndexedDatabaseManager final { PersistenceType aPersistenceType, const nsACString& aOrigin, const nsAString& aDatabaseName); + [[nodiscard]] SafeRefPtr<DatabaseFileManager> + GetFileManagerByDatabaseFilePath(PersistenceType aPersistenceType, + const nsACString& aOrigin, + const nsAString& aDatabaseFilePath); + + const nsTArray<SafeRefPtr<DatabaseFileManager>>& GetFileManagers( + PersistenceType aPersistenceType, const nsACString& aOrigin); + void AddFileManager(SafeRefPtr<DatabaseFileManager> aFileManager); void InvalidateAllFileManagers(); diff --git a/dom/indexedDB/Key.cpp b/dom/indexedDB/Key.cpp index 6f96023a54..7e0c607617 100644 --- a/dom/indexedDB/Key.cpp +++ b/dom/indexedDB/Key.cpp @@ -560,6 +560,51 @@ Result<Ok, nsresult> Key::EncodeString(const Span<const T> aInput, #define KEY_MAXIMUM_BUFFER_LENGTH \ ::mozilla::detail::nsTStringLengthStorage<char>::kMax +void Key::ReserveAutoIncrementKey(bool aFirstOfArray) { + // Allocate memory for the new size + uint32_t oldLen = mBuffer.Length(); + char* buffer; + if (!mBuffer.GetMutableData(&buffer, oldLen + 1 + sizeof(double))) { + return; + } + + // Remember the offset of the buffer to be updated later. + mAutoIncrementKeyOffsets.AppendElement(oldLen + 1); + + // Fill the type. + buffer += oldLen; + *(buffer++) = aFirstOfArray ? (eMaxType + eFloat) : eFloat; + + // Fill up with 0xFF to reserve the buffer in fixed size because the encoded + // string could be trimmed if ended with padding zeros. + mozilla::BigEndian::writeUint64(buffer, UINT64_MAX); +} + +void Key::MaybeUpdateAutoIncrementKey(int64_t aKey) { + if (mAutoIncrementKeyOffsets.IsEmpty()) { + return; + } + + for (uint32_t offset : mAutoIncrementKeyOffsets) { + char* buffer; + MOZ_ALWAYS_TRUE(mBuffer.GetMutableData(&buffer)); + buffer += offset; + WriteDoubleToUint64(buffer, double(aKey)); + } + + TrimBuffer(); +} + +void Key::WriteDoubleToUint64(char* aBuffer, double aValue) { + MOZ_ASSERT(aBuffer); + + uint64_t bits = BitwiseCast<uint64_t>(aValue); + const uint64_t signbit = FloatingPoint<double>::kSignBit; + uint64_t number = bits & signbit ? (-bits) : (bits | signbit); + + mozilla::BigEndian::writeUint64(aBuffer, number); +} + template <typename T> Result<Ok, nsresult> Key::EncodeAsString(const Span<const T> aInput, uint8_t aType) { @@ -797,13 +842,8 @@ Result<Ok, nsresult> Key::EncodeNumber(double aFloat, uint8_t aType) { *(buffer++) = aType; - uint64_t bits = BitwiseCast<uint64_t>(aFloat); - // Note: The subtraction from 0 below is necessary to fix - // MSVC build warning C4146 (negating an unsigned value). - const uint64_t signbit = FloatingPoint<double>::kSignBit; - uint64_t number = bits & signbit ? (0 - bits) : (bits | signbit); + WriteDoubleToUint64(buffer, aFloat); - mozilla::BigEndian::writeUint64(buffer, number); return Ok(); } diff --git a/dom/indexedDB/Key.h b/dom/indexedDB/Key.h index d19bc94e53..25ddd3e0b1 100644 --- a/dom/indexedDB/Key.h +++ b/dom/indexedDB/Key.h @@ -25,6 +25,7 @@ class Key { friend struct IPC::ParamTraits<Key>; nsCString mBuffer; + CopyableTArray<uint32_t> mAutoIncrementKeyOffsets; public: enum { @@ -86,7 +87,10 @@ class Key { return Compare(mBuffer, aOther.mBuffer) >= 0; } - void Unset() { mBuffer.SetIsVoid(true); } + void Unset() { + mBuffer.SetIsVoid(true); + mAutoIncrementKeyOffsets.Clear(); + } bool IsUnset() const { return mBuffer.IsVoid(); } @@ -174,6 +178,10 @@ class Key { return 0; } + void ReserveAutoIncrementKey(bool aFirstOfArray); + + void MaybeUpdateAutoIncrementKey(int64_t aKey); + private: class MOZ_STACK_CLASS ArrayValueEncoder; @@ -273,6 +281,8 @@ class Key { template <typename T> nsresult SetFromSource(T* aSource, uint32_t aIndex); + + void WriteDoubleToUint64(char* aBuffer, double aValue); }; } // namespace mozilla::dom::indexedDB diff --git a/dom/indexedDB/KeyPath.cpp b/dom/indexedDB/KeyPath.cpp index 5cad164296..f27ddd007a 100644 --- a/dom/indexedDB/KeyPath.cpp +++ b/dom/indexedDB/KeyPath.cpp @@ -329,8 +329,9 @@ bool KeyPath::AppendStringWithValidation(const nsAString& aString) { return false; } -nsresult KeyPath::ExtractKey(JSContext* aCx, const JS::Value& aValue, - Key& aKey) const { +nsresult KeyPath::ExtractKey(JSContext* aCx, const JS::Value& aValue, Key& aKey, + const VoidOrObjectStoreKeyPathString& + aAutoIncrementedObjectStoreKeyPath) const { uint32_t len = mStrings.Length(); JS::Rooted<JS::Value> value(aCx); @@ -341,6 +342,17 @@ nsresult KeyPath::ExtractKey(JSContext* aCx, const JS::Value& aValue, GetJSValFromKeyPathString(aCx, aValue, mStrings[i], value.address(), DoNotCreateProperties, nullptr, nullptr); if (NS_FAILED(rv)) { + if (!aAutoIncrementedObjectStoreKeyPath.IsVoid() && + mStrings[i].Equals(aAutoIncrementedObjectStoreKeyPath)) { + // We are extracting index keys of an object to be added if + // object store key path for a string key is provided. + // Because the autoIncrement primary key is part of + // this index key but is not defined in |aValue|, so we reserve + // the space here to update the key later in parent. + aKey.ReserveAutoIncrementKey(IsArray() && i == 0); + continue; + } + return rv; } diff --git a/dom/indexedDB/KeyPath.h b/dom/indexedDB/KeyPath.h index 4e1042650f..5fac30ecf8 100644 --- a/dom/indexedDB/KeyPath.h +++ b/dom/indexedDB/KeyPath.h @@ -45,6 +45,8 @@ class KeyPath { KeyPath() : mType(KeyPathType::NonExistent) { MOZ_COUNT_CTOR(KeyPath); } public: + using VoidOrObjectStoreKeyPathString = nsAString; + enum class KeyPathType { NonExistent, String, Array, EndGuard }; void SetType(KeyPathType aType); @@ -76,7 +78,10 @@ class KeyPath { static Result<KeyPath, nsresult> Parse( const Nullable<OwningStringOrStringSequence>& aValue); - nsresult ExtractKey(JSContext* aCx, const JS::Value& aValue, Key& aKey) const; + nsresult ExtractKey( + JSContext* aCx, const JS::Value& aValue, Key& aKey, + const VoidOrObjectStoreKeyPathString& aAutoIncrementedObjectStoreKeyPath = + VoidString()) const; nsresult ExtractKeyAsJSVal(JSContext* aCx, const JS::Value& aValue, JS::Value* aOutVal) const; diff --git a/dom/indexedDB/PBackgroundIDBCursor.ipdl b/dom/indexedDB/PBackgroundIDBCursor.ipdl index 60c8d2ca7b..e7669f2554 100644 --- a/dom/indexedDB/PBackgroundIDBCursor.ipdl +++ b/dom/indexedDB/PBackgroundIDBCursor.ipdl @@ -87,8 +87,8 @@ protocol PBackgroundIDBCursor parent: async DeleteMe(); - async Continue(CursorRequestParams params, Key currentKey, - Key currentObjectStoreKey); + async Continue(int64_t requestId, CursorRequestParams params, + Key currentKey, Key currentObjectStoreKey); child: async __delete__(); diff --git a/dom/indexedDB/PBackgroundIDBDatabase.ipdl b/dom/indexedDB/PBackgroundIDBDatabase.ipdl index f9262ebadf..88d5e79392 100644 --- a/dom/indexedDB/PBackgroundIDBDatabase.ipdl +++ b/dom/indexedDB/PBackgroundIDBDatabase.ipdl @@ -19,6 +19,9 @@ using struct mozilla::null_t from "mozilla/ipc/IPCCore.h"; using mozilla::dom::IDBTransaction::Mode from "mozilla/dom/IDBTransaction.h"; +using mozilla::dom::IDBTransaction::Durability + from "mozilla/dom/IDBTransaction.h"; + namespace mozilla { namespace dom { namespace indexedDB { @@ -41,7 +44,7 @@ parent: async PBackgroundIDBDatabaseFile(IPCBlob blob); - async PBackgroundIDBTransaction(nsString[] objectStoreNames, Mode mode); + async PBackgroundIDBTransaction(nsString[] objectStoreNames, Mode mode, Durability durability); child: async __delete__(); diff --git a/dom/indexedDB/PBackgroundIDBFactory.ipdl b/dom/indexedDB/PBackgroundIDBFactory.ipdl index ef2a88e55b..c838189b2b 100644 --- a/dom/indexedDB/PBackgroundIDBFactory.ipdl +++ b/dom/indexedDB/PBackgroundIDBFactory.ipdl @@ -40,6 +40,12 @@ union FactoryRequestParams DeleteDatabaseRequestParams; }; +union GetDatabasesResponse +{ + nsresult; + DatabaseMetadata[]; +}; + [ChildImpl="indexedDB::BackgroundFactoryChild", ParentImpl=virtual] sync protocol PBackgroundIDBFactory { @@ -53,6 +59,10 @@ parent: async PBackgroundIDBFactoryRequest(FactoryRequestParams params); + async GetDatabases(PersistenceType persistenceType, + PrincipalInfo principalInfo) + returns(GetDatabasesResponse response); + child: async __delete__(); diff --git a/dom/indexedDB/PBackgroundIDBTransaction.ipdl b/dom/indexedDB/PBackgroundIDBTransaction.ipdl index 85a9f76265..071e031cb2 100644 --- a/dom/indexedDB/PBackgroundIDBTransaction.ipdl +++ b/dom/indexedDB/PBackgroundIDBTransaction.ipdl @@ -34,9 +34,9 @@ parent: async Commit(int64_t? lastRequest); async Abort(nsresult resultCode); - async PBackgroundIDBCursor(OpenCursorParams params); + async PBackgroundIDBCursor(int64_t requestId, OpenCursorParams params); - async PBackgroundIDBRequest(RequestParams params); + async PBackgroundIDBRequest(int64_t requestId, RequestParams params); child: async __delete__(); diff --git a/dom/indexedDB/PBackgroundIDBVersionChangeTransaction.ipdl b/dom/indexedDB/PBackgroundIDBVersionChangeTransaction.ipdl index e139f0f1c3..c096c28996 100644 --- a/dom/indexedDB/PBackgroundIDBVersionChangeTransaction.ipdl +++ b/dom/indexedDB/PBackgroundIDBVersionChangeTransaction.ipdl @@ -36,9 +36,9 @@ parent: async DeleteIndex(int64_t objectStoreId, int64_t indexId); async RenameIndex(int64_t objectStoreId, int64_t indexId, nsString name); - async PBackgroundIDBCursor(OpenCursorParams params); + async PBackgroundIDBCursor(int64_t requestId, OpenCursorParams params); - async PBackgroundIDBRequest(RequestParams params); + async PBackgroundIDBRequest(int64_t requestId, RequestParams params); child: async __delete__(); diff --git a/dom/indexedDB/SchemaUpgrades.cpp b/dom/indexedDB/SchemaUpgrades.cpp index 234c4c9f04..17230e247f 100644 --- a/dom/indexedDB/SchemaUpgrades.cpp +++ b/dom/indexedDB/SchemaUpgrades.cpp @@ -2863,7 +2863,7 @@ nsresult UpgradeFileIdsFunction::Init(nsIFile* aFMDirectory, auto fileManager = MakeSafeRefPtr<DatabaseFileManager>( PERSISTENCE_TYPE_INVALID, quota::OriginMetadata{}, /* aDatabaseName */ u""_ns, /* aDatabaseID */ ""_ns, - /* aEnforcingQuota */ false, + /* aDatabaseFilePath */ u""_ns, /* aEnforcingQuota */ false, /* aIsInPrivateBrowsingMode */ false); nsresult rv = fileManager->Init(aFMDirectory, aConnection); diff --git a/dom/indexedDB/SerializationHelpers.h b/dom/indexedDB/SerializationHelpers.h index 488c1eb044..a79c6cefb4 100644 --- a/dom/indexedDB/SerializationHelpers.h +++ b/dom/indexedDB/SerializationHelpers.h @@ -31,10 +31,12 @@ struct ParamTraits<mozilla::dom::indexedDB::Key> { static void Write(MessageWriter* aWriter, const paramType& aParam) { WriteParam(aWriter, aParam.mBuffer); + WriteParam(aWriter, aParam.mAutoIncrementKeyOffsets); } static bool Read(MessageReader* aReader, paramType* aResult) { - return ReadParam(aReader, &aResult->mBuffer); + return ReadParam(aReader, &aResult->mBuffer) && + ReadParam(aReader, &aResult->mAutoIncrementKeyOffsets); } }; @@ -72,6 +74,13 @@ struct ParamTraits<mozilla::dom::IDBTransaction::Mode> mozilla::dom::IDBTransaction::Mode::ReadOnly, mozilla::dom::IDBTransaction::Mode::Invalid> {}; +template <> +struct ParamTraits<mozilla::dom::IDBTransaction::Durability> + : public ContiguousEnumSerializer< + mozilla::dom::IDBTransaction::Durability, + mozilla::dom::IDBTransaction::Durability::Default, + mozilla::dom::IDBTransaction::Durability::Invalid> {}; + } // namespace IPC #endif // mozilla_dom_indexeddb_serializationhelpers_h__ diff --git a/dom/indexedDB/test/mochitest-common.toml b/dom/indexedDB/test/mochitest-common.toml index d42228da45..0959f36e13 100644 --- a/dom/indexedDB/test/mochitest-common.toml +++ b/dom/indexedDB/test/mochitest-common.toml @@ -73,6 +73,7 @@ support-files = [ "unit/test_objectStore_remove_values.js", "unit/test_object_identity.js", "unit/test_odd_result_order.js", + "unit/test_open_and_databases.js", "unit/test_open_empty_db.js", "unit/test_open_for_principal.js", "unit/test_open_objectStore.js", @@ -299,6 +300,8 @@ skip-if = [ ["test_odd_result_order.html"] +["test_open_and_databases.html"] + ["test_open_empty_db.html"] ["test_open_for_principal.html"] diff --git a/dom/indexedDB/test/test_open_and_databases.html b/dom/indexedDB/test/test_open_and_databases.html new file mode 100644 index 0000000000..9d0113136e --- /dev/null +++ b/dom/indexedDB/test/test_open_and_databases.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Indexed Database Property Test</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + + <script type="text/javascript" src="unit/test_open_and_databases.js"></script> + <script type="text/javascript" src="helpers.js"></script> + +</head> + +<body onload="runTest();"></body> + +</html> diff --git a/dom/indexedDB/test/test_third_party.html b/dom/indexedDB/test/test_third_party.html index ee90de8dac..439f50d754 100644 --- a/dom/indexedDB/test/test_third_party.html +++ b/dom/indexedDB/test/test_third_party.html @@ -25,12 +25,12 @@ { host: "http://sub1.test2.example.org:8000", cookieBehavior: BEHAVIOR_REJECT, expectedResultFrame1: false, expectedResultFrame2: false }, { host: "http://" + window.location.host, cookieBehavior: BEHAVIOR_REJECTFOREIGN, expectedResultFrame1: true, expectedResultFrame2: true }, - { host: "http://example.com", cookieBehavior: BEHAVIOR_REJECTFOREIGN, expectedResultFrame1: false, expectedResultFrame2: true }, - { host: "http://sub1.test2.example.org:8000", cookieBehavior: BEHAVIOR_REJECTFOREIGN, expectedResultFrame1: false, expectedResultFrame2: true }, + { host: "http://example.com", cookieBehavior: BEHAVIOR_REJECTFOREIGN, expectedResultFrame1: false, expectedResultFrame2: false }, + { host: "http://sub1.test2.example.org:8000", cookieBehavior: BEHAVIOR_REJECTFOREIGN, expectedResultFrame1: false, expectedResultFrame2: false }, { host: "http://" + window.location.host, cookieBehavior: BEHAVIOR_LIMITFOREIGN, expectedResultFrame1: true, expectedResultFrame2: true }, - { host: "http://example.com", cookieBehavior: BEHAVIOR_LIMITFOREIGN, expectedResultFrame1: false, expectedResultFrame2: true }, - { host: "http://sub1.test2.example.org:8000", cookieBehavior: BEHAVIOR_LIMITFOREIGN, expectedResultFrame1: false, expectedResultFrame2: true }, + { host: "http://example.com", cookieBehavior: BEHAVIOR_LIMITFOREIGN, expectedResultFrame1: false, expectedResultFrame2: false }, + { host: "http://sub1.test2.example.org:8000", cookieBehavior: BEHAVIOR_LIMITFOREIGN, expectedResultFrame1: false, expectedResultFrame2: false }, ]; const iframe1Path = diff --git a/dom/indexedDB/test/unit/test_connection_idle_maintenance.js b/dom/indexedDB/test/unit/test_connection_idle_maintenance.js index 288f656c65..819b0b61bb 100644 --- a/dom/indexedDB/test/unit/test_connection_idle_maintenance.js +++ b/dom/indexedDB/test/unit/test_connection_idle_maintenance.js @@ -8,7 +8,7 @@ async function testSteps() { // A constant used to deal with small decrease in usage when transactions are // transferred from the WAL file back into the original database, also called // as checkpointing. - const cosmologicalConstant = 31768; + const cosmologicalConstant = 20000; // The length of time that database connections will be held open after all // transactions have completed before doing idle maintenance. diff --git a/dom/indexedDB/test/unit/test_connection_idle_maintenance_stop.js b/dom/indexedDB/test/unit/test_connection_idle_maintenance_stop.js index 33dc69b210..5453e4dc4e 100644 --- a/dom/indexedDB/test/unit/test_connection_idle_maintenance_stop.js +++ b/dom/indexedDB/test/unit/test_connection_idle_maintenance_stop.js @@ -8,7 +8,7 @@ async function testSteps() { // A constant used to deal with small decrease in usage when transactions are // transferred from the WAL file back into the original database, also called // as checkpointing. - const cosmologicalConstant = 32768; + const cosmologicalConstant = 35000; // The maximum number of threads that can be used for database activity at a // single time. diff --git a/dom/indexedDB/test/unit/test_open_and_databases.js b/dom/indexedDB/test/unit/test_open_and_databases.js new file mode 100644 index 0000000000..b13cba067a --- /dev/null +++ b/dom/indexedDB/test/unit/test_open_and_databases.js @@ -0,0 +1,76 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* exported testSteps */ +async function testSteps() { + const openInfos = [ + { name: "foo-a", version: 1 }, + { name: "foo-b", version: 1 }, + ]; + + info("Creating databases"); + + for (let index = 0; index < openInfos.length; index++) { + const openInfo = openInfos[index]; + + const request = indexedDB.open(openInfo.name, openInfo.version); + + await expectingUpgrade(request); + + const event = await expectingSuccess(request); + + const database = event.target.result; + + database.close(); + } + + info("Getting databases"); + + const databasesPromise = indexedDB.databases(); + + info("Opening databases"); + + const openPromises = []; + + for (let index = 0; index < openInfos.length; index++) { + const openInfo = openInfos[index]; + + const request = indexedDB.open(openInfo.name, openInfo.version); + + const promise = expectingSuccess(request); + + openPromises.push(promise); + } + + info("Waiting for databases operation to complete"); + + const databaseInfos = await databasesPromise; + + info("Verifying databases"); + + is( + databaseInfos.length, + openInfos.length, + "The result of databases() should contain one result per database" + ); + + for (let index = 0; index < openInfos.length; index++) { + const openInfo = openInfos[index]; + + ok( + databaseInfos.some(function (element) { + return ( + element.name === openInfo.name && element.version === openInfo.version + ); + }), + "The result of databases() should be a sequence of the correct names " + + "and versions of all databases for the origin" + ); + } + + info("Waiting for open operations to complete"); + + await Promise.all(openPromises); +} diff --git a/dom/indexedDB/test/unit/xpcshell-head-parent-process.js b/dom/indexedDB/test/unit/xpcshell-head-parent-process.js index f6bd2608ef..840eef7908 100644 --- a/dom/indexedDB/test/unit/xpcshell-head-parent-process.js +++ b/dom/indexedDB/test/unit/xpcshell-head-parent-process.js @@ -56,7 +56,11 @@ if (!this.runTest) { if (testSteps.constructor.name === "AsyncFunction") { // Do run our existing cleanup function that would normally be called by // the generator's call to finishTest(). - registerCleanupFunction(resetTesting); + registerCleanupFunction(function () { + if (SpecialPowers.isMainProcess()) { + resetTesting(); + } + }); add_task(testSteps); @@ -644,7 +648,7 @@ var SpecialPowers = { clearUserPref(prefName) { Services.prefs.clearUserPref(prefName); }, - // Copied (and slightly adjusted) from testing/specialpowers/content/SpecialPowersAPI.jsm + // Copied (and slightly adjusted) from testing/specialpowers/api.js exactGC(callback) { let count = 0; diff --git a/dom/indexedDB/test/unit/xpcshell-shared.toml b/dom/indexedDB/test/unit/xpcshell-shared.toml index ac1183ab2d..f967236a9e 100644 --- a/dom/indexedDB/test/unit/xpcshell-shared.toml +++ b/dom/indexedDB/test/unit/xpcshell-shared.toml @@ -111,6 +111,8 @@ skip-if = ["os == 'android'"] # bug 864843 ["test_odd_result_order.js"] +["test_open_and_databases.js"] + ["test_open_empty_db.js"] ["test_open_for_principal.js"] diff --git a/dom/interfaces/base/nsIBrowser.idl b/dom/interfaces/base/nsIBrowser.idl index 499580240a..3046e167e0 100644 --- a/dom/interfaces/base/nsIBrowser.idl +++ b/dom/interfaces/base/nsIBrowser.idl @@ -145,5 +145,5 @@ interface nsIBrowser : nsISupports * If this method returns `true`, Gecko will assume frontend handled resuming * the load, and will not attempt to resume the load itself. */ - bool finishChangeRemoteness(in uint64_t aPendingSwitchId); + boolean finishChangeRemoteness(in uint64_t aPendingSwitchId); }; diff --git a/dom/interfaces/base/nsIDOMWindowUtils.idl b/dom/interfaces/base/nsIDOMWindowUtils.idl index 6a0df1b435..27b9ff5acf 100644 --- a/dom/interfaces/base/nsIDOMWindowUtils.idl +++ b/dom/interfaces/base/nsIDOMWindowUtils.idl @@ -1112,7 +1112,7 @@ interface nsIDOMWindowUtils : nsISupports { /** * Returns true if a flush of the given type is needed. */ - bool needsFlush(in long aFlushtype); + boolean needsFlush(in long aFlushtype); /** * Flush pending layout-type notification without flushing throttled @@ -1627,14 +1627,14 @@ interface nsIDOMWindowUtils : nsISupports { * Reports whether the current state is test-controlled refreshes * (see advanceTimeAndRefresh and restoreNormalRefresh above). */ - readonly attribute bool isTestControllingRefreshes; + readonly attribute boolean isTestControllingRefreshes; /** * Reports whether APZ is enabled on the widget that this window is attached * to. If there is no widget it will report the default platform value of * whether or not APZ is enabled. */ - readonly attribute bool asyncPanZoomEnabled; + readonly attribute boolean asyncPanZoomEnabled; /** * Set async scroll offset on an element. The next composite will render @@ -1659,7 +1659,7 @@ interface nsIDOMWindowUtils : nsISupports { * false, an error occurred or a flush is not needed, and the notification * will not fire. This is intended to be used by test code only! */ - bool flushApzRepaints(); + boolean flushApzRepaints(); /** * Sets a flag on the element to forcibly disable APZ on it. This affects @@ -1843,7 +1843,7 @@ interface nsIDOMWindowUtils : nsISupports { */ void disableDialogs(); void enableDialogs(); - bool areDialogsEnabled(); + boolean areDialogsEnabled(); void resetDialogAbuseState(); const unsigned long AGENT_SHEET = 0; @@ -1949,7 +1949,7 @@ interface nsIDOMWindowUtils : nsISupports { * it would mark the document with the ChromeOnly userHasInteracted * property. */ - bool isKeyboardEventUserActivity(in Event aKeyboardEvent); + boolean isKeyboardEventUserActivity(in Event aKeyboardEvent); /** * Get the content- and compositor-side APZ test data instances. @@ -2008,7 +2008,7 @@ interface nsIDOMWindowUtils : nsISupports { * * May throw NS_ERROR_NOT_AVAILABLE. */ - readonly attribute bool refreshDriverHasPendingTick; + readonly attribute boolean refreshDriverHasPendingTick; /** * Controls the amount of chrome that should be visible on each side of @@ -2061,7 +2061,7 @@ interface nsIDOMWindowUtils : nsISupports { * * @param aSheetType One of the nsIStyleSheetService.*_SHEET constants. */ - bool hasRuleProcessorUsedByMultipleStyleSets(in unsigned long aSheetType); + boolean hasRuleProcessorUsedByMultipleStyleSets(in unsigned long aSheetType); /** * Enable or disable displayport suppression. This is intended to be used by @@ -2207,7 +2207,7 @@ interface nsIDOMWindowUtils : nsISupports { * * Throws if there's no document or the property is invalid. */ - bool isCssPropertyRecordedInUseCounter(in ACString aProperty); + boolean isCssPropertyRecordedInUseCounter(in ACString aProperty); /** * Calls SetInitialViewport on the MobileViewportManager, which effectively @@ -2216,7 +2216,7 @@ interface nsIDOMWindowUtils : nsISupports { */ void resetMobileViewportManager(); - bool isCoepCredentialless(); + boolean isCoepCredentialless(); /** * Change the DPI setting for the primary monitor. @@ -2279,7 +2279,7 @@ interface nsIDOMWindowUtils : nsISupports { unsigned long long getLayersId(); // Returns true if we are effectively throttling frame requests. - readonly attribute bool effectivelyThrottlesFrameRequests; + readonly attribute boolean effectivelyThrottlesFrameRequests; // Returns the ID for the underlying window widget, which can // be compared against the rawId from a nsIMediaDevice to determine @@ -2291,7 +2291,7 @@ interface nsIDOMWindowUtils : nsISupports { readonly attribute AString webrtcRawDeviceId; // Used for testing to check the suspend status. - readonly attribute bool suspendedByBrowsingContextGroup; + readonly attribute boolean suspendedByBrowsingContextGroup; // Whether there's any scroll-linked effect in this document. This is only // meaningful after the script in question tried to mutate something in a @@ -2299,7 +2299,7 @@ interface nsIDOMWindowUtils : nsISupports { // // See https://firefox-source-docs.mozilla.org/performance/scroll-linked_effects.html // about scroll-linked effects. - readonly attribute bool hasScrollLinkedEffect; + readonly attribute boolean hasScrollLinkedEffect; // Returns the current orientation lock value in browsing context. // This value is defined in hal/HalScreenConfiguration.h diff --git a/dom/interfaces/base/nsIServiceWorkerManager.idl b/dom/interfaces/base/nsIServiceWorkerManager.idl index d89da1ac9c..05614c74c3 100644 --- a/dom/interfaces/base/nsIServiceWorkerManager.idl +++ b/dom/interfaces/base/nsIServiceWorkerManager.idl @@ -31,7 +31,7 @@ interface nsIServiceWorkerUnregisterCallback : nsISupports { // aState is true if the unregistration succeded. // It's false if this ServiceWorkerRegistration doesn't exist. - void unregisterSucceeded(in bool aState); + void unregisterSucceeded(in boolean aState); void unregisterFailed(); }; @@ -61,7 +61,7 @@ interface nsIServiceWorkerInfo : nsISupports // Return whether the ServiceWorker has a "fetch" event listener. Throws if // this is unknown because the worker's main script hasn't finished executing // (when exposed as evaluatingWorker). - readonly attribute bool handlesFetchEvents; + readonly attribute boolean handlesFetchEvents; readonly attribute PRTime installedTime; readonly attribute PRTime activatedTime; @@ -250,8 +250,8 @@ interface nsIServiceWorkerManager : nsISupports nsIServiceWorkerRegistrationInfo getRegistrationByPrincipal(in nsIPrincipal aPrincipal, in AString aScope); - [notxpcom, nostdcall] bool StartControlling(in const_ClientInfoRef aClientInfo, - in const_ServiceWorkerDescriptorRef aServiceWorker); + [notxpcom, nostdcall] boolean StartControlling(in const_ClientInfoRef aClientInfo, + in const_ServiceWorkerDescriptorRef aServiceWorker); // Testing AString getScopeForUrl(in nsIPrincipal aPrincipal, in AString aPath); diff --git a/dom/interfaces/base/nsITextInputProcessorCallback.idl b/dom/interfaces/base/nsITextInputProcessorCallback.idl index 8cee842108..2464d175c1 100644 --- a/dom/interfaces/base/nsITextInputProcessorCallback.idl +++ b/dom/interfaces/base/nsITextInputProcessorCallback.idl @@ -73,7 +73,7 @@ interface nsITextInputProcessorNotification : nsISupports * This is true if selection has a range. Otherwise, i.e., there is no * range such as after calling Selection.removeAllRanges, this is false. */ - readonly attribute bool hasRange; + readonly attribute boolean hasRange; /** * Be careful, line breakers in the editor are treated as native line diff --git a/dom/interfaces/payments/nsIPaymentActionResponse.idl b/dom/interfaces/payments/nsIPaymentActionResponse.idl index 7ff24c3b7f..c4a7ff1943 100644 --- a/dom/interfaces/payments/nsIPaymentActionResponse.idl +++ b/dom/interfaces/payments/nsIPaymentActionResponse.idl @@ -176,14 +176,14 @@ interface nsIPaymentCanMakeActionResponse : nsIPaymentActionResponse /** * The result of canMakePayment action. */ - readonly attribute bool result; + readonly attribute boolean result; /** * The initial method. * @param aRequestId - the request identifier of the payment request. * @param aResult - the canMakePayment result. */ - void init(in AString aRequestId, in bool aResult); + void init(in AString aRequestId, in boolean aResult); }; /** @@ -266,7 +266,7 @@ interface nsIPaymentAbortActionResponse : nsIPaymentActionResponse /** * Check if the abort action is succeeded */ - bool isSucceeded(); + boolean isSucceeded(); }; [builtinclass, scriptable, uuid(62c01e69-9ca4-4060-99e4-b95f628c8e6d)] @@ -289,7 +289,7 @@ interface nsIPaymentCompleteActionResponse : nsIPaymentActionResponse /** * Check if the complete action is succeeded. */ - bool isCompleted(); + boolean isCompleted(); }; [builtinclass, scriptable, uuid(2035e0a9-c9ab-4c9f-b8e9-28b2ed61548c)] diff --git a/dom/interfaces/push/moz.build b/dom/interfaces/push/moz.build index 4fbeff1b05..f84c00a61b 100644 --- a/dom/interfaces/push/moz.build +++ b/dom/interfaces/push/moz.build @@ -5,7 +5,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. with Files("**"): - BUG_COMPONENT = ("Core", "DOM: Notifications") + BUG_COMPONENT = ("Core", "DOM: Push Subscriptions") XPIDL_SOURCES += [ "nsIPushErrorReporter.idl", diff --git a/dom/interfaces/push/nsIPushService.idl b/dom/interfaces/push/nsIPushService.idl index b41d7a5502..371c763751 100644 --- a/dom/interfaces/push/nsIPushService.idl +++ b/dom/interfaces/push/nsIPushService.idl @@ -18,11 +18,11 @@ interface nsIPushSubscription : nsISupports readonly attribute long long pushCount; readonly attribute long long lastPush; readonly attribute long quota; - readonly attribute bool isSystemSubscription; + readonly attribute boolean isSystemSubscription; readonly attribute jsval p256dhPrivateKey; - bool quotaApplies(); - bool isExpired(); + boolean quotaApplies(); + boolean isExpired(); Array<uint8_t> getKey(in AString name); }; @@ -50,7 +50,7 @@ interface nsIPushSubscription : nsISupports [scriptable, uuid(d574118f-61a9-4270-b1f6-4461aa85c4f5), function] interface nsIUnsubscribeResultCallback : nsISupports { - void onUnsubscribe(in nsresult status, in bool success); + void onUnsubscribe(in nsresult status, in boolean success); }; /** diff --git a/dom/interfaces/security/nsIContentSecurityPolicy.idl b/dom/interfaces/security/nsIContentSecurityPolicy.idl index fdb99ceb26..26f00a0220 100644 --- a/dom/interfaces/security/nsIContentSecurityPolicy.idl +++ b/dom/interfaces/security/nsIContentSecurityPolicy.idl @@ -91,7 +91,7 @@ interface nsIContentSecurityPolicy : nsISerializable * Please note that upgrade-insecure-reqeusts also applies if the parent or * including document (context) makes use of the directive. */ - readonly attribute bool upgradeInsecureRequests; + readonly attribute boolean upgradeInsecureRequests; /** * Returns whether this policy uses the directive block-all-mixed-content. @@ -100,12 +100,12 @@ interface nsIContentSecurityPolicy : nsISerializable * will therefore block all mixed content without even trying to perform * an upgrade. */ - readonly attribute bool blockAllMixedContent; + readonly attribute boolean blockAllMixedContent; /** * Returns whether this policy enforces the frame-ancestors directive. */ - readonly attribute bool enforcesFrameAncestors; + readonly attribute boolean enforcesFrameAncestors; /** * Parse and install a CSP policy. @@ -144,7 +144,7 @@ interface nsIContentSecurityPolicy : nsISerializable * (block the rules if false). */ boolean getAllowsInline(in nsIContentSecurityPolicy_CSPDirective aDirective, - in bool aHasUnsafeHash, + in boolean aHasUnsafeHash, in AString aNonce, in boolean aParserCreated, in Element aTriggeringElement, @@ -241,7 +241,7 @@ interface nsIContentSecurityPolicy : nsISerializable [must_use] void setRequestContextWithDocument(in Document aDocument); [must_use] void setRequestContextWithPrincipal(in nsIPrincipal aRequestPrincipal, in nsIURI aSelfURI, - in AString aReferrer, + in ACString aReferrer, in unsigned long long aInnerWindowId); /** @@ -249,7 +249,7 @@ interface nsIContentSecurityPolicy : nsISerializable */ [noscript, notxpcom, nostdcall] readonly attribute nsIPrincipal requestPrincipal; [noscript, notxpcom, nostdcall] readonly attribute nsIURI selfURI; - [noscript] readonly attribute AString referrer; + [noscript] readonly attribute ACString referrer; [noscript, notxpcom, nostdcall] readonly attribute unsigned long long innerWindowID; /** @@ -328,7 +328,7 @@ interface nsIContentSecurityPolicy : nsISerializable in nsILoadInfo aLoadInfo, in nsIURI aContentLocation, in nsIURI aOriginalURIIfRedirect, - in bool aSendViolationReports); + in boolean aSendViolationReports); %{ C++ // nsIObserver topic to fire when the policy encounters a violation. diff --git a/dom/interfaces/security/nsIReferrerInfo.idl b/dom/interfaces/security/nsIReferrerInfo.idl index 07e2f603bc..fc8b39465d 100644 --- a/dom/interfaces/security/nsIReferrerInfo.idl +++ b/dom/interfaces/security/nsIReferrerInfo.idl @@ -108,7 +108,7 @@ interface nsIReferrerInfo : nsISerializable /** * Indicates if the referrer should not be sent or not even when it's available. */ - readonly attribute AString computedReferrerSpec; + readonly attribute ACString computedReferrerSpec; /** * Get the computed referrer, if one has been set. The computed referrer is diff --git a/dom/interfaces/storage/nsIDOMStorageManager.idl b/dom/interfaces/storage/nsIDOMStorageManager.idl index 1be7525e4a..e3e3ed4659 100644 --- a/dom/interfaces/storage/nsIDOMStorageManager.idl +++ b/dom/interfaces/storage/nsIDOMStorageManager.idl @@ -61,7 +61,7 @@ interface nsIDOMStorageManager : nsISupports in nsIPrincipal aPrincipal, in nsIPrincipal aStoragePrincipal, in AString aDocumentURI, - [optional] in bool aPrivate); + [optional] in boolean aPrivate); /** * DEPRECATED. The only good reason to use this was if you were writing a * test and wanted to hackily determine if a preload happened. That's now @@ -85,7 +85,7 @@ interface nsIDOMStorageManager : nsISupports Storage getStorage(in mozIDOMWindow aWindow, in nsIPrincipal aPrincipal, in nsIPrincipal aStoragePrincipal, - [optional] in bool aPrivate); + [optional] in boolean aPrivate); /** * Clones given storage into this storage manager. @@ -112,8 +112,8 @@ interface nsIDOMStorageManager : nsISupports * by this storage manager. * false otherwise */ - bool checkStorage(in nsIPrincipal aPrincipal, - in Storage aStorage); + boolean checkStorage(in nsIPrincipal aPrincipal, + in Storage aStorage); }; [uuid(b3bfbbd0-e738-4cbf-b0f0-b65f25265e82)] diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp index bdd10fdcb2..3d1f399edb 100644 --- a/dom/ipc/BrowserChild.cpp +++ b/dom/ipc/BrowserChild.cpp @@ -2342,7 +2342,7 @@ mozilla::ipc::IPCResult BrowserChild::RecvPrintPreview( /* aListener = */ nullptr, docShellToCloneInto, nsGlobalWindowOuter::IsPreview::Yes, nsGlobalWindowOuter::IsForWindowDotPrint::No, - std::move(aCallback), IgnoreErrors()); + std::move(aCallback), nullptr, IgnoreErrors()); #endif return IPC_OK(); } @@ -2359,8 +2359,9 @@ mozilla::ipc::IPCResult BrowserChild::RecvExitPrintPreview() { return IPC_OK(); } -mozilla::ipc::IPCResult BrowserChild::RecvPrint( - const MaybeDiscardedBrowsingContext& aBc, const PrintData& aPrintData) { +mozilla::ipc::IPCResult BrowserChild::CommonPrint( + const MaybeDiscardedBrowsingContext& aBc, const PrintData& aPrintData, + RefPtr<BrowsingContext>* aCachedBrowsingContext) { #ifdef NS_PRINTING if (NS_WARN_IF(aBc.IsNullOrDiscarded())) { return IPC_OK(); @@ -2389,12 +2390,12 @@ mozilla::ipc::IPCResult BrowserChild::RecvPrint( IgnoredErrorResult rv; RefPtr printJob = static_cast<RemotePrintJobChild*>( aPrintData.remotePrintJob().AsChild()); - outerWindow->Print(printSettings, printJob, - /* aListener = */ nullptr, - /* aWindowToCloneInto = */ nullptr, - nsGlobalWindowOuter::IsPreview::No, - nsGlobalWindowOuter::IsForWindowDotPrint::No, - /* aPrintPreviewCallback = */ nullptr, rv); + outerWindow->Print( + printSettings, printJob, + /* aListener = */ nullptr, + /* aWindowToCloneInto = */ nullptr, nsGlobalWindowOuter::IsPreview::No, + nsGlobalWindowOuter::IsForWindowDotPrint::No, + /* aPrintPreviewCallback = */ nullptr, aCachedBrowsingContext, rv); if (NS_WARN_IF(rv.Failed())) { return IPC_OK(); } @@ -2403,6 +2404,49 @@ mozilla::ipc::IPCResult BrowserChild::RecvPrint( return IPC_OK(); } +mozilla::ipc::IPCResult BrowserChild::RecvPrint( + const MaybeDiscardedBrowsingContext& aBc, const PrintData& aPrintData, + bool aReturnStaticClone, PrintResolver&& aResolve) { +#ifdef NS_PRINTING + RefPtr<BrowsingContext> browsingContext; + auto result = CommonPrint(aBc, aPrintData, + aReturnStaticClone ? &browsingContext : nullptr); + aResolve(browsingContext); + return result; +#else + aResolve(nullptr); + return IPC_OK(); +#endif +} + +mozilla::ipc::IPCResult BrowserChild::RecvPrintClonedPage( + const MaybeDiscardedBrowsingContext& aBc, const PrintData& aPrintData, + const MaybeDiscardedBrowsingContext& aClonedBc) { +#ifdef NS_PRINTING + if (aClonedBc.IsNullOrDiscarded()) { + return IPC_OK(); + } + RefPtr<BrowsingContext> clonedBc = aClonedBc.get(); + return CommonPrint(aBc, aPrintData, &clonedBc); +#else + return IPC_OK(); +#endif +} + +mozilla::ipc::IPCResult BrowserChild::RecvDestroyPrintClone( + const MaybeDiscardedBrowsingContext& aCachedPage) { +#ifdef NS_PRINTING + if (aCachedPage) { + RefPtr<nsPIDOMWindowOuter> window = aCachedPage->GetDOMWindow(); + if (NS_WARN_IF(!window)) { + return IPC_OK(); + } + window->Close(); + } +#endif + return IPC_OK(); +} + mozilla::ipc::IPCResult BrowserChild::RecvUpdateNativeWindowHandle( const uintptr_t& aNewHandle) { #if defined(XP_WIN) && defined(ACCESSIBILITY) diff --git a/dom/ipc/BrowserChild.h b/dom/ipc/BrowserChild.h index ddbf8f0b74..90cb712476 100644 --- a/dom/ipc/BrowserChild.h +++ b/dom/ipc/BrowserChild.h @@ -507,7 +507,15 @@ class BrowserChild final : public nsMessageManagerScriptExecutor, mozilla::ipc::IPCResult RecvExitPrintPreview(); MOZ_CAN_RUN_SCRIPT_BOUNDARY mozilla::ipc::IPCResult RecvPrint( - const MaybeDiscardedBrowsingContext&, const PrintData&); + const MaybeDiscardedBrowsingContext&, const PrintData&, bool, + PrintResolver&&); + + MOZ_CAN_RUN_SCRIPT_BOUNDARY mozilla::ipc::IPCResult RecvPrintClonedPage( + const MaybeDiscardedBrowsingContext&, const PrintData&, + const MaybeDiscardedBrowsingContext&); + + mozilla::ipc::IPCResult RecvDestroyPrintClone( + const MaybeDiscardedBrowsingContext&); mozilla::ipc::IPCResult RecvUpdateNativeWindowHandle( const uintptr_t& aNewHandle); @@ -712,6 +720,11 @@ class BrowserChild final : public nsMessageManagerScriptExecutor, void InternalSetDocShellIsActive(bool aIsActive); + MOZ_CAN_RUN_SCRIPT + mozilla::ipc::IPCResult CommonPrint( + const MaybeDiscardedBrowsingContext& aBc, const PrintData& aPrintData, + RefPtr<BrowsingContext>* aCachedBrowsingContext); + bool CreateRemoteLayerManager( mozilla::layers::PCompositorBridgeChild* aCompositorChild); diff --git a/dom/ipc/BrowserParent.cpp b/dom/ipc/BrowserParent.cpp index 5d13d2e814..23c677c09c 100644 --- a/dom/ipc/BrowserParent.cpp +++ b/dom/ipc/BrowserParent.cpp @@ -826,8 +826,6 @@ void BrowserParent::ActorDestroy(ActorDestroyReason why) { // out-of-process iframe. RefPtr<nsFrameLoader> frameLoader = GetFrameLoader(true); if (frameLoader) { - ReceiveMessage(CHILD_PROCESS_SHUTDOWN_MESSAGE, false, nullptr); - if (mBrowsingContext->IsTop()) { // If this is a top-level BrowsingContext, tell the frameloader it's time // to go away. Otherwise, this is a subframe crash, and we can keep the diff --git a/dom/ipc/CoalescedTouchData.cpp b/dom/ipc/CoalescedTouchData.cpp index 3c3bc71a0c..551e6ae901 100644 --- a/dom/ipc/CoalescedTouchData.cpp +++ b/dom/ipc/CoalescedTouchData.cpp @@ -25,8 +25,7 @@ void CoalescedTouchData::CreateCoalescedTouchEvent( WidgetPointerEvent* event = touch->mCoalescedWidgetEvents->mEvents.AppendElement(WidgetPointerEvent( aEvent.IsTrusted(), ePointerMove, aEvent.mWidget)); - PointerEventHandler::InitPointerEventFromTouch(*event, aEvent, *touch, - i == 0); + PointerEventHandler::InitPointerEventFromTouch(*event, aEvent, *touch); event->mFlags.mBubbles = false; event->mFlags.mCancelable = false; } @@ -63,8 +62,7 @@ void CoalescedTouchData::Coalesce(const WidgetTouchEvent& aEvent, sameTouch->mCoalescedWidgetEvents->mEvents.AppendElement( WidgetPointerEvent(aEvent.IsTrusted(), ePointerMove, aEvent.mWidget)); - PointerEventHandler::InitPointerEventFromTouch(*event, aEvent, *touch, - i == 0); + PointerEventHandler::InitPointerEventFromTouch(*event, aEvent, *touch); event->mFlags.mBubbles = false; event->mFlags.mCancelable = false; } diff --git a/dom/ipc/ContentChild.cpp b/dom/ipc/ContentChild.cpp index 7507d88fe0..2ebdd303d8 100644 --- a/dom/ipc/ContentChild.cpp +++ b/dom/ipc/ContentChild.cpp @@ -2310,8 +2310,10 @@ mozilla::ipc::IPCResult ContentChild::RecvThemeChanged( mozilla::ipc::IPCResult ContentChild::RecvLoadProcessScript( const nsString& aURL) { auto* global = ContentProcessMessageManager::Get(); - global->LoadScript(aURL); - return IPC_OK(); + if (global && global->LoadScript(aURL)) { + return IPC_OK(); + } + return IPC_FAIL(this, "ContentProcessMessageManager::LoadScript failed"); } mozilla::ipc::IPCResult ContentChild::RecvAsyncMessage( @@ -3923,7 +3925,8 @@ mozilla::ipc::IPCResult ContentChild::RecvRaiseWindow( mozilla::ipc::IPCResult ContentChild::RecvAdjustWindowFocus( const MaybeDiscarded<BrowsingContext>& aContext, bool aIsVisible, - uint64_t aActionId) { + uint64_t aActionId, bool aShouldClearAncestorFocus, + const MaybeDiscarded<BrowsingContext>& aAncestorBrowsingContextToFocus) { if (aContext.IsNullOrDiscarded()) { MOZ_LOG(BrowsingContext::GetLog(), LogLevel::Debug, ("ChildIPC: Trying to send a message to dead or detached context")); @@ -3932,7 +3935,12 @@ mozilla::ipc::IPCResult ContentChild::RecvAdjustWindowFocus( if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { RefPtr<BrowsingContext> bc = aContext.get(); - fm->AdjustInProcessWindowFocus(bc, false, aIsVisible, aActionId); + RefPtr<BrowsingContext> ancestor = + aAncestorBrowsingContextToFocus.IsNullOrDiscarded() + ? nullptr + : aAncestorBrowsingContextToFocus.get(); + fm->AdjustInProcessWindowFocus(bc, false, aIsVisible, aActionId, + aShouldClearAncestorFocus, ancestor); } return IPC_OK(); } diff --git a/dom/ipc/ContentChild.h b/dom/ipc/ContentChild.h index b9ab77ef5b..12b6c3cabf 100644 --- a/dom/ipc/ContentChild.h +++ b/dom/ipc/ContentChild.h @@ -661,7 +661,8 @@ class ContentChild final : public PContentChild, uint64_t aActionId); MOZ_CAN_RUN_SCRIPT_BOUNDARY mozilla::ipc::IPCResult RecvAdjustWindowFocus( const MaybeDiscarded<BrowsingContext>& aContext, bool aIsVisible, - uint64_t aActionId); + uint64_t aActionId, bool aShouldClearFocus, + const MaybeDiscarded<BrowsingContext>& aAncestorBrowsingContextToFocus); MOZ_CAN_RUN_SCRIPT_BOUNDARY mozilla::ipc::IPCResult RecvClearFocus( const MaybeDiscarded<BrowsingContext>& aContext); mozilla::ipc::IPCResult RecvSetFocusedBrowsingContext( diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp index 2dbdb662ff..412b7e8796 100644 --- a/dom/ipc/ContentParent.cpp +++ b/dom/ipc/ContentParent.cpp @@ -644,6 +644,10 @@ static const char* sObserverTopics[] = { DEFAULT_TIMEZONE_CHANGED_OBSERVER_TOPIC, }; +void ContentParent_NotifyUpdatedDictionaries() { + ContentParent::NotifyUpdatedDictionaries(); +} + // PreallocateProcess is called by the PreallocatedProcessManager. // ContentParent then takes this process back within GetNewOrUsedBrowserProcess. /*static*/ already_AddRefed<ContentParent> @@ -1895,10 +1899,6 @@ void ContentParent::ShutDownMessageManager() { return; } - mMessageManager->ReceiveMessage(mMessageManager, nullptr, - CHILD_PROCESS_SHUTDOWN_MESSAGE, false, - nullptr, nullptr, IgnoreErrors()); - mMessageManager->SetOsPid(-1); mMessageManager->Disconnect(); mMessageManager = nullptr; @@ -3273,7 +3273,7 @@ bool ContentParent::InitInternal(ProcessPriority aInitialPriority) { // because content scripts mean that a moz-extension can live in any // process. Same thing for system principal Blob URLs. Content Blob // URL's are sent for content principals on-demand by - // AboutToLoadHttpFtpDocumentForChild and RemoteWorkerManager. + // AboutToLoadHttpDocumentForChild and RemoteWorkerManager. if (!BlobURLProtocolHandler::IsBlobURLBroadcastPrincipal(aPrincipal)) { return true; } @@ -6365,7 +6365,7 @@ void ContentParent::UpdateCookieStatus(nsIChannel* aChannel) { } } -nsresult ContentParent::AboutToLoadHttpFtpDocumentForChild( +nsresult ContentParent::AboutToLoadHttpDocumentForChild( nsIChannel* aChannel, bool* aShouldWaitForPermissionCookieUpdate) { MOZ_ASSERT(aChannel); @@ -7362,7 +7362,8 @@ mozilla::ipc::IPCResult ContentParent::RecvRaiseWindow( mozilla::ipc::IPCResult ContentParent::RecvAdjustWindowFocus( const MaybeDiscarded<BrowsingContext>& aContext, bool aIsVisible, - uint64_t aActionId) { + uint64_t aActionId, bool aShouldClearFocus, + const MaybeDiscarded<BrowsingContext>& aAncestorBrowsingContextToFocus) { if (aContext.IsNullOrDiscarded()) { MOZ_LOG( BrowsingContext::GetLog(), LogLevel::Debug, @@ -7389,7 +7390,9 @@ mozilla::ipc::IPCResult ContentParent::RecvAdjustWindowFocus( ContentParent* cp = cpm->GetContentProcessById( ContentParentId(canonicalParent->OwnerProcessId())); if (cp && !processes.Get(cp)) { - Unused << cp->SendAdjustWindowFocus(context, aIsVisible, aActionId); + Unused << cp->SendAdjustWindowFocus(context, aIsVisible, aActionId, + aShouldClearFocus, + aAncestorBrowsingContextToFocus); processes.InsertOrUpdate(cp, true); } context = canonicalParent; @@ -8092,6 +8095,8 @@ IPCResult ContentParent::RecvRawMessage( stack.emplace(); stack->BorrowFromClonedMessageData(*aStack); } + MMPrinter::Print("ContentParent::RecvRawMessage", aMeta.actorName(), + aMeta.messageName(), aData); ReceiveRawMessage(aMeta, std::move(data), std::move(stack)); return IPC_OK(); } diff --git a/dom/ipc/ContentParent.h b/dom/ipc/ContentParent.h index 15139a93f1..31cfa6de88 100644 --- a/dom/ipc/ContentParent.h +++ b/dom/ipc/ContentParent.h @@ -50,8 +50,6 @@ #include "DriverCrashGuard.h" #include "nsIReferrerInfo.h" -#define CHILD_PROCESS_SHUTDOWN_MESSAGE u"child-process-shutdown"_ns - class nsConsoleService; class nsIContentProcessInfo; class nsICycleCollectorLogSink; @@ -551,12 +549,12 @@ class ContentParent final : public PContentParent, void SetMainThreadQoSPriority(nsIThread::QoSPriority aQoSPriority); // This function is called when we are about to load a document from an - // HTTP(S) or FTP channel for a content process. It is a useful place + // HTTP(S) channel for a content process. It is a useful place // to start to kick off work as early as possible in response to such // document loads. // aShouldWaitForPermissionCookieUpdate is set to true if main thread IPCs for // updating permissions/cookies are sent. - nsresult AboutToLoadHttpFtpDocumentForChild( + nsresult AboutToLoadHttpDocumentForChild( nsIChannel* aChannel, bool* aShouldWaitForPermissionCookieUpdate = nullptr); @@ -618,7 +616,8 @@ class ContentParent final : public PContentParent, uint64_t aActionId); mozilla::ipc::IPCResult RecvAdjustWindowFocus( const MaybeDiscarded<BrowsingContext>& aContext, bool aIsVisible, - uint64_t aActionId); + uint64_t aActionId, bool aShouldClearFocus, + const MaybeDiscarded<BrowsingContext>& aAncestorBrowsingContextToFocus); mozilla::ipc::IPCResult RecvClearFocus( const MaybeDiscarded<BrowsingContext>& aContext); mozilla::ipc::IPCResult RecvSetFocusedBrowsingContext( diff --git a/dom/ipc/ContentParent_NotifyUpdatedDictionaries.h b/dom/ipc/ContentParent_NotifyUpdatedDictionaries.h new file mode 100644 index 0000000000..0c78fb874d --- /dev/null +++ b/dom/ipc/ContentParent_NotifyUpdatedDictionaries.h @@ -0,0 +1,17 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ContentParent_NotifyUpdatedDictionaries_h +#define mozilla_dom_ContentParent_NotifyUpdatedDictionaries_h + +// Avoid including ContentParent.h because it ends up including WebGLTypes.h, +// where we have our mozilla::malloc(ForbidNarrowing<size_t>) overrides, which +// cause issues with the way hunspell overrides malloc for itself. +namespace mozilla::dom { +void ContentParent_NotifyUpdatedDictionaries(); +} // namespace mozilla::dom + +#endif // mozilla_dom_ContentParent_NotifyUpdatedDictionaries_h diff --git a/dom/ipc/DOMTypes.ipdlh b/dom/ipc/DOMTypes.ipdlh index faee4e861c..1be3537e68 100644 --- a/dom/ipc/DOMTypes.ipdlh +++ b/dom/ipc/DOMTypes.ipdlh @@ -114,6 +114,7 @@ struct ScreenDetails { ScreenOrientation orientation; uint16_t orientationAngle; bool isPseudoDisplay; + bool isHDR; }; struct DimensionInfo diff --git a/dom/ipc/MMPrinter.cpp b/dom/ipc/MMPrinter.cpp index be2911341a..796b4fbeb3 100644 --- a/dom/ipc/MMPrinter.cpp +++ b/dom/ipc/MMPrinter.cpp @@ -26,8 +26,8 @@ LazyLogModule MMPrinter::sMMLog("MessageManager"); // the output of the logs into logs more friendly to reading. /* static */ -void MMPrinter::PrintImpl(char const* aLocation, const nsAString& aMsg, - ClonedMessageData const& aData) { +Maybe<uint64_t> MMPrinter::PrintHeader(char const* aLocation, + const nsAString& aMsg) { NS_ConvertUTF16toUTF8 charMsg(aMsg); /* @@ -43,15 +43,29 @@ void MMPrinter::PrintImpl(char const* aLocation, const nsAString& aMsg, char* mmSkipLog = PR_GetEnv("MOZ_LOG_MESSAGEMANAGER_SKIP"); if (mmSkipLog && strstr(mmSkipLog, charMsg.get())) { - return; + return Nothing(); } - uint64_t msg_id = RandomUint64OrDie(); + uint64_t aMsgId = RandomUint64OrDie(); MOZ_LOG(MMPrinter::sMMLog, LogLevel::Debug, - ("%" PRIu64 " %s Message: %s in process type: %s", msg_id, aLocation, + ("%" PRIu64 " %s Message: %s in process type: %s", aMsgId, aLocation, charMsg.get(), XRE_GetProcessTypeString())); + return Some(aMsgId); +} + +/* static */ +void MMPrinter::PrintNoData(uint64_t aMsgId) { + if (!MOZ_LOG_TEST(sMMLog, LogLevel::Verbose)) { + return; + } + MOZ_LOG(MMPrinter::sMMLog, LogLevel::Verbose, + ("%" PRIu64 " (No Data)", aMsgId)); +} + +/* static */ +void MMPrinter::PrintData(uint64_t aMsgId, ClonedMessageData const& aData) { if (!MOZ_LOG_TEST(sMMLog, LogLevel::Verbose)) { return; } @@ -75,8 +89,7 @@ void MMPrinter::PrintImpl(char const* aLocation, const nsAString& aMsg, if (rv.Failed()) { // In testing, the only reason this would fail was if there was no data in // the message; so it seems like this is safe-ish. - MOZ_LOG(MMPrinter::sMMLog, LogLevel::Verbose, - ("%" PRIu64 " (No Data)", msg_id)); + MMPrinter::PrintNoData(aMsgId); rv.SuppressException(); return; } @@ -86,7 +99,7 @@ void MMPrinter::PrintImpl(char const* aLocation, const nsAString& aMsg, if (!srcString.init(cx, unevalObj)) return; MOZ_LOG(MMPrinter::sMMLog, LogLevel::Verbose, - ("%" PRIu64 " %s", msg_id, NS_ConvertUTF16toUTF8(srcString).get())); + ("%" PRIu64 " %s", aMsgId, NS_ConvertUTF16toUTF8(srcString).get())); } } // namespace mozilla::dom diff --git a/dom/ipc/MMPrinter.h b/dom/ipc/MMPrinter.h index 7d89f9ee8f..1d00bfc2e6 100644 --- a/dom/ipc/MMPrinter.h +++ b/dom/ipc/MMPrinter.h @@ -7,6 +7,7 @@ #ifndef MMPrinter_h #define MMPrinter_h +#include "mozilla/Maybe.h" #include "mozilla/dom/DOMTypes.h" #include "nsString.h" @@ -17,14 +18,40 @@ class MMPrinter { static void Print(char const* aLocation, const nsAString& aMsg, ClonedMessageData const& aData) { if (MOZ_UNLIKELY(MOZ_LOG_TEST(MMPrinter::sMMLog, LogLevel::Debug))) { - MMPrinter::PrintImpl(aLocation, aMsg, aData); + Maybe<uint64_t> msgId = MMPrinter::PrintHeader(aLocation, aMsg); + if (!msgId.isSome()) { + return; + } + MMPrinter::PrintData(*msgId, aData); + } + } + + static void Print(char const* aLocation, const nsACString& aActorName, + const nsAString& aMessageName, + const Maybe<ClonedMessageData>& aData) { + if (MOZ_UNLIKELY(MOZ_LOG_TEST(MMPrinter::sMMLog, LogLevel::Debug))) { + Maybe<uint64_t> msgId = MMPrinter::PrintHeader( + aLocation, + NS_ConvertUTF8toUTF16(aActorName + " - "_ns) + aMessageName); + + if (!msgId.isSome()) { + return; + } + + if (aData.isSome()) { + MMPrinter::PrintData(*msgId, *aData); + } else { + MMPrinter::PrintNoData(*msgId); + } } } private: static LazyLogModule sMMLog; - static void PrintImpl(char const* aLocation, const nsAString& aMsg, - ClonedMessageData const& aData); + static Maybe<uint64_t> PrintHeader(char const* aLocation, + const nsAString& aMsg); + static void PrintNoData(uint64_t aMsgId); + static void PrintData(uint64_t aMsgId, ClonedMessageData const& aData); }; } // namespace mozilla::dom diff --git a/dom/ipc/PBrowser.ipdl b/dom/ipc/PBrowser.ipdl index caef472ec2..372a81b139 100644 --- a/dom/ipc/PBrowser.ipdl +++ b/dom/ipc/PBrowser.ipdl @@ -87,7 +87,7 @@ using nsCursor from "nsIWidget.h"; using struct LookAndFeelInt from "mozilla/widget/WidgetMessageUtils.h"; using struct mozilla::DimensionRequest from "mozilla/widget/WidgetMessageUtils.h"; using class mozilla::dom::MessagePort from "mozilla/dom/MessagePort.h"; -using class mozilla::dom::ipc::StructuredCloneData from "mozilla/dom/ipc/StructuredCloneData.h"; +[MoveOnly=data] using class mozilla::dom::ipc::StructuredCloneData from "mozilla/dom/ipc/StructuredCloneData.h"; using mozilla::dom::MaybeDiscardedWindowContext from "mozilla/dom/WindowContext.h"; using mozilla::EventMessage from "mozilla/EventForwards.h"; using nsEventStatus from "mozilla/EventForwards.h"; @@ -960,8 +960,33 @@ child: * * @param aBrowsingContext the browsing context to print. * @param aPrintData the serialized settings to print with + * @param aReturnStaticClone If the document in aBrowsingContext is not a static clone, whether + * to return the static document clone created. + * Note that if you call this with true but do not later call PrintClonedPage(), + * you must call DestroyPrintCache() to avoid leaks. */ - async Print(MaybeDiscardedBrowsingContext aBC, PrintData aPrintData); + async Print(MaybeDiscardedBrowsingContext aBC, PrintData aPrintData, bool aReturnStaticClone) returns(MaybeDiscardedBrowsingContext staticCloneBrowsingContext); + + /** + * Tell the child to print the passed in static clone browsing context with the given settings. + * + * @param aBrowsingContext the browsing context to print. + * @param aPrintData the serialized settings to print with + * @param aStaticCloneBrowsingContext The static clone of aBrowsingContext that + * was created by an earlier call to Print(). This is the page that will actually be + * printed. + */ + async PrintClonedPage(MaybeDiscardedBrowsingContext aBC, PrintData aPrintData, MaybeDiscardedBrowsingContext aStaticCloneBrowsingContext); + + /** + * Destroy the static document clone for printing, if present. See Print() for details. + * For callers' simplicity, it is safe to call this method even if aStaticCloneBrowsingContext + * is null or has already been discarded. + * + * @param aStaticCloneBrowsingContext The static clone that was created by + * an earlier call to Print(). + */ + async DestroyPrintClone(MaybeDiscardedBrowsingContext aStaticCloneBrowsingContext); /** * Update the child with the tab's current top-level native window handle. diff --git a/dom/ipc/PContent.ipdl b/dom/ipc/PContent.ipdl index 3789c046c8..67c4c298e4 100644 --- a/dom/ipc/PContent.ipdl +++ b/dom/ipc/PContent.ipdl @@ -104,7 +104,7 @@ using mozilla::ImagePoint from "Units.h"; using mozilla::ImageIntSize from "Units.h"; using mozilla::widget::ThemeChangeKind from "mozilla/widget/WidgetMessageUtils.h"; using class mozilla::dom::MessagePort from "mozilla/dom/MessagePort.h"; -using class mozilla::dom::ipc::StructuredCloneData from "mozilla/dom/ipc/StructuredCloneData.h"; +[MoveOnly=data] using class mozilla::dom::ipc::StructuredCloneData from "mozilla/dom/ipc/StructuredCloneData.h"; using mozilla::OriginAttributes from "mozilla/ipc/BackgroundUtils.h"; using struct mozilla::layers::TextureFactoryIdentifier from "mozilla/layers/CompositorTypes.h"; using mozilla::layers::CompositorOptions from "mozilla/layers/CompositorOptions.h"; @@ -1874,8 +1874,18 @@ both: async DiscardBrowsingContext(MaybeDiscardedBrowsingContext aContext, bool aDoDiscard) returns (uint64_t unused); + /** + * aContext is the one that's taking the affect. Either the one that + * receives the focus, or the one that loses the focus. + * + * aAncestorBrowsingContextToFocus can only be non-null when + * aShouldClearAncestorFocus is true. This is the browsing context + * that receives the focus when aContext loses the focus. + */ async AdjustWindowFocus(MaybeDiscardedBrowsingContext aContext, - bool aIsVisible, uint64_t aActionId); + bool aIsVisible, uint64_t aActionId, + bool aShouldClearAncestorFocus, + MaybeDiscardedBrowsingContext aAncestorBrowsingContextToFocus); async WindowClose(MaybeDiscardedBrowsingContext aContext, bool aTrustedCaller); async WindowFocus(MaybeDiscardedBrowsingContext aContext, diff --git a/dom/ipc/ProcessIsolation.cpp b/dom/ipc/ProcessIsolation.cpp index f0c6dd8863..ddfbeb3d28 100644 --- a/dom/ipc/ProcessIsolation.cpp +++ b/dom/ipc/ProcessIsolation.cpp @@ -343,6 +343,7 @@ static IsolationBehavior IsolationBehaviorForURI(nsIURI* aURI, bool aIsSubframe, browser_tabs_remote_separatePrivilegedMozillaWebContentProcess()) { nsAutoCString host; if (NS_SUCCEEDED(aURI->GetAsciiHost(host))) { + // This code is duplicated in E10SUtils.sys.mjs, please update both for (const auto& separatedDomain : sSeparatedMozillaDomains) { // If the domain exactly matches our host, or our host ends with "." + // separatedDomain, we consider it matching. @@ -407,8 +408,8 @@ static already_AddRefed<BasePrincipal> GetAboutReaderURLPrincipal( // Extract the "url" parameter from the `about:reader`'s query parameters, // and recover a content principal from it. - nsAutoString readerSpec; - if (URLParams::Extract(query, u"url"_ns, readerSpec)) { + nsAutoCString readerSpec; + if (URLParams::Extract(query, "url"_ns, readerSpec)) { nsCOMPtr<nsIURI> readerUri; if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(readerUri), readerSpec))) { return BasePrincipal::CreateContentPrincipal(readerUri, aAttrs); diff --git a/dom/ipc/StructuredCloneData.h b/dom/ipc/StructuredCloneData.h index 0566a3b07f..6beba4f745 100644 --- a/dom/ipc/StructuredCloneData.h +++ b/dom/ipc/StructuredCloneData.h @@ -93,20 +93,14 @@ class SharedJSAllocatedData final { * to send a structured clone that may include blobs or transferables such as * message ports. * - To send the data, instantiate a StructuredCloneData instance and Write() - * into it like a normal structure clone. When you are ready to send the - * ClonedMessageData-bearing IPC message, use the appropriate - * BuildClonedMessageDataFor{Parent,Child,BackgroundParent,BackgroundChild} + * into it like a normal structured clone. When you are ready to send the + * ClonedMessageData-bearing IPC message, call the BuildClonedMessageData * method to populate the ClonedMessageData and then send it before your * StructuredCloneData instance is destroyed. (Buffer borrowing is used * under-the-hood to avoid duplicating the serialized data, requiring this.) - * - To receive the data, instantiate a StructuredCloneData and use the - * appropriate {Borrow,Copy,Steal}FromClonedMessageDataFor{Parent,Child, - * BackgroundParent,BackgroundChild} method. See the memory management - * section for more info. - * - * Variations: - * - If transferables are not allowed (ex: BroadcastChannel), then use the - * StructuredCloneDataNoTransfers subclass instead of StructuredCloneData. + * - To receive the data, instantiate a StructuredCloneData and then call + * the BorrowFromClonedMessageData method. See the memory management + * section for more information. * * ## Memory Management ## * @@ -135,11 +129,11 @@ class SharedJSAllocatedData final { * * 1: Specifically, in the Write() case an owning SharedJSAllocatedData is * created efficiently (by stealing from StructuredCloneHolder). The - * BuildClonedMessageDataFor* method can be called at any time and it will + * BuildClonedMessageData method can be called at any time and it will * borrow the underlying memory. While it would be even better if * SerializedStructuredCloneBuffer could hold a SharedJSAllocatedData ref, - * there's no reason you can't wait to BuildClonedMessageDataFor* until you - * need to make the IPC Send* call. + * there's no reason you can't wait to call the BuildClonedMessageData + * method until you need to make the IPC Send* call. */ class StructuredCloneData : public StructuredCloneHolder { public: diff --git a/dom/ipc/WindowGlobalChild.cpp b/dom/ipc/WindowGlobalChild.cpp index ec2811f972..4072828911 100644 --- a/dom/ipc/WindowGlobalChild.cpp +++ b/dom/ipc/WindowGlobalChild.cpp @@ -35,7 +35,7 @@ #include "nsQueryObject.h" #include "nsSerializationHelper.h" #include "nsFrameLoader.h" -#include "nsIScriptSecurityManager.h" +#include "nsScriptSecurityManager.h" #include "mozilla/dom/JSWindowActorBinding.h" #include "mozilla/dom/JSWindowActorChild.h" @@ -588,30 +588,19 @@ void WindowGlobalChild::SetDocumentURI(nsIURI* aDocumentURI) { embedderInnerWindowID, BrowsingContext()->UsePrivateBrowsing()); if (StaticPrefs::dom_security_setdocumenturi()) { - auto isLoadableViaInternet = [](nsIURI* uri) { - return (uri && (net::SchemeIsHTTP(uri) || net::SchemeIsHTTPS(uri))); - }; - if (isLoadableViaInternet(aDocumentURI)) { - nsCOMPtr<nsIURI> principalURI = mDocumentPrincipal->GetURI(); - if (mDocumentPrincipal->GetIsNullPrincipal()) { - nsCOMPtr<nsIPrincipal> precursor = - mDocumentPrincipal->GetPrecursorPrincipal(); - if (precursor) { - principalURI = precursor->GetURI(); - } - } - - if (isLoadableViaInternet(principalURI)) { - nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); - - if (!NS_SUCCEEDED(ssm->CheckSameOriginURI(principalURI, aDocumentURI, - false, false))) { - MOZ_DIAGNOSTIC_ASSERT(false, - "Setting DocumentURI with a different origin " - "than principal URI"); - } + nsCOMPtr<nsIURI> principalURI = mDocumentPrincipal->GetURI(); + if (mDocumentPrincipal->GetIsNullPrincipal()) { + nsCOMPtr<nsIPrincipal> precursor = + mDocumentPrincipal->GetPrecursorPrincipal(); + if (precursor) { + principalURI = precursor->GetURI(); } } + + MOZ_DIAGNOSTIC_ASSERT(!nsScriptSecurityManager::IsHttpOrHttpsAndCrossOrigin( + principalURI, aDocumentURI), + "Setting DocumentURI with a different origin " + "than principal URI"); } mDocumentURI = aDocumentURI; diff --git a/dom/ipc/WindowGlobalParent.cpp b/dom/ipc/WindowGlobalParent.cpp index 13fbe2940c..f60790a155 100644 --- a/dom/ipc/WindowGlobalParent.cpp +++ b/dom/ipc/WindowGlobalParent.cpp @@ -39,6 +39,7 @@ #include "mozilla/Telemetry.h" #include "mozilla/Variant.h" #include "mozilla/ipc/ProtocolUtils.h" +#include "MMPrinter.h" #include "nsContentUtils.h" #include "nsDocShell.h" #include "nsDocShellLoadState.h" @@ -230,8 +231,8 @@ void WindowGlobalParent::OriginCounter::UpdateSiteOriginsFrom( } void WindowGlobalParent::OriginCounter::Accumulate() { - mozilla::glean::geckoview::per_document_site_origins.AccumulateSamples( - {mMaxOrigins}); + mozilla::glean::geckoview::per_document_site_origins.AccumulateSingleSample( + mMaxOrigins); mMaxOrigins = 0; mOriginMap.Clear(); @@ -398,26 +399,20 @@ IPCResult WindowGlobalParent::RecvUpdateDocumentURI(NotNull<nsIURI*> aURI) { return IPC_FAIL(this, "Setting DocumentURI with unknown protocol."); } - auto isLoadableViaInternet = [](nsIURI* uri) { - return (uri && (net::SchemeIsHTTP(uri) || net::SchemeIsHTTPS(uri))); - }; - - if (isLoadableViaInternet(aURI)) { - nsCOMPtr<nsIURI> principalURI = mDocumentPrincipal->GetURI(); - if (mDocumentPrincipal->GetIsNullPrincipal()) { - nsCOMPtr<nsIPrincipal> precursor = - mDocumentPrincipal->GetPrecursorPrincipal(); - if (precursor) { - principalURI = precursor->GetURI(); - } + nsCOMPtr<nsIURI> principalURI = mDocumentPrincipal->GetURI(); + if (mDocumentPrincipal->GetIsNullPrincipal()) { + nsCOMPtr<nsIPrincipal> precursor = + mDocumentPrincipal->GetPrecursorPrincipal(); + if (precursor) { + principalURI = precursor->GetURI(); } + } - if (isLoadableViaInternet(principalURI) && - !nsScriptSecurityManager::SecurityCompareURIs(principalURI, aURI)) { - return IPC_FAIL(this, - "Setting DocumentURI with a different Origin than " - "principal URI"); - } + if (nsScriptSecurityManager::IsHttpOrHttpsAndCrossOrigin(principalURI, + aURI)) { + return IPC_FAIL(this, + "Setting DocumentURI with a different Origin than " + "principal URI"); } } @@ -570,6 +565,8 @@ IPCResult WindowGlobalParent::RecvRawMessage( stack.emplace(); stack->BorrowFromClonedMessageData(*aStack); } + MMPrinter::Print("WindowGlobalParent::RecvRawMessage", aMeta.actorName(), + aMeta.messageName(), aData); ReceiveRawMessage(aMeta, std::move(data), std::move(stack)); return IPC_OK(); } diff --git a/dom/ipc/jsactor/JSActor.cpp b/dom/ipc/jsactor/JSActor.cpp index 03926fee04..2c706ca515 100644 --- a/dom/ipc/jsactor/JSActor.cpp +++ b/dom/ipc/jsactor/JSActor.cpp @@ -441,14 +441,8 @@ void JSActor::QueryHandler::ResolvedCallback(JSContext* aCx, return; } - Maybe<ipc::StructuredCloneData> data{std::in_place}; - data->InitScope(JS::StructuredCloneScope::DifferentProcess); - - IgnoredErrorResult error; - data->Write(aCx, aValue, error); - if (NS_WARN_IF(error.Failed())) { - JS_ClearPendingException(aCx); - + Maybe<ipc::StructuredCloneData> data = TryClone(aCx, aValue); + if (!data) { nsAutoCString msg; msg.Append(mActor->Name()); msg.Append(':'); diff --git a/dom/ipc/moz.build b/dom/ipc/moz.build index 3f10b9fbab..35b6039d89 100644 --- a/dom/ipc/moz.build +++ b/dom/ipc/moz.build @@ -52,6 +52,7 @@ EXPORTS.mozilla.dom += [ "CoalescedWheelData.h", "ContentChild.h", "ContentParent.h", + "ContentParent_NotifyUpdatedDictionaries.h", "ContentProcess.h", "ContentProcessManager.h", "CSPMessageUtils.h", diff --git a/dom/ipc/nsIHangReport.idl b/dom/ipc/nsIHangReport.idl index ff01d7f51a..d99137d942 100644 --- a/dom/ipc/nsIHangReport.idl +++ b/dom/ipc/nsIHangReport.idl @@ -47,5 +47,5 @@ interface nsIHangReport : nsISupports // Inquire whether the report is for a content process loaded by the given // frameloader, or any descendents in its BrowsingContext tree. - bool isReportForBrowserOrChildren(in FrameLoader aFrameLoader); + boolean isReportForBrowserOrChildren(in FrameLoader aFrameLoader); }; diff --git a/dom/ipc/nsILoginDetectionService.idl b/dom/ipc/nsILoginDetectionService.idl index a0b1ae90a7..123318d5db 100644 --- a/dom/ipc/nsILoginDetectionService.idl +++ b/dom/ipc/nsILoginDetectionService.idl @@ -18,5 +18,5 @@ interface nsILoginDetectionService : nsISupports * Returns true if we have loaded logins from the password manager. * This is now used by testcase only. */ - bool isLoginsLoaded(); + boolean isLoginsLoaded(); }; diff --git a/dom/ipc/tests/JSWindowActor/browser_event_listener.js b/dom/ipc/tests/JSWindowActor/browser_event_listener.js index 725c2c3753..21cb0c5ee9 100644 --- a/dom/ipc/tests/JSWindowActor/browser_event_listener.js +++ b/dom/ipc/tests/JSWindowActor/browser_event_listener.js @@ -144,7 +144,7 @@ declTest("test in-process content events are not processed twice", { "content", "Should be a content <browser>" ); - is(browser.getAttribute("remotetype"), "", "Should not be remote"); + is(browser.getAttribute("remotetype"), null, "Should not be remote"); await testEventProcessedOnce(browser); }, }); @@ -160,8 +160,12 @@ declTest("test in-process chrome events are processed correctly", { "chrome://mochitests/content/browser/dom/ipc/tests/JSWindowActor/file_dummyChromePage.html" ); let chromeBrowser = dialog._frame; - is(chromeBrowser.getAttribute("type"), "", "Should be a chrome <browser>"); - is(chromeBrowser.getAttribute("remotetype"), "", "Should not be remote"); + is( + chromeBrowser.getAttribute("type"), + null, + "Should be a chrome <browser>" + ); + is(chromeBrowser.getAttribute("remotetype"), null, "Should not be remote"); await testEventProcessedOnce(chromeBrowser, "dummyChromePage.html"); diff --git a/dom/ipc/tests/browser.toml b/dom/ipc/tests/browser.toml index cd2129e9c1..550558ca46 100644 --- a/dom/ipc/tests/browser.toml +++ b/dom/ipc/tests/browser.toml @@ -64,6 +64,8 @@ skip-if = [ ["browser_hide_tooltip.js"] +["browser_isactiveintab.js"] + ["browser_layers_unloaded_while_interruptingJS.js"] ["browser_memory_distribution_telemetry.js"] diff --git a/dom/ipc/tests/browser_isactiveintab.js b/dom/ipc/tests/browser_isactiveintab.js new file mode 100644 index 0000000000..bd189f1826 --- /dev/null +++ b/dom/ipc/tests/browser_isactiveintab.js @@ -0,0 +1,138 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function documentURL(origin, html) { + let params = new URLSearchParams(); + params.append("html", html.trim()); + return `${origin}/document-builder.sjs?${params.toString()}`; +} + +add_task(async function checkIsActiveInTab() { + // This test creates a few tricky situations with navigation and iframes and + // examines the results of various ways you might think to check if a page + // is currently visible, and confirms that isActiveInTab works, even if the + // others don't. + + // Make a top level page with two nested iframes. + const IFRAME2_URL = documentURL("https://example.com", `<h1>iframe2</h1>`); + const IFRAME1_URL = documentURL( + "https://example.com", + `<iframe src=${JSON.stringify(IFRAME2_URL)}></iframe><h1>iframe1</h1>` + ); + const TEST_URL = documentURL( + "https://example.com", + `<iframe src=${JSON.stringify(IFRAME1_URL)}></iframe><h1>top window</h1>` + ); + + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + let topBC1 = browser.browsingContext; + let topWindowGlobal1 = topBC1.currentWindowGlobal; + + is( + browser.browsingContext.children.length, + 1, + "only one child for top window" + ); + let iframe1 = browser.browsingContext.children[0]; + let iframeWindowGlobal1a = iframe1.currentWindowGlobal; + + is(iframe1.children.length, 1, "only one child for iframe"); + let iframe2 = iframe1.children[0]; + let iframeWindowGlobal2 = iframe2.currentWindowGlobal; + + ok(topWindowGlobal1.isActiveInTab, "top window global is active in tab"); + ok(iframeWindowGlobal1a.isActiveInTab, "topmost iframe is active in tab"); + ok(iframeWindowGlobal2.isActiveInTab, "innermost iframe is active in tab"); + + // Do a same-origin navigation on the topmost iframe. + let iframe1bURI = + "https://example.com/browser/dom/ipc/tests/file_dummy.html"; + let loadedIframe = BrowserTestUtils.browserLoaded( + browser, + true, + iframe1bURI + ); + await SpecialPowers.spawn( + iframe1, + [iframe1bURI], + async function (_iframe1bURI) { + content.location = _iframe1bURI; + } + ); + await loadedIframe; + + ok( + topWindowGlobal1.isActiveInTab, + "top window global is still active in tab" + ); + + let iframeWindowGlobal1b = iframe1.currentWindowGlobal; + isnot( + iframeWindowGlobal1a, + iframeWindowGlobal1b, + "navigating changed the iframe's current window" + ); + + // This tests the !CanSend() case but unfortunately not the + // `bc->GetCurrentWindowGlobal() != this` case. Apparently the latter will + // only hold temporarily, so that is likely hard to test. + ok( + !iframeWindowGlobal1a.isActiveInTab, + "topmost iframe is not active in tab" + ); + + ok(iframeWindowGlobal1b.isActiveInTab, "new iframe is active in tab"); + + is( + iframe2.currentWindowGlobal, + iframeWindowGlobal2, + "innermost iframe current window global has not changed" + ); + + ok( + iframeWindowGlobal2.isCurrentGlobal, + "innermost iframe is still the current global for its BC" + ); + + // With a same-origin navigation, this hits the !bc->AncestorsAreCurrent() + // case. (With a cross-origin navigation, this hits the !CanSend() case.) + ok( + !iframeWindowGlobal2.isActiveInTab, + "innermost iframe is not active in tab" + ); + + // Load a new page into the tab to test the behavior when a page is in + // the BFCache. + let newTopURI = "https://example.com/browser/dom/ipc/tests/file_dummy.html"; + let loadedTop2 = BrowserTestUtils.browserLoaded(browser, false, newTopURI); + await BrowserTestUtils.startLoadingURIString(browser, newTopURI); + await loadedTop2; + + isnot(browser.browsingContext, topBC1, "Navigated to a new BC"); + + is( + topBC1.currentWindowGlobal, + topWindowGlobal1, + "old top window is still the current window global for the old BC" + ); + ok(topWindowGlobal1.isInBFCache, "old top window's BC is in the BFCache"); + ok(!topWindowGlobal1.isCurrentGlobal, "old top frame isn't current"); + ok(!topWindowGlobal1.isActiveInTab, "old top frame not active in tab"); + + is( + iframe1.currentWindowGlobal, + iframeWindowGlobal1b, + "old iframe is still the current window global for the BC" + ); + ok(!iframeWindowGlobal1b.isCurrentGlobal, "newer top iframe isn't current"); + ok( + iframeWindowGlobal1b.isInBFCache, + "old top window's BC is in the BFCache" + ); + ok(iframe1.ancestorsAreCurrent, "ancestors of iframe are current"); + ok( + !iframeWindowGlobal1b.isActiveInTab, + "newer top iframe is active in not active in tab after top level navigation" + ); + }); +}); diff --git a/dom/locales/en-US/chrome/dom/dom.properties b/dom/locales/en-US/chrome/dom/dom.properties index 1c56e22b2c..f5b1b2c234 100644 --- a/dom/locales/en-US/chrome/dom/dom.properties +++ b/dom/locales/en-US/chrome/dom/dom.properties @@ -17,7 +17,7 @@ StopScriptButton=Stop script DebugScriptButton=Debug script WaitForScriptButton=Continue DontAskAgain=&Don’t ask me again -WindowCloseBlockedWarning=Scripts may not close windows that were not opened by script. +WindowCloseByScriptBlockedWarning=Scripts may only close windows that were opened by a script. OnBeforeUnloadTitle=Are you sure? OnBeforeUnloadMessage2=This page is asking you to confirm that you want to leave — information you’ve entered may not be saved. OnBeforeUnloadStayButton=Stay on page diff --git a/dom/localstorage/ActorsParent.cpp b/dom/localstorage/ActorsParent.cpp index 2554628d7f..639c3822ac 100644 --- a/dom/localstorage/ActorsParent.cpp +++ b/dom/localstorage/ActorsParent.cpp @@ -1530,9 +1530,7 @@ class Datastore final uint32_t PrivateBrowsingId() const { return mPrivateBrowsingId; } bool IsPersistent() const { - // Private-browsing is forbidden from touching disk, but - // StorageAccess::eSessionScoped is allowed to touch disk because - // QuotaManager's storage for such origins is wiped at shutdown. + // Private-browsing is forbidden from touching disk. return mPrivateBrowsingId == 0; } diff --git a/dom/localstorage/LocalStorageManager2.cpp b/dom/localstorage/LocalStorageManager2.cpp index 5e19cf2254..cd2cd9eca0 100644 --- a/dom/localstorage/LocalStorageManager2.cpp +++ b/dom/localstorage/LocalStorageManager2.cpp @@ -223,7 +223,7 @@ LocalStorageManager2::PrecacheStorage(nsIPrincipal* aPrincipal, // implementation to perform a preload in the content/current process. That's // not how things work in LSNG. Instead everything happens in the parent // process, triggered by the official preloading spot, - // ContentParent::AboutToLoadHttpFtpDocumentForChild. + // ContentParent::AboutToLoadHttpDocumentForChild. return NS_ERROR_NOT_IMPLEMENTED; } diff --git a/dom/localstorage/PBackgroundLSDatabase.ipdl b/dom/localstorage/PBackgroundLSDatabase.ipdl index 2f2f9dffac..179bd4164d 100644 --- a/dom/localstorage/PBackgroundLSDatabase.ipdl +++ b/dom/localstorage/PBackgroundLSDatabase.ipdl @@ -89,7 +89,7 @@ struct LSSnapshotInitInfo * calls to PBackground are the only viable mechanism because blocking * PBackground is not acceptable. (Note that an attempt is made to minimize any * I/O latency by triggering preloading from - * ContentParent::AboutToLoadHttpFtpDocumentForChild, the central place + * ContentParent::AboutToLoadHttpDocumentForChild, the central place * for pre-loading.) */ [ChildImpl=virtual, ParentImpl=virtual] diff --git a/dom/localstorage/SnappyUtils.cpp b/dom/localstorage/SnappyUtils.cpp index cf47421bc5..cf8c08e13f 100644 --- a/dom/localstorage/SnappyUtils.cpp +++ b/dom/localstorage/SnappyUtils.cpp @@ -16,7 +16,7 @@ namespace mozilla::dom { -static_assert(SNAPPY_VERSION == 0x010109); +static_assert(SNAPPY_VERSION == 0x010200); bool SnappyCompress(const nsACString& aSource, nsACString& aDest) { MOZ_ASSERT(!aSource.IsVoid()); diff --git a/dom/localstorage/nsILocalStorageManager.idl b/dom/localstorage/nsILocalStorageManager.idl index 0e8db22482..6ee29b4e14 100644 --- a/dom/localstorage/nsILocalStorageManager.idl +++ b/dom/localstorage/nsILocalStorageManager.idl @@ -20,7 +20,7 @@ interface nsILocalStorageManager : nsISupports /** * Trigger preload of LocalStorage for the given principal. For use by - * ContentParent::AboutToLoadHttpFtpDocumentForChild to maximize the + * ContentParent::AboutToLoadHttpDocumentForChild to maximize the * amount of time we have to load the data off disk before the page might * attempt to touch LocalStorage. * diff --git a/dom/manifest/Manifest.sys.mjs b/dom/manifest/Manifest.sys.mjs index 15e1e2ef93..97f786318f 100644 --- a/dom/manifest/Manifest.sys.mjs +++ b/dom/manifest/Manifest.sys.mjs @@ -29,11 +29,11 @@ ChromeUtils.defineESModuleGetters(lazy, { * @note The generated hash is returned in base64 form. Mind the fact base64 * is case-sensitive if you are going to reuse this code. */ -function generateHash(aString) { +function generateHash(aString, hashAlg) { const cryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance( Ci.nsICryptoHash ); - cryptoHash.init(Ci.nsICryptoHash.MD5); + cryptoHash.init(hashAlg); const stringStream = Cc[ "@mozilla.org/io/string-input-stream;1" ].createInstance(Ci.nsIStringInputStream); @@ -66,11 +66,39 @@ class Manifest { this._manifestUrl = manifestUrl; // The key for this is the manifests URL that is required to be unique. // However arbitrary urls are not safe file paths so lets hash it. - const fileName = generateHash(manifestUrl) + ".json"; - this._path = PathUtils.join(MANIFESTS_DIR, fileName); + const filename = + generateHash(manifestUrl, Ci.nsICryptoHash.SHA256) + ".json"; + this._path = PathUtils.join(MANIFESTS_DIR, filename); this.browser = browser; } + /** + * See Bug 1871109 + * This function is called at the beginning of initialize() to check if a given + * manifest has MD5 based filename, if so we remove it and migrate the content to + * a new file with SHA256 based name. + * This is done due to security concern, as MD5 is an outdated hashing algorithm and + * shouldn't be used anymore + */ + async removeMD5BasedFilename() { + const filenameMD5 = + generateHash(this._manifestUrl, Ci.nsICryptoHash.MD5) + ".json"; + const MD5Path = PathUtils.join(MANIFESTS_DIR, filenameMD5); + try { + await IOUtils.copy(MD5Path, this._path, { noOverwrite: true }); + } catch (error) { + // we are ignoring the failures returned from copy as it should not stop us from + // installing a new manifest + } + + // Remove the old MD5 based file unconditionally to ensure it's no longer used + try { + await IOUtils.remove(MD5Path); + } catch { + // ignore the error in case MD5 based file does not exist + } + } + get browser() { return this._browser; } @@ -80,6 +108,7 @@ class Manifest { } async initialize() { + await this.removeMD5BasedFilename(); this._store = new lazy.JSONFile({ path: this._path, saveDelayMs: 100 }); await this._store.load(); } diff --git a/dom/manifest/test/browser_Manifest_install.js b/dom/manifest/test/browser_Manifest_install.js index d3b949be19..aad43e0263 100644 --- a/dom/manifest/test/browser_Manifest_install.js +++ b/dom/manifest/test/browser_Manifest_install.js @@ -23,18 +23,59 @@ function makeTestURL() { return url.href; } +function generateHash(aString, hashAlg) { + const cryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance( + Ci.nsICryptoHash + ); + cryptoHash.init(hashAlg); + const stringStream = Cc[ + "@mozilla.org/io/string-input-stream;1" + ].createInstance(Ci.nsIStringInputStream); + stringStream.data = aString; + cryptoHash.updateFromStream(stringStream, -1); + // base64 allows the '/' char, but we can't use it for filenames. + return cryptoHash.finish(true).replace(/\//g, "-"); +} + +const MANIFESTS_DIR = PathUtils.join(PathUtils.profileDir, "manifests"); + add_task(async function () { const tabOptions = { gBrowser, url: makeTestURL() }; + const filenameMD5 = generateHash(manifestUrl, Ci.nsICryptoHash.MD5) + ".json"; + const filenameSHA = + generateHash(manifestUrl, Ci.nsICryptoHash.SHA256) + ".json"; + const manifestMD5Path = PathUtils.join(MANIFESTS_DIR, filenameMD5); + const manifestSHAPath = PathUtils.join(MANIFESTS_DIR, filenameSHA); + await BrowserTestUtils.withNewTab(tabOptions, async function (browser) { - let manifest = await Manifests.getManifest(browser, manifestUrl); - is(manifest.installed, false, "We haven't installed this manifest yet"); + let tmpManifest = await Manifests.getManifest(browser, manifestUrl); + is(tmpManifest.installed, false, "We haven't installed this manifest yet"); + + await tmpManifest.install(); + // making sure the manifest is actually installed before proceeding + await tmpManifest._store._save(); + await IOUtils.move(tmpManifest.path, manifestMD5Path); + + let exists = await IOUtils.exists(tmpManifest.path); + is( + exists, + false, + "Manually moved manifest from SHA256 based path to MD5 based path" + ); + Manifests.manifestObjs.delete(manifestUrl); + + let manifest = await Manifests.getManifest(browser, manifestUrl); await manifest.install(browser); is(manifest.name, "hello World", "Manifest has correct name"); is(manifest.installed, true, "Manifest is installed"); is(manifest.url, manifestUrl, "has correct url"); is(manifest.browser, browser, "has correct browser"); + is(manifest.path, manifestSHAPath, "has correct path"); + + exists = await IOUtils.exists(manifestMD5Path); + is(exists, false, "MD5 based manifest removed"); manifest = await Manifests.getManifest(browser, manifestUrl); is(manifest.installed, true, "New instances are installed"); diff --git a/dom/media/AudioConverter.h b/dom/media/AudioConverter.h index 0ace580b26..c8cbbb7949 100644 --- a/dom/media/AudioConverter.h +++ b/dom/media/AudioConverter.h @@ -137,7 +137,6 @@ class AudioConverter { AlignedBuffer<Value> temp = buffer.Forget(); Process(temp, temp.Data(), SamplesInToFrames(temp.Length())); return AudioDataBuffer<Format, Value>(std::move(temp)); - ; } return Process(buffer); } diff --git a/dom/media/AudioPacketizer.h b/dom/media/AudioPacketizer.h index 8df04c0c5c..17579618ea 100644 --- a/dom/media/AudioPacketizer.h +++ b/dom/media/AudioPacketizer.h @@ -100,11 +100,15 @@ class AudioPacketizer { return out; } - void Output(OutputType* aOutputBuffer) { + // Return the number of actual frames dequeued -- this can be lower than the + // packet size when underruning or draining. + size_t Output(OutputType* aOutputBuffer) { uint32_t samplesNeeded = mPacketSize * mChannels; + size_t rv = 0; // Under-run. Pad the end of the buffer with silence. if (AvailableSamples() < samplesNeeded) { + rv = AvailableSamples() / mChannels; #ifdef LOG_PACKETIZER_UNDERRUN char buf[256]; snprintf(buf, 256, @@ -115,6 +119,8 @@ class AudioPacketizer { uint32_t zeros = samplesNeeded - AvailableSamples(); PodZero(aOutputBuffer + AvailableSamples(), zeros); samplesNeeded -= zeros; + } else { + rv = mPacketSize; } if (ReadIndex() + samplesNeeded <= mLength) { ConvertAudioSamples<InputType, OutputType>(mStorage.get() + ReadIndex(), @@ -128,6 +134,7 @@ class AudioPacketizer { mStorage.get(), aOutputBuffer + firstPartLength, secondPartLength); } mReadIndex += samplesNeeded; + return rv; } void Clear() { diff --git a/dom/media/CubebUtils.cpp b/dom/media/CubebUtils.cpp index bad1ab649d..dbdab3a56a 100644 --- a/dom/media/CubebUtils.cpp +++ b/dom/media/CubebUtils.cpp @@ -300,12 +300,10 @@ RefPtr<CubebHandle> GetCubeb() { // This is only exported when running tests. void ForceSetCubebContext(cubeb* aCubebContext) { + RefPtr<CubebHandle> oldHandle; // For release without sMutex StaticMutexAutoLock lock(sMutex); - if (aCubebContext) { - sCubebHandle = new CubebHandle(aCubebContext); - } else { - sCubebHandle = nullptr; - } + oldHandle = sCubebHandle.forget(); + sCubebHandle = aCubebContext ? new CubebHandle(aCubebContext) : nullptr; sCubebState = CubebState::Initialized; } @@ -384,10 +382,33 @@ int CubebStreamInit(cubeb* context, cubeb_stream** stream, if (ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); } - return cubeb_stream_init(context, stream, stream_name, input_device, - input_stream_params, output_device, - output_stream_params, latency_frames, data_callback, - state_callback, user_ptr); + cubeb_stream_params inputParamData; + cubeb_stream_params outputParamData; + cubeb_stream_params* inputParamPtr = input_stream_params; + cubeb_stream_params* outputParamPtr = output_stream_params; + if (input_stream_params && !output_stream_params) { + inputParamData = *input_stream_params; + inputParamData.rate = llround( + static_cast<double>(StaticPrefs::media_cubeb_input_drift_factor()) * + inputParamData.rate); + MOZ_LOG( + gCubebLog, LogLevel::Info, + ("CubebStreamInit input stream rate %" PRIu32, inputParamData.rate)); + inputParamPtr = &inputParamData; + } else if (output_stream_params && !input_stream_params) { + outputParamData = *output_stream_params; + outputParamData.rate = llround( + static_cast<double>(StaticPrefs::media_cubeb_output_drift_factor()) * + outputParamData.rate); + MOZ_LOG( + gCubebLog, LogLevel::Info, + ("CubebStreamInit output stream rate %" PRIu32, outputParamData.rate)); + outputParamPtr = &outputParamData; + } + + return cubeb_stream_init( + context, stream, stream_name, input_device, inputParamPtr, output_device, + outputParamPtr, latency_frames, data_callback, state_callback, user_ptr); } void InitBrandName() { @@ -655,15 +676,20 @@ uint32_t GetCubebMTGLatencyInFrames(cubeb_stream_params* params) { } static const char* gInitCallbackPrefs[] = { - PREF_VOLUME_SCALE, PREF_CUBEB_OUTPUT_DEVICE, - PREF_CUBEB_LATENCY_PLAYBACK, PREF_CUBEB_LATENCY_MTG, - PREF_CUBEB_BACKEND, PREF_CUBEB_FORCE_NULL_CONTEXT, - PREF_CUBEB_SANDBOX, PREF_AUDIOIPC_STACK_SIZE, - PREF_AUDIOIPC_SHM_AREA_SIZE, nullptr, + PREF_VOLUME_SCALE, + PREF_CUBEB_OUTPUT_DEVICE, + PREF_CUBEB_LATENCY_PLAYBACK, + PREF_CUBEB_LATENCY_MTG, + PREF_CUBEB_BACKEND, + PREF_CUBEB_FORCE_SAMPLE_RATE, + PREF_CUBEB_FORCE_NULL_CONTEXT, + PREF_CUBEB_SANDBOX, + PREF_AUDIOIPC_STACK_SIZE, + PREF_AUDIOIPC_SHM_AREA_SIZE, + nullptr, }; static const char* gCallbackPrefs[] = { - PREF_CUBEB_FORCE_SAMPLE_RATE, // We don't want to call the callback on startup, because the pref is the // empty string by default ("", which means "logging disabled"). Because the // logging can be enabled via environment variables (MOZ_LOG="module:5"), diff --git a/dom/media/DOMMediaStream.cpp b/dom/media/DOMMediaStream.cpp index 5031882c19..2c1e2ea514 100644 --- a/dom/media/DOMMediaStream.cpp +++ b/dom/media/DOMMediaStream.cpp @@ -99,6 +99,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DOMMediaStream, tmp->Destroy(); NS_IMPL_CYCLE_COLLECTION_UNLINK(mTracks) NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsumersToKeepAlive) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTrackListeners) NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR NS_IMPL_CYCLE_COLLECTION_UNLINK_END @@ -106,6 +107,7 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DOMMediaStream, DOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTracks) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsumersToKeepAlive) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTrackListeners) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_ADDREF_INHERITED(DOMMediaStream, DOMEventTargetHelper) @@ -115,6 +117,13 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMMediaStream) NS_INTERFACE_MAP_ENTRY_CONCRETE(DOMMediaStream) NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION(DOMMediaStream::TrackListener) +NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMMediaStream::TrackListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMMediaStream::TrackListener) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMMediaStream::TrackListener) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + DOMMediaStream::DOMMediaStream(nsPIDOMWindowInner* aWindow) : DOMEventTargetHelper(aWindow), mPlaybackTrackListener(MakeAndAddRef<PlaybackTrackListener>(this)) { diff --git a/dom/media/DOMMediaStream.h b/dom/media/DOMMediaStream.h index b0a9f895bb..bfce7b65f0 100644 --- a/dom/media/DOMMediaStream.h +++ b/dom/media/DOMMediaStream.h @@ -59,9 +59,10 @@ class DOMMediaStream : public DOMEventTargetHelper, public: typedef dom::MediaTrackConstraints MediaTrackConstraints; - class TrackListener { + class TrackListener : public nsISupports { public: - virtual ~TrackListener() = default; + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(TrackListener) /** * Called when the DOMMediaStream has a live track added, either by @@ -94,6 +95,9 @@ class DOMMediaStream : public DOMEventTargetHelper, * Called when the DOMMediaStream has become inaudible. */ virtual void NotifyInaudible(){}; + + protected: + virtual ~TrackListener() = default; }; explicit DOMMediaStream(nsPIDOMWindowInner* aWindow); @@ -236,7 +240,7 @@ class DOMMediaStream : public DOMEventTargetHelper, nsTArray<nsCOMPtr<nsISupports>> mConsumersToKeepAlive; // The track listeners subscribe to changes in this stream's track set. - nsTArray<TrackListener*> mTrackListeners; + nsTArray<RefPtr<TrackListener>> mTrackListeners; // True if this stream has live tracks. bool mActive = false; diff --git a/dom/media/DecoderTraits.cpp b/dom/media/DecoderTraits.cpp index af4d08ae4b..2ad76587ae 100644 --- a/dom/media/DecoderTraits.cpp +++ b/dom/media/DecoderTraits.cpp @@ -6,6 +6,7 @@ #include "DecoderTraits.h" #include "MediaContainerType.h" +#include "mozilla/glean/GleanMetrics.h" #include "mozilla/Preferences.h" #include "OggDecoder.h" @@ -127,11 +128,11 @@ static CanPlayStatus CanHandleCodecsType( static CanPlayStatus CanHandleMediaType( const MediaContainerType& aType, DecoderDoctorDiagnostics* aDiagnostics) { if (DecoderTraits::IsHttpLiveStreamingType(aType)) { - Telemetry::Accumulate(Telemetry::MEDIA_HLS_CANPLAY_REQUESTED, true); + glean::hls::canplay_requested.Add(); } #ifdef MOZ_ANDROID_HLS_SUPPORT if (HLSDecoder::IsSupportedType(aType)) { - Telemetry::Accumulate(Telemetry::MEDIA_HLS_CANPLAY_SUPPORTED, true); + glean::hls::canplay_supported.Add(); return CANPLAY_MAYBE; } #endif diff --git a/dom/media/DeviceInputTrack.cpp b/dom/media/DeviceInputTrack.cpp index 87d1ae73ab..5d69f7107a 100644 --- a/dom/media/DeviceInputTrack.cpp +++ b/dom/media/DeviceInputTrack.cpp @@ -127,28 +127,42 @@ NotNull<AudioDataListener*> DeviceInputConsumerTrack::GetAudioDataListener() return WrapNotNull(mListener.get()); } -bool DeviceInputConsumerTrack::ConnectToNativeDevice() const { +bool DeviceInputConsumerTrack::ConnectedToNativeDevice() const { MOZ_ASSERT(NS_IsMainThread()); return mDeviceInputTrack && mDeviceInputTrack->AsNativeInputTrack(); } -bool DeviceInputConsumerTrack::ConnectToNonNativeDevice() const { +bool DeviceInputConsumerTrack::ConnectedToNonNativeDevice() const { MOZ_ASSERT(NS_IsMainThread()); return mDeviceInputTrack && mDeviceInputTrack->AsNonNativeInputTrack(); } +DeviceInputTrack* DeviceInputConsumerTrack::GetDeviceInputTrackGraphThread() + const { + AssertOnGraphThread(); + + if (mInputs.IsEmpty()) { + return nullptr; + } + MOZ_ASSERT(mInputs.Length() == 1); + MediaTrack* track = mInputs[0]->GetSource(); + MOZ_ASSERT(track->AsDeviceInputTrack()); + return static_cast<DeviceInputTrack*>(track); +} + void DeviceInputConsumerTrack::GetInputSourceData(AudioSegment& aOutput, - const MediaInputPort* aPort, GraphTime aFrom, GraphTime aTo) const { AssertOnGraphThread(); MOZ_ASSERT(aOutput.IsEmpty()); + MOZ_ASSERT(mInputs.Length() == 1); - MediaTrack* source = aPort->GetSource(); + MediaInputPort* port = mInputs[0]; + MediaTrack* source = port->GetSource(); GraphTime next; for (GraphTime t = aFrom; t < aTo; t = next) { MediaInputPort::InputInterval interval = - MediaInputPort::GetNextInputInterval(aPort, t); + MediaInputPort::GetNextInputInterval(port, t); interval.mEnd = std::min(interval.mEnd, aTo); const bool inputEnded = diff --git a/dom/media/DeviceInputTrack.h b/dom/media/DeviceInputTrack.h index 6206dc0dfc..0a92ded13c 100644 --- a/dom/media/DeviceInputTrack.h +++ b/dom/media/DeviceInputTrack.h @@ -44,8 +44,7 @@ class NonNativeInputTrack; // } else { // MOZ_ASSERT(mInputs.Length() == 1); // AudioSegment data; -// DeviceInputConsumerTrack::GetInputSourceData(data, mInputs[0], aFrom, -// aTo); +// DeviceInputConsumerTrack::GetInputSourceData(data, aFrom, aTo); // // You can do audio data processing before appending to mSegment here. // GetData<AudioSegment>()->AppendFrom(&data); // } @@ -77,20 +76,22 @@ class DeviceInputConsumerTrack : public ProcessedMediaTrack { void DisconnectDeviceInput(); Maybe<CubebUtils::AudioDeviceID> DeviceId() const; NotNull<AudioDataListener*> GetAudioDataListener() const; - bool ConnectToNativeDevice() const; - bool ConnectToNonNativeDevice() const; + bool ConnectedToNativeDevice() const; + bool ConnectedToNonNativeDevice() const; // Any thread: DeviceInputConsumerTrack* AsDeviceInputConsumerTrack() override { return this; } - protected: // Graph thread API: - // Get the data in [aFrom, aTo) from aPort->GetSource() to aOutput. aOutput - // needs to be empty. - void GetInputSourceData(AudioSegment& aOutput, const MediaInputPort* aPort, - GraphTime aFrom, GraphTime aTo) const; + DeviceInputTrack* GetDeviceInputTrackGraphThread() const; + + protected: + // Get the data in [aFrom, aTo) from the device input to aOutput. aOutput + // needs to be empty. A device input must be connected. Graph thread. + void GetInputSourceData(AudioSegment& aOutput, GraphTime aFrom, + GraphTime aTo) const; // Main Thread variables: RefPtr<MediaInputPort> mPort; diff --git a/dom/media/EncoderTraits.h b/dom/media/EncoderTraits.h index d96bc37e4a..ead78a8c40 100644 --- a/dom/media/EncoderTraits.h +++ b/dom/media/EncoderTraits.h @@ -12,12 +12,13 @@ namespace mozilla::EncoderSupport { -bool Supports(const RefPtr<dom::VideoEncoderConfigInternal>& aEncoderConfigInternal) { +template <typename T> +bool Supports(const RefPtr<T>& aEncoderConfigInternal) { RefPtr<PEMFactory> factory = new PEMFactory(); EncoderConfig config = aEncoderConfigInternal->ToEncoderConfig(); return factory->Supports(config); } -} // namespace mozilla +} // namespace mozilla::EncoderSupport #endif diff --git a/dom/media/ExternalEngineStateMachine.cpp b/dom/media/ExternalEngineStateMachine.cpp index acfc1f5fa2..1728728dc6 100644 --- a/dom/media/ExternalEngineStateMachine.cpp +++ b/dom/media/ExternalEngineStateMachine.cpp @@ -164,11 +164,11 @@ void ExternalEngineStateMachine::ChangeStateTo(State aNextState) { LOG("Change state : '%s' -> '%s' (play-state=%d)", StateToStr(mState.mName), StateToStr(aNextState), mPlayState.Ref()); // Assert the possible state transitions. - MOZ_ASSERT_IF(mState.IsInitEngine(), aNextState == State::ReadingMetadata || + MOZ_ASSERT_IF( + mState.IsReadingMetadata(), + aNextState == State::InitEngine || aNextState == State::ShutdownEngine); + MOZ_ASSERT_IF(mState.IsInitEngine(), aNextState == State::RunningEngine || aNextState == State::ShutdownEngine); - MOZ_ASSERT_IF(mState.IsReadingMetadata(), - aNextState == State::RunningEngine || - aNextState == State::ShutdownEngine); MOZ_ASSERT_IF(mState.IsRunningEngine(), aNextState == State::SeekingData || aNextState == State::ShutdownEngine || @@ -183,8 +183,8 @@ void ExternalEngineStateMachine::ChangeStateTo(State aNextState) { aNextState == State::SeekingData || aNextState == State::ShutdownEngine); if (aNextState == State::SeekingData) { mState = StateObject({StateObject::SeekingData()}); - } else if (aNextState == State::ReadingMetadata) { - mState = StateObject({StateObject::ReadingMetadata()}); + } else if (aNextState == State::InitEngine) { + mState = StateObject({StateObject::InitEngine()}); } else if (aNextState == State::RunningEngine) { mState = StateObject({StateObject::RunningEngine()}); } else if (aNextState == State::ShutdownEngine) { @@ -200,8 +200,8 @@ ExternalEngineStateMachine::ExternalEngineStateMachine( MediaDecoder* aDecoder, MediaFormatReader* aReader) : MediaDecoderStateMachineBase(aDecoder, aReader) { LOG("Created ExternalEngineStateMachine"); - MOZ_ASSERT(mState.IsInitEngine()); - InitEngine(); + MOZ_ASSERT(mState.IsReadingMetadata()); + ReadMetadata(); } ExternalEngineStateMachine::~ExternalEngineStateMachine() { @@ -214,8 +214,9 @@ void ExternalEngineStateMachine::InitEngine() { mEngine.reset(new MFMediaEngineWrapper(this, mFrameStats)); #endif if (mEngine) { + MOZ_ASSERT(mInfo); auto* state = mState.AsInitEngine(); - state->mInitPromise = mEngine->Init(!mMinimizePreroll); + state->mInitPromise = mEngine->Init(*mInfo, !mMinimizePreroll); state->mInitPromise ->Then(OwnerThread(), __func__, this, &ExternalEngineStateMachine::OnEngineInitSuccess, @@ -235,14 +236,10 @@ void ExternalEngineStateMachine::OnEngineInitSuccess() { mReader->UpdateMediaEngineId(mEngine->Id()); state->mInitPromise = nullptr; if (mState.IsInitEngine()) { - ChangeStateTo(State::ReadingMetadata); - ReadMetadata(); + StartRunningEngine(); return; } - // We just recovered from CDM process crash, so we need to update the media - // info to the new CDM process. - MOZ_ASSERT(mInfo); - mEngine->SetMediaInfo(*mInfo); + // We just recovered from CDM process crash, seek to previous position. SeekTarget target(mCurrentPosition.Ref(), SeekTarget::Type::Accurate); Seek(target); } @@ -260,13 +257,17 @@ void ExternalEngineStateMachine::OnEngineInitFailure() { } void ExternalEngineStateMachine::ReadMetadata() { - AssertOnTaskQueue(); + MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mState.IsReadingMetadata()); - mReader->ReadMetadata() - ->Then(OwnerThread(), __func__, this, - &ExternalEngineStateMachine::OnMetadataRead, - &ExternalEngineStateMachine::OnMetadataNotRead) - ->Track(mState.AsReadingMetadata()->mMetadataRequest); + Unused << OwnerThread()->Dispatch(NS_NewRunnableFunction( + "ExternalEngineStateMachine::ReadMetadata", + [self = RefPtr<ExternalEngineStateMachine>{this}, this] { + mReader->ReadMetadata() + ->Then(OwnerThread(), __func__, this, + &ExternalEngineStateMachine::OnMetadataRead, + &ExternalEngineStateMachine::OnMetadataNotRead) + ->Track(mState.AsReadingMetadata()->mMetadataRequest); + })); } void ExternalEngineStateMachine::OnMetadataRead(MetadataHolder&& aMetadata) { @@ -303,8 +304,6 @@ void ExternalEngineStateMachine::OnMetadataRead(MetadataHolder&& aMetadata) { } #endif - mEngine->SetMediaInfo(*mInfo); - if (Info().mMetadataDuration.isSome()) { mDuration = Info().mMetadataDuration; } else if (Info().mUnadjustedMetadataEndTime.isSome()) { @@ -333,7 +332,8 @@ void ExternalEngineStateMachine::OnMetadataRead(MetadataHolder&& aMetadata) { mMetadataLoadedEvent.Notify(std::move(aMetadata.mInfo), std::move(aMetadata.mTags), MediaDecoderEventVisibility::Observable); - StartRunningEngine(); + ChangeStateTo(State::InitEngine); + InitEngine(); } void ExternalEngineStateMachine::OnMetadataNotRead(const MediaResult& aError) { @@ -365,6 +365,58 @@ bool ExternalEngineStateMachine::IsFormatSupportedByExternalEngine( #endif } +RefPtr<MediaDecoder::SeekPromise> ExternalEngineStateMachine::InvokeSeek( + const SeekTarget& aTarget) { + return InvokeAsync( + OwnerThread(), __func__, + [self = RefPtr<ExternalEngineStateMachine>(this), this, + target = aTarget]() -> RefPtr<MediaDecoder::SeekPromise> { + AssertOnTaskQueue(); + if (!mEngine || !mEngine->IsInited()) { + LOG("Can't perform seek (%" PRId64 ") now, add a pending seek task", + target.GetTime().ToMicroseconds()); + // We haven't added any pending seek before + if (mPendingSeek.mPromise.IsEmpty()) { + mPendingTasks.AppendElement(NS_NewRunnableFunction( + "ExternalEngineStateMachine::InvokeSeek", + [self = RefPtr{this}, this] { + if (!mPendingSeek.Exists()) { + return; + } + Seek(*mPendingSeek.mTarget) + ->Then(OwnerThread(), __func__, + [self = RefPtr{this}, + this](const MediaDecoder::SeekPromise:: + ResolveOrRejectValue& aVal) { + mPendingSeekRequest.Complete(); + if (aVal.IsResolve()) { + mPendingSeek.Resolve(__func__); + } else { + mPendingSeek.RejectIfExists(__func__); + } + mPendingSeek = SeekJob(); + }) + ->Track(mPendingSeekRequest); + })); + } else { + // Reject previous pending promise, as we will create a new one + LOG("Replace previous pending seek with a new one"); + mPendingSeek.RejectIfExists(__func__); + mPendingSeekRequest.DisconnectIfExists(); + } + mPendingSeek.mTarget = Some(target); + return mPendingSeek.mPromise.Ensure(__func__); + } + if (mPendingSeek.Exists()) { + LOG("Discard pending seek because another new seek happens"); + mPendingSeek.RejectIfExists(__func__); + mPendingSeek = SeekJob(); + mPendingSeekRequest.DisconnectIfExists(); + } + return self->Seek(target); + }); +} + RefPtr<MediaDecoder::SeekPromise> ExternalEngineStateMachine::Seek( const SeekTarget& aTarget) { AssertOnTaskQueue(); @@ -570,11 +622,15 @@ RefPtr<ShutdownPromise> ExternalEngineStateMachine::Shutdown() { mSetCDMProxyPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_ABORT_ERR, __func__); mSetCDMProxyRequest.DisconnectIfExists(); - mInitEngineForCDMRequest.DisconnectIfExists(); + + mPendingSeek.RejectIfExists(__func__); + mPendingSeekRequest.DisconnectIfExists(); mPendingTasks.Clear(); - mEngine->Shutdown(); + if (mEngine) { + mEngine->Shutdown(); + } auto* state = mState.AsShutdownEngine(); state->mShutdown = mReader->Shutdown()->Then( @@ -622,8 +678,7 @@ void ExternalEngineStateMachine::BufferedRangeUpdated() { return; \ } \ /* Initialzation is not done yet, postpone the operation */ \ - if ((mState.IsInitEngine() || mState.IsRecoverEngine()) && \ - mState.AsInitEngine()->mInitPromise) { \ + if (!mEngine || !mEngine->IsInited()) { \ LOG("%s is called before init", __func__); \ mPendingTasks.AppendElement(NewRunnableMethod( \ __func__, this, &ExternalEngineStateMachine::Func)); \ @@ -1213,30 +1268,25 @@ RefPtr<SetCDMPromise> ExternalEngineStateMachine::SetCDMProxy( return SetCDMPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } - if (mState.IsInitEngine() && mState.AsInitEngine()->mInitPromise) { + if (!mEngine || !mEngine->IsInited()) { LOG("SetCDMProxy is called before init"); - mState.AsInitEngine() - ->mInitPromise - ->Then( - OwnerThread(), __func__, - [self = RefPtr{this}, proxy = RefPtr{aProxy}, this]( - const GenericNonExclusivePromise::ResolveOrRejectValue& aVal) { - mInitEngineForCDMRequest.Complete(); - SetCDMProxy(proxy) - ->Then(OwnerThread(), __func__, - [self = RefPtr{this}, this]( - const SetCDMPromise::ResolveOrRejectValue& aVal) { - mSetCDMProxyRequest.Complete(); - if (aVal.IsResolve()) { - mSetCDMProxyPromise.Resolve(true, __func__); - } else { - mSetCDMProxyPromise.Reject( - NS_ERROR_DOM_MEDIA_CDM_ERR, __func__); - } - }) - ->Track(mSetCDMProxyRequest); - }) - ->Track(mInitEngineForCDMRequest); + mPendingTasks.AppendElement(NS_NewRunnableFunction( + "ExternalEngineStateMachine::SetCDMProxy", + [self = RefPtr{this}, proxy = RefPtr{aProxy}, this] { + SetCDMProxy(proxy) + ->Then(OwnerThread(), __func__, + [self = RefPtr{this}, + this](const SetCDMPromise::ResolveOrRejectValue& aVal) { + mSetCDMProxyRequest.Complete(); + if (aVal.IsResolve()) { + mSetCDMProxyPromise.Resolve(true, __func__); + } else { + mSetCDMProxyPromise.Reject(NS_ERROR_DOM_MEDIA_CDM_ERR, + __func__); + } + }) + ->Track(mSetCDMProxyRequest); + })); return mSetCDMProxyPromise.Ensure(__func__); } @@ -1244,6 +1294,7 @@ RefPtr<SetCDMPromise> ExternalEngineStateMachine::SetCDMProxy( mKeySystem = NS_ConvertUTF16toUTF8(aProxy->KeySystem()); LOG("SetCDMProxy=%p (key-system=%s)", aProxy, mKeySystem.get()); MOZ_DIAGNOSTIC_ASSERT(mEngine); + // TODO : we should check the result of setting CDM proxy in the MFCDM process if (!mEngine->SetCDMProxy(aProxy)) { LOG("Failed to set CDM proxy on the engine"); return SetCDMPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CDM_ERR, __func__); diff --git a/dom/media/ExternalEngineStateMachine.h b/dom/media/ExternalEngineStateMachine.h index 79183f894d..83250b0f3c 100644 --- a/dom/media/ExternalEngineStateMachine.h +++ b/dom/media/ExternalEngineStateMachine.h @@ -56,6 +56,9 @@ class ExternalEngineStateMachine final ExternalEngineStateMachine(MediaDecoder* aDecoder, MediaFormatReader* aReader); + RefPtr<MediaDecoder::SeekPromise> InvokeSeek( + const SeekTarget& aTarget) override; + RefPtr<GenericPromise> InvokeSetSink( const RefPtr<AudioDeviceInfo>& aSink) override; @@ -178,9 +181,9 @@ class ExternalEngineStateMachine final // crashes. struct RecoverEngine : public InitEngine {}; - StateObject() : mData(InitEngine()), mName(State::InitEngine){}; - explicit StateObject(ReadingMetadata&& aArg) - : mData(std::move(aArg)), mName(State::ReadingMetadata){}; + StateObject() : mData(ReadingMetadata()), mName(State::ReadingMetadata){}; + explicit StateObject(InitEngine&& aArg) + : mData(std::move(aArg)), mName(State::InitEngine){}; explicit StateObject(RunningEngine&& aArg) : mData(std::move(aArg)), mName(State::RunningEngine){}; explicit StateObject(SeekingData&& aArg) @@ -308,7 +311,11 @@ class ExternalEngineStateMachine final // Only used if setting CDM happens before the engine finishes initialization. MozPromiseHolder<SetCDMPromise> mSetCDMProxyPromise; MozPromiseRequestHolder<SetCDMPromise> mSetCDMProxyRequest; - MozPromiseRequestHolder<GenericNonExclusivePromise> mInitEngineForCDMRequest; + + // If seek happens while the engine is still initializing, then we would + // postpone the seek until the engine is ready. + SeekJob mPendingSeek; + MozPromiseRequestHolder<MediaDecoder::SeekPromise> mPendingSeekRequest; // It would be zero for audio-only playback. gfx::IntSize mVideoDisplay; @@ -332,9 +339,11 @@ class ExternalPlaybackEngine { virtual ~ExternalPlaybackEngine() = default; // Init the engine and specify the preload request. - virtual RefPtr<GenericNonExclusivePromise> Init(bool aShouldPreload) = 0; + virtual RefPtr<GenericNonExclusivePromise> Init(const MediaInfo& aInfo, + bool aShouldPreload) = 0; virtual void Shutdown() = 0; virtual uint64_t Id() const = 0; + virtual bool IsInited() const = 0; // Following methods should only be called after successfully initialize the // external engine. @@ -347,7 +356,6 @@ class ExternalPlaybackEngine { virtual void SetPreservesPitch(bool aPreservesPitch) = 0; virtual media::TimeUnit GetCurrentPosition() = 0; virtual void NotifyEndOfStream(TrackInfo::TrackType aType) = 0; - virtual void SetMediaInfo(const MediaInfo& aInfo) = 0; virtual bool SetCDMProxy(CDMProxy* aProxy) = 0; virtual void NotifyResizing(uint32_t aWidth, uint32_t aHeight) = 0; diff --git a/dom/media/GraphDriver.cpp b/dom/media/GraphDriver.cpp index 36c5b58864..744de30bb5 100644 --- a/dom/media/GraphDriver.cpp +++ b/dom/media/GraphDriver.cpp @@ -482,15 +482,9 @@ AudioCallbackDriver::AudioCallbackDriver( "Invalid output channel count"); MOZ_ASSERT(mOutputChannelCount <= 8); - bool allowVoice = StaticPrefs:: - media_getusermedia_microphone_prefer_voice_stream_with_processing_enabled(); -#ifdef MOZ_WIDGET_COCOA - // Using the VoiceProcessingIO audio unit on MacOS 12 causes crashes in - // OS code. - allowVoice = allowVoice && nsCocoaFeatures::macOSVersionMajor() != 12; -#endif - - if (aAudioInputType == AudioInputType::Voice && allowVoice) { + if (aAudioInputType == AudioInputType::Voice && + StaticPrefs:: + media_getusermedia_microphone_prefer_voice_stream_with_processing_enabled()) { LOG(LogLevel::Debug, ("VOICE.")); mInputDevicePreference = CUBEB_DEVICE_PREF_VOICE; CubebUtils::SetInCommunication(true); diff --git a/dom/media/MediaData.cpp b/dom/media/MediaData.cpp index fa545604e6..15774ec533 100644 --- a/dom/media/MediaData.cpp +++ b/dom/media/MediaData.cpp @@ -62,6 +62,15 @@ Span<AudioDataValue> AudioData::Data() const { return Span{GetAdjustedData(), mFrames * mChannels}; } +nsCString AudioData::ToString() const { + nsCString rv; + rv.AppendPrintf("AudioData: %s %s %" PRIu32 " frames %" PRIu32 "Hz, %" PRIu32 + "ch", + mTime.ToString().get(), mDuration.ToString().get(), mFrames, + mRate, mChannels); + return rv; +} + void AudioData::SetOriginalStartTime(const media::TimeUnit& aStartTime) { MOZ_ASSERT(mTime == mOriginalTime, "Do not call this if data has been trimmed!"); diff --git a/dom/media/MediaData.h b/dom/media/MediaData.h index 3ae8c1dbc2..f9f1aad2f1 100644 --- a/dom/media/MediaData.h +++ b/dom/media/MediaData.h @@ -23,6 +23,7 @@ # include "mozilla/gfx/Rect.h" # include "nsString.h" # include "nsTArray.h" +# include "EncoderConfig.h" namespace mozilla { @@ -379,7 +380,7 @@ class NullData : public MediaData { static const Type sType = Type::NULL_DATA; }; -// Holds chunk a decoded audio frames. +// Holds chunk a decoded interleaved audio frames. class AudioData : public MediaData { public: AudioData(int64_t aOffset, const media::TimeUnit& aTime, @@ -389,6 +390,8 @@ class AudioData : public MediaData { static const Type sType = Type::AUDIO_DATA; static const char* sTypeName; + nsCString ToString() const; + // Access the buffer as a Span. Span<AudioDataValue> Data() const; @@ -721,6 +724,9 @@ class MediaRawData final : public MediaData { // Currently this is only used for the media engine DRM playback. bool mShouldCopyCryptoToRemoteRawData = false; + // Config used to encode this packet. + UniquePtr<const EncoderConfig> mConfig; + // It's only used when the remote decoder reconstructs the media raw data. CryptoSample& GetWritableCrypto() { return mCryptoInternal; } diff --git a/dom/media/MediaDecoder.cpp b/dom/media/MediaDecoder.cpp index 23c30eed2a..159c6a6121 100644 --- a/dom/media/MediaDecoder.cpp +++ b/dom/media/MediaDecoder.cpp @@ -893,9 +893,44 @@ void MediaDecoder::FirstFrameLoaded( // We only care about video first frame. if (mInfo->HasVideo() && mMDSMCreationTime) { - mTelemetryProbesReporter->OntFirstFrameLoaded( - TimeStamp::Now() - *mMDSMCreationTime, IsMSE(), - mDecoderStateMachine->IsExternalEngineStateMachine()); + auto info = MakeUnique<dom::MediaDecoderDebugInfo>(); + RequestDebugInfo(*info)->Then( + GetMainThreadSerialEventTarget(), __func__, + [self = RefPtr<MediaDecoder>{this}, this, now = TimeStamp::Now(), + creationTime = *mMDSMCreationTime, result = std::move(info)]( + GenericPromise::ResolveOrRejectValue&& aValue) mutable { + if (IsShutdown()) { + return; + } + if (aValue.IsReject()) { + NS_WARNING("Failed to get debug info for the first frame probe!"); + return; + } + auto firstFrameLoadedTime = (now - creationTime).ToMilliseconds(); + MOZ_ASSERT(result->mReader.mTotalReadMetadataTimeMs >= 0.0); + MOZ_ASSERT(result->mReader.mTotalWaitingForVideoDataTimeMs >= 0.0); + MOZ_ASSERT(result->mStateMachine.mTotalBufferingTimeMs >= 0.0); + + using FirstFrameLoadedFlag = + TelemetryProbesReporter::FirstFrameLoadedFlag; + TelemetryProbesReporter::FirstFrameLoadedFlagSet flags; + if (IsMSE()) { + flags += FirstFrameLoadedFlag::IsMSE; + } + if (mDecoderStateMachine->IsExternalEngineStateMachine()) { + flags += FirstFrameLoadedFlag::IsExternalEngineStateMachine; + } + if (IsHLSDecoder()) { + flags += FirstFrameLoadedFlag::IsHLS; + } + if (result->mReader.mVideoHardwareAccelerated) { + flags += FirstFrameLoadedFlag::IsHardwareDecoding; + } + mTelemetryProbesReporter->OntFirstFrameLoaded( + firstFrameLoadedTime, result->mReader.mTotalReadMetadataTimeMs, + result->mReader.mTotalWaitingForVideoDataTimeMs, + result->mStateMachine.mTotalBufferingTimeMs, flags, *mInfo); + }); mMDSMCreationTime.reset(); } diff --git a/dom/media/MediaDecoder.h b/dom/media/MediaDecoder.h index a5494e9a84..033e751533 100644 --- a/dom/media/MediaDecoder.h +++ b/dom/media/MediaDecoder.h @@ -452,6 +452,8 @@ class MediaDecoder : public DecoderDoctorLifeLogger<MediaDecoder> { void GetDebugInfo(dom::MediaDecoderDebugInfo& aInfo); + virtual bool IsHLSDecoder() const { return false; } + protected: virtual ~MediaDecoder(); diff --git a/dom/media/MediaDecoderStateMachine.cpp b/dom/media/MediaDecoderStateMachine.cpp index f3cd79047b..7c84690832 100644 --- a/dom/media/MediaDecoderStateMachine.cpp +++ b/dom/media/MediaDecoderStateMachine.cpp @@ -2331,16 +2331,16 @@ class MediaDecoderStateMachine::NextFrameSeekingState } // Otherwise, we need to do the seek operation asynchronously for a special - // case (bug504613.ogv) which has no data at all, the 1st seekToNextFrame() - // operation reaches the end of the media. If we did the seek operation - // synchronously, we immediately resolve the SeekPromise in mSeekJob and - // then switch to the CompletedState which dispatches an "ended" event. - // However, the ThenValue of the SeekPromise has not yet been set, so the - // promise resolving is postponed and then the JS developer receives the - // "ended" event before the seek promise is resolved. - // An asynchronous seek operation helps to solve this issue since while the - // seek is actually performed, the ThenValue of SeekPromise has already - // been set so that it won't be postponed. + // case (video with no data)which has no data at all, the 1st + // seekToNextFrame() operation reaches the end of the media. If we did the + // seek operation synchronously, we immediately resolve the SeekPromise in + // mSeekJob and then switch to the CompletedState which dispatches an + // "ended" event. However, the ThenValue of the SeekPromise has not yet been + // set, so the promise resolving is postponed and then the JS developer + // receives the "ended" event before the seek promise is resolved. An + // asynchronous seek operation helps to solve this issue since while the + // seek is actually performed, the ThenValue of SeekPromise has already been + // set so that it won't be postponed. RefPtr<Runnable> r = mAsyncSeekTask = new AysncNextFrameSeekTask(this); nsresult rv = OwnerThread()->Dispatch(r.forget()); MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); @@ -3377,6 +3377,7 @@ void MediaDecoderStateMachine::BufferingState::Step() { } SLOG("Buffered for %.3lfs", (now - mBufferingStart).ToSeconds()); + mMaster->mTotalBufferingDuration += (now - mBufferingStart); SetDecodingState(); } @@ -3471,13 +3472,15 @@ MediaDecoderStateMachine::MediaDecoderStateMachine(MediaDecoder* aDecoder, mIsMSE(aDecoder->IsMSE()), mShouldResistFingerprinting(aDecoder->ShouldResistFingerprinting()), mSeamlessLoopingAllowed(false), + mTotalBufferingDuration(TimeDuration::Zero()), INIT_MIRROR(mStreamName, nsAutoString()), INIT_MIRROR(mSinkDevice, nullptr), INIT_MIRROR(mOutputCaptureState, MediaDecoder::OutputCaptureState::None), INIT_MIRROR(mOutputDummyTrack, nullptr), INIT_MIRROR(mOutputTracks, nsTArray<RefPtr<ProcessedMediaTrack>>()), INIT_MIRROR(mOutputPrincipal, PRINCIPAL_HANDLE_NONE), - INIT_CANONICAL(mCanonicalOutputPrincipal, PRINCIPAL_HANDLE_NONE) { + INIT_CANONICAL(mCanonicalOutputPrincipal, PRINCIPAL_HANDLE_NONE), + mShuttingDown(false) { MOZ_COUNT_CTOR(MediaDecoderStateMachine); NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); @@ -3812,6 +3815,7 @@ void MediaDecoderStateMachine::VolumeChanged() { RefPtr<ShutdownPromise> MediaDecoderStateMachine::Shutdown() { AUTO_PROFILER_LABEL("MediaDecoderStateMachine::Shutdown", MEDIA_PLAYBACK); MOZ_ASSERT(OnTaskQueue()); + mShuttingDown = true; return mStateObj->HandleShutdown(); } @@ -4694,10 +4698,15 @@ void MediaDecoderStateMachine::GetDebugInfo( aInfo.mVideoCompleted = mVideoCompleted; mStateObj->GetDebugInfo(aInfo.mStateObj); mMediaSink->GetDebugInfo(aInfo.mMediaSink); + aInfo.mTotalBufferingTimeMs = mTotalBufferingDuration.ToMilliseconds(); } RefPtr<GenericPromise> MediaDecoderStateMachine::RequestDebugInfo( dom::MediaDecoderStateMachineDebugInfo& aInfo) { + if (mShuttingDown) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + RefPtr<GenericPromise::Private> p = new GenericPromise::Private(__func__); RefPtr<MediaDecoderStateMachine> self = this; nsresult rv = OwnerThread()->Dispatch( @@ -4707,7 +4716,7 @@ RefPtr<GenericPromise> MediaDecoderStateMachine::RequestDebugInfo( p->Resolve(true, __func__); }), AbstractThread::TailDispatch); - MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); Unused << rv; return p; } diff --git a/dom/media/MediaDecoderStateMachine.h b/dom/media/MediaDecoderStateMachine.h index bcedf1790a..aa7919aa23 100644 --- a/dom/media/MediaDecoderStateMachine.h +++ b/dom/media/MediaDecoderStateMachine.h @@ -17,6 +17,7 @@ # include "MediaStatistics.h" # include "MediaTimer.h" # include "SeekJob.h" +# include "mozilla/Atomics.h" # include "mozilla/Attributes.h" # include "mozilla/ReentrantMonitor.h" # include "mozilla/StateMirroring.h" @@ -530,6 +531,9 @@ class MediaDecoderStateMachine // logic until the media loops back. bool mBypassingSkipToNextKeyFrameCheck = false; + // The total amount of time we've spent on the buffering state. + TimeDuration mTotalBufferingDuration; + private: // Audio stream name Mirror<nsAutoString> mStreamName; @@ -559,6 +563,8 @@ class MediaDecoderStateMachine // after Initialization. TaskQueue thread only. bool mIsMediaSinkSuspended = false; + Atomic<bool> mShuttingDown; + public: AbstractCanonical<PrincipalHandle>* CanonicalOutputPrincipal() { return &mCanonicalOutputPrincipal; diff --git a/dom/media/MediaDecoderStateMachineBase.h b/dom/media/MediaDecoderStateMachineBase.h index 5872151015..fb172b2d22 100644 --- a/dom/media/MediaDecoderStateMachineBase.h +++ b/dom/media/MediaDecoderStateMachineBase.h @@ -85,7 +85,8 @@ class MediaDecoderStateMachineBase { RefPtr<ShutdownPromise> BeginShutdown(); // Seeks to the decoder to aTarget asynchronously. - RefPtr<MediaDecoder::SeekPromise> InvokeSeek(const SeekTarget& aTarget); + virtual RefPtr<MediaDecoder::SeekPromise> InvokeSeek( + const SeekTarget& aTarget); virtual size_t SizeOfVideoQueue() const = 0; virtual size_t SizeOfAudioQueue() const = 0; diff --git a/dom/media/MediaFormatReader.cpp b/dom/media/MediaFormatReader.cpp index 9553e67b00..7eb8e4e5e2 100644 --- a/dom/media/MediaFormatReader.cpp +++ b/dom/media/MediaFormatReader.cpp @@ -891,7 +891,10 @@ MediaFormatReader::MediaFormatReader(MediaFormatReaderInit& aInit, "MediaFormatReader::mBuffered (Canonical)"), mFrameStats(aInit.mFrameStats), mMediaDecoderOwnerID(aInit.mMediaDecoderOwnerID), - mTrackingId(std::move(aInit.mTrackingId)) { + mTrackingId(std::move(aInit.mTrackingId)), + mReadMetadataStartTime(Nothing()), + mReadMetaDataTime(TimeDuration::Zero()), + mTotalWaitingForVideoDataTime(TimeDuration::Zero()) { MOZ_ASSERT(aDemuxer); MOZ_COUNT_CTOR(MediaFormatReader); DDLINKCHILD("audio decoder data", "MediaFormatReader::DecoderDataWithPromise", @@ -1162,6 +1165,10 @@ MediaFormatReader::AsyncReadMetadata() { return MetadataPromise::CreateAndResolve(std::move(metadata), __func__); } + if (!mReadMetadataStartTime) { + mReadMetadataStartTime = Some(TimeStamp::Now()); + } + RefPtr<MetadataPromise> p = mMetadataPromise.Ensure(__func__); mDemuxer->Init() @@ -1342,6 +1349,11 @@ void MediaFormatReader::MaybeResolveMetadataPromise() { &MediaFormatReader::NotifyTrackInfoUpdated); mIsWatchingWorkingInfo = true; + if (mReadMetadataStartTime) { + mReadMetaDataTime = TimeStamp::Now() - *mReadMetadataStartTime; + mReadMetadataStartTime.reset(); + } + mMetadataPromise.Resolve(std::move(metadata), __func__); } @@ -1490,7 +1502,7 @@ void MediaFormatReader::OnDemuxFailed(TrackType aTrack, aTrack == TrackType::kVideoTrack ? "video_demux_interruption" : "audio_demux_interruption", aError); - if (!decoder.mWaitingForData) { + if (!decoder.mWaitingForDataStartTime) { decoder.RequestDrain(); } NotifyEndOfStream(aTrack); @@ -1500,7 +1512,7 @@ void MediaFormatReader::OnDemuxFailed(TrackType aTrack, aTrack == TrackType::kVideoTrack ? "video_demux_interruption" : "audio_demux_interruption", aError); - if (!decoder.mWaitingForData) { + if (!decoder.mWaitingForDataStartTime) { decoder.RequestDrain(); } NotifyWaitingForData(aTrack); @@ -1783,7 +1795,7 @@ void MediaFormatReader::NotifyError(TrackType aTrack, void MediaFormatReader::NotifyWaitingForData(TrackType aTrack) { MOZ_ASSERT(OnTaskQueue()); auto& decoder = GetDecoderData(aTrack); - decoder.mWaitingForData = true; + decoder.mWaitingForDataStartTime = Some(TimeStamp::Now()); if (decoder.mTimeThreshold) { decoder.mTimeThreshold.ref().mWaiting = true; } @@ -1848,7 +1860,7 @@ bool MediaFormatReader::UpdateReceivedNewData(TrackType aTrack) { return false; } - // We do not want to clear mWaitingForData while there are pending + // We do not want to clear mWaitingForDataStartTime while there are pending // demuxing or seeking operations that could affect the value of this flag. // This is in order to ensure that we will retry once they complete as we may // now have new data that could potentially allow those operations to @@ -1870,7 +1882,7 @@ bool MediaFormatReader::UpdateReceivedNewData(TrackType aTrack) { } if (decoder.HasPendingDrain()) { - // We do not want to clear mWaitingForData or mDemuxEOS while + // We do not want to clear mWaitingForDataStartTime or mDemuxEOS while // a drain is in progress in order to properly complete the operation. return false; } @@ -1879,7 +1891,11 @@ bool MediaFormatReader::UpdateReceivedNewData(TrackType aTrack) { if (decoder.mTimeThreshold) { decoder.mTimeThreshold.ref().mWaiting = false; } - decoder.mWaitingForData = false; + if (aTrack == TrackType::kVideoTrack && decoder.mWaitingForDataStartTime) { + mTotalWaitingForVideoDataTime += + TimeStamp::Now() - *decoder.mWaitingForDataStartTime; + } + decoder.mWaitingForDataStartTime.reset(); if (decoder.HasFatalError()) { return false; @@ -2390,7 +2406,7 @@ void MediaFormatReader::Update(TrackType aTrack) { NotifyDecoderBenchmarkStore(); } decoder.RejectPromise(NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__); - } else if (decoder.mWaitingForData) { + } else if (decoder.mWaitingForDataStartTime) { if (decoder.mDrainState == DrainState::DrainCompleted && decoder.mLastDecodedSampleTime && !decoder.mNextStreamSourceID) { // We have completed draining the decoder following WaitingForData. @@ -2562,8 +2578,9 @@ void MediaFormatReader::Update(TrackType aTrack) { decoder.mNumSamplesOutput, uint32_t(size_t(decoder.mSizeOfQueue)), decoder.mDecodeRequest.Exists(), decoder.mFlushing, decoder.mDescription.get(), uint32_t(decoder.mOutput.Length()), - decoder.mWaitingForData, decoder.mDemuxEOS, int32_t(decoder.mDrainState), - decoder.mLastStreamSourceID, IsDecoderWaitingForCDM(aTrack)); + !!decoder.mWaitingForDataStartTime, decoder.mDemuxEOS, + int32_t(decoder.mDrainState), decoder.mLastStreamSourceID, + IsDecoderWaitingForCDM(aTrack)); if (IsWaitingOnCDMResource() || !ResolveSetCDMPromiseIfDone(aTrack)) { // If the content is encrypted, MFR won't start to create decoder until @@ -2576,7 +2593,7 @@ void MediaFormatReader::Update(TrackType aTrack) { (decoder.IsWaitingForKey())) { // Nothing more we can do at present. LOGV("Still waiting for data or key. data(%d)/key(%d)", - decoder.mWaitingForData, decoder.mWaitingForKey); + !!decoder.mWaitingForDataStartTime, decoder.mWaitingForKey); return; } @@ -3319,7 +3336,7 @@ void MediaFormatReader::GetDebugInfo(dom::MediaFormatReaderDebugInfo& aInfo) { aInfo.mAudioState.mQueueSize = AssertedCast<int32_t>(size_t(mAudio.mSizeOfQueue)); aInfo.mAudioState.mPending = AssertedCast<int>(mAudio.mOutput.Length()); - aInfo.mAudioState.mWaitingForData = mAudio.mWaitingForData; + aInfo.mAudioState.mWaitingForData = !!mAudio.mWaitingForDataStartTime; aInfo.mAudioState.mDemuxEOS = mAudio.mDemuxEOS; aInfo.mAudioState.mDrainState = int32_t(mAudio.mDrainState); aInfo.mAudioState.mWaitingForKey = mAudio.mWaitingForKey; @@ -3359,12 +3376,15 @@ void MediaFormatReader::GetDebugInfo(dom::MediaFormatReaderDebugInfo& aInfo) { aInfo.mVideoState.mQueueSize = AssertedCast<int32_t>(size_t(mVideo.mSizeOfQueue)); aInfo.mVideoState.mPending = AssertedCast<int32_t>(mVideo.mOutput.Length()); - aInfo.mVideoState.mWaitingForData = mVideo.mWaitingForData; + aInfo.mVideoState.mWaitingForData = !!mVideo.mWaitingForDataStartTime; aInfo.mVideoState.mDemuxEOS = mVideo.mDemuxEOS; aInfo.mVideoState.mDrainState = int32_t(mVideo.mDrainState); aInfo.mVideoState.mWaitingForKey = mVideo.mWaitingForKey; aInfo.mVideoState.mLastStreamSourceID = AssertedCast<int64_t>(mVideo.mLastStreamSourceID); + aInfo.mTotalReadMetadataTimeMs = mReadMetaDataTime.ToMilliseconds(); + aInfo.mTotalWaitingForVideoDataTimeMs = + mTotalWaitingForVideoDataTime.ToMilliseconds(); } CopyUTF8toUTF16(videoDecoderName, aInfo.mVideoDecoderName); diff --git a/dom/media/MediaFormatReader.h b/dom/media/MediaFormatReader.h index fcc3f20036..5c4e04172d 100644 --- a/dom/media/MediaFormatReader.h +++ b/dom/media/MediaFormatReader.h @@ -21,6 +21,7 @@ # include "mozilla/StateMirroring.h" # include "mozilla/StaticPrefs_media.h" # include "mozilla/TaskQueue.h" +# include "mozilla/TimeStamp.h" # include "mozilla/ThreadSafeWeakPtr.h" # include "mozilla/dom/MediaDebugInfoBinding.h" @@ -370,7 +371,7 @@ class MediaFormatReader final mCodecName(""), mUpdateScheduled(false), mDemuxEOS(false), - mWaitingForData(false), + mWaitingForDataStartTime(Nothing()), mWaitingForKey(false), mReceivedNewData(false), mFlushing(false), @@ -426,7 +427,7 @@ class MediaFormatReader final // Only accessed from reader's task queue. bool mUpdateScheduled; bool mDemuxEOS; - bool mWaitingForData; + Maybe<TimeStamp> mWaitingForDataStartTime; bool mWaitingForKey; bool mReceivedNewData; @@ -446,7 +447,7 @@ class MediaFormatReader final bool IsWaitingForData() const { MOZ_ASSERT(mOwner->OnTaskQueue()); - return mWaitingForData; + return !!mWaitingForDataStartTime; } bool IsWaitingForKey() const { @@ -583,7 +584,7 @@ class MediaFormatReader final void ResetState() { MOZ_ASSERT(mOwner->OnTaskQueue()); mDemuxEOS = false; - mWaitingForData = false; + mWaitingForDataStartTime.reset(); mQueuedSamples.Clear(); mDecodeRequest.DisconnectIfExists(); mDrainRequest.DisconnectIfExists(); @@ -885,6 +886,16 @@ class MediaFormatReader final Maybe<uint64_t> mMediaEngineId; const Maybe<TrackingId> mTrackingId; + + // The start time of reading the metdata and how long does it take. This + // measurement includes the time of downloading media resource over the + // internet. + Maybe<TimeStamp> mReadMetadataStartTime; + TimeDuration mReadMetaDataTime; + + // The total amount of time we have been waiting for the video data due to + // lacking of data. + TimeDuration mTotalWaitingForVideoDataTime; }; } // namespace mozilla diff --git a/dom/media/MediaInfo.h b/dom/media/MediaInfo.h index 73704d1593..7ab5df4e0a 100644 --- a/dom/media/MediaInfo.h +++ b/dom/media/MediaInfo.h @@ -109,12 +109,16 @@ struct FlacCodecSpecificData { RefPtr<MediaByteBuffer> mStreamInfoBinaryBlob{new MediaByteBuffer}; }; -struct Mp3CodecSpecificData { +struct Mp3CodecSpecificData final { bool operator==(const Mp3CodecSpecificData& rhs) const { return mEncoderDelayFrames == rhs.mEncoderDelayFrames && mEncoderPaddingFrames == rhs.mEncoderPaddingFrames; } + auto MutTiedFields() { + return std::tie(mEncoderDelayFrames, mEncoderPaddingFrames); + } + // The number of frames that should be skipped from the beginning of the // decoded stream. // See https://bugzilla.mozilla.org/show_bug.cgi?id=1566389 for more info. @@ -558,7 +562,7 @@ class AudioInfo : public TrackInfo { bool operator==(const AudioInfo& rhs) const; - static const uint32_t MAX_RATE = 640000; + static const uint32_t MAX_RATE = 768000; static const uint32_t MAX_CHANNEL_COUNT = 256; bool IsValid() const override { diff --git a/dom/media/MediaManager.cpp b/dom/media/MediaManager.cpp index 422769587a..fb4384c826 100644 --- a/dom/media/MediaManager.cpp +++ b/dom/media/MediaManager.cpp @@ -2227,6 +2227,7 @@ MediaManager::MediaManager(already_AddRefed<TaskQueue> aMediaThread) mPrefs.mNoiseOn = false; mPrefs.mTransientOn = false; mPrefs.mAgc2Forced = false; + mPrefs.mExpectDrift = -1; // auto #ifdef MOZ_WEBRTC mPrefs.mAgc = webrtc::AudioProcessing::Config::GainController1::Mode::kAdaptiveDigital; @@ -3482,17 +3483,26 @@ void MediaManager::GetPrefs(nsIPrefBranch* aBranch, const char* aData) { &mPrefs.mTransientOn); GetPrefBool(aBranch, "media.getusermedia.agc2_forced", aData, &mPrefs.mAgc2Forced); + // Use 0 or 1 to force to false or true + // EchoCanceller3Config::echo_removal_control.has_clock_drift. + // -1 is the default, which means automatically set has_clock_drift as + // deemed appropriate. + GetPref(aBranch, "media.getusermedia.audio.processing.aec.expect_drift", + aData, &mPrefs.mExpectDrift); GetPref(aBranch, "media.getusermedia.agc", aData, &mPrefs.mAgc); GetPref(aBranch, "media.getusermedia.noise", aData, &mPrefs.mNoise); GetPref(aBranch, "media.getusermedia.channels", aData, &mPrefs.mChannels); #endif LOG("%s: default prefs: %dx%d @%dfps, %dHz test tones, aec: %s, " - "agc: %s, hpf: %s, noise: %s, agc level: %d, agc version: %s, noise " - "level: %d, transient: %s, channels %d", + "agc: %s, hpf: %s, noise: %s, drift: %s, agc level: %d, agc version: %s, " + "noise level: %d, transient: %s, channels %d", __FUNCTION__, mPrefs.mWidth, mPrefs.mHeight, mPrefs.mFPS, mPrefs.mFreq, mPrefs.mAecOn ? "on" : "off", mPrefs.mAgcOn ? "on" : "off", - mPrefs.mHPFOn ? "on" : "off", mPrefs.mNoiseOn ? "on" : "off", mPrefs.mAgc, - mPrefs.mAgc2Forced ? "2" : "1", mPrefs.mNoise, + mPrefs.mHPFOn ? "on" : "off", mPrefs.mNoiseOn ? "on" : "off", + mPrefs.mExpectDrift < 0 ? "auto" + : mPrefs.mExpectDrift ? "on" + : "off", + mPrefs.mAgc, mPrefs.mAgc2Forced ? "2" : "1", mPrefs.mNoise, mPrefs.mTransientOn ? "on" : "off", mPrefs.mChannels); } diff --git a/dom/media/MediaRecorder.cpp b/dom/media/MediaRecorder.cpp index 42cd655093..be3b97cc99 100644 --- a/dom/media/MediaRecorder.cpp +++ b/dom/media/MediaRecorder.cpp @@ -569,7 +569,9 @@ void SelectBitrates(uint32_t aBitsPerSecond, uint8_t aNumVideoTracks, */ class MediaRecorder::Session : public PrincipalChangeObserver<MediaStreamTrack>, public DOMMediaStream::TrackListener { - NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Session) + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Session, + DOMMediaStream::TrackListener) struct TrackTypeComparator { enum Type { @@ -1170,6 +1172,14 @@ class MediaRecorder::Session : public PrincipalChangeObserver<MediaStreamTrack>, RefPtr<ShutdownBlocker> mShutdownBlocker; }; +NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaRecorder::Session, + DOMMediaStream::TrackListener, mMediaStream, + mMediaStreamTracks) +NS_IMPL_ADDREF_INHERITED(MediaRecorder::Session, DOMMediaStream::TrackListener) +NS_IMPL_RELEASE_INHERITED(MediaRecorder::Session, DOMMediaStream::TrackListener) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaRecorder::Session) +NS_INTERFACE_MAP_END_INHERITING(DOMMediaStream::TrackListener) + MediaRecorder::~MediaRecorder() { LOG(LogLevel::Debug, ("~MediaRecorder (%p)", this)); UnRegisterActivityObserver(); diff --git a/dom/media/MediaStreamWindowCapturer.cpp b/dom/media/MediaStreamWindowCapturer.cpp index 142242eff0..0e3eca801b 100644 --- a/dom/media/MediaStreamWindowCapturer.cpp +++ b/dom/media/MediaStreamWindowCapturer.cpp @@ -13,6 +13,15 @@ namespace mozilla { using dom::AudioStreamTrack; using dom::MediaStreamTrack; +NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaStreamWindowCapturer, + DOMMediaStream::TrackListener) +NS_IMPL_ADDREF_INHERITED(MediaStreamWindowCapturer, + DOMMediaStream::TrackListener) +NS_IMPL_RELEASE_INHERITED(MediaStreamWindowCapturer, + DOMMediaStream::TrackListener) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaStreamWindowCapturer) +NS_INTERFACE_MAP_END_INHERITING(DOMMediaStream::TrackListener) + MediaStreamWindowCapturer::CapturedTrack::CapturedTrack( MediaStreamTrack* aTrack, uint64_t aWindowID) : mTrack(aTrack), @@ -28,7 +37,6 @@ MediaStreamWindowCapturer::CapturedTrack::~CapturedTrack() { MediaStreamWindowCapturer::MediaStreamWindowCapturer(DOMMediaStream* aStream, uint64_t aWindowId) : mStream(aStream), mWindowId(aWindowId) { - mStream->RegisterTrackListener(this); nsTArray<RefPtr<AudioStreamTrack>> tracks; mStream->GetAudioTracks(tracks); for (const auto& t : tracks) { @@ -39,11 +47,7 @@ MediaStreamWindowCapturer::MediaStreamWindowCapturer(DOMMediaStream* aStream, } } -MediaStreamWindowCapturer::~MediaStreamWindowCapturer() { - if (mStream) { - mStream->UnregisterTrackListener(this); - } -} +MediaStreamWindowCapturer::~MediaStreamWindowCapturer() = default; void MediaStreamWindowCapturer::NotifyTrackAdded( const RefPtr<MediaStreamTrack>& aTrack) { diff --git a/dom/media/MediaStreamWindowCapturer.h b/dom/media/MediaStreamWindowCapturer.h index 8a6695ed43..dfd6062c3c 100644 --- a/dom/media/MediaStreamWindowCapturer.h +++ b/dom/media/MediaStreamWindowCapturer.h @@ -24,7 +24,9 @@ class MediaInputPort; class MediaStreamWindowCapturer : public DOMMediaStream::TrackListener { public: MediaStreamWindowCapturer(DOMMediaStream* aStream, uint64_t aWindowId); - ~MediaStreamWindowCapturer(); + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaStreamWindowCapturer, + DOMMediaStream::TrackListener) void NotifyTrackAdded(const RefPtr<dom::MediaStreamTrack>& aTrack) override; void NotifyTrackRemoved(const RefPtr<dom::MediaStreamTrack>& aTrack) override; @@ -41,6 +43,7 @@ class MediaStreamWindowCapturer : public DOMMediaStream::TrackListener { const uint64_t mWindowId; protected: + ~MediaStreamWindowCapturer(); void AddTrack(dom::AudioStreamTrack* aTrack); void RemoveTrack(dom::AudioStreamTrack* aTrack); diff --git a/dom/media/MediaTrackGraph.cpp b/dom/media/MediaTrackGraph.cpp index 2af7aacb1f..a4f81d0071 100644 --- a/dom/media/MediaTrackGraph.cpp +++ b/dom/media/MediaTrackGraph.cpp @@ -4070,6 +4070,9 @@ double MediaTrackGraphImpl::AudioOutputLatency() { return mAudioOutputLatency; } +bool MediaTrackGraph::OutputForAECMightDrift() { + return static_cast<MediaTrackGraphImpl*>(this)->OutputForAECMightDrift(); +} bool MediaTrackGraph::IsNonRealtime() const { return !static_cast<const MediaTrackGraphImpl*>(this)->mRealtime; } diff --git a/dom/media/MediaTrackGraph.h b/dom/media/MediaTrackGraph.h index 8afeb1dd0d..a754b158eb 100644 --- a/dom/media/MediaTrackGraph.h +++ b/dom/media/MediaTrackGraph.h @@ -1200,6 +1200,10 @@ class MediaTrackGraph { } double AudioOutputLatency(); + /* Return whether the clock for the audio output device used for the AEC + * reverse stream might drift from the clock for this MediaTrackGraph. + * Graph thread only. */ + bool OutputForAECMightDrift(); void RegisterCaptureTrackForWindow(uint64_t aWindowId, ProcessedMediaTrack* aCaptureTrack); diff --git a/dom/media/MediaTrackGraphImpl.h b/dom/media/MediaTrackGraphImpl.h index e733b961ff..5daed83ef3 100644 --- a/dom/media/MediaTrackGraphImpl.h +++ b/dom/media/MediaTrackGraphImpl.h @@ -571,7 +571,12 @@ class MediaTrackGraphImpl : public MediaTrackGraph, void SetMaxOutputChannelCount(uint32_t aMaxChannelCount); double AudioOutputLatency(); - + /* Return whether the clock for the audio output device used for the AEC + * reverse stream might drift from the clock for this MediaTrackGraph. */ + bool OutputForAECMightDrift() { + AssertOnGraphThread(); + return mOutputDeviceForAEC != PrimaryOutputDeviceID(); + } /** * The audio input channel count for a MediaTrackGraph is the max of all the * channel counts requested by the listeners. The max channel count is @@ -1115,12 +1120,14 @@ class MediaTrackGraphImpl : public MediaTrackGraph, const float mGlobalVolume; #ifdef DEBUG + protected: /** * Used to assert when AppendMessage() runs control messages synchronously. */ bool mCanRunMessagesSynchronously; #endif + private: /** * The graph's main-thread observable graph time. * Updated by the stable state runnable after each iteration. diff --git a/dom/media/PeerConnection.sys.mjs b/dom/media/PeerConnection.sys.mjs index 00b4023c2f..a775a92d99 100644 --- a/dom/media/PeerConnection.sys.mjs +++ b/dom/media/PeerConnection.sys.mjs @@ -217,6 +217,51 @@ setupPrototype(GlobalPCList, { var _globalPCList = new GlobalPCList(); +// Parses grammar in RFC5245 section 15 and ICE TCP from RFC6544 section 4.5. +function parseCandidate(line) { + const match = line.match( + /^(a=)?candidate:([A-Za-z0-9+\/]{1,32}) (\d+) (UDP|TCP) (\d+) ([A-Za-z0-9.:-]+) (\d+) typ (host|srflx|prflx|relay)(?: raddr ([A-Za-z0-9.:-]+) rport (\d+))?(.*)$/i + ); + if (!match) { + return null; + } + const candidate = { + foundation: match[2], + componentId: parseInt(match[3], 10), + transport: match[4], + priority: parseInt(match[5], 10), + address: match[6], + port: parseInt(match[7], 10), + type: match[8], + relatedAddress: match[9], + relatedPort: match[10], + }; + if (candidate.componentId < 1 || candidate.componentId > 256) { + return null; + } + if (candidate.priority < 0 || candidate.priority > 4294967295) { + return null; + } + if (candidate.port < 0 || candidate.port > 65535) { + return null; + } + candidate.component = { 1: "rtp", 2: "rtcp" }[candidate.componentId] || null; + candidate.protocol = + { udp: "udp", tcp: "tcp" }[candidate.transport.toLowerCase()] || null; + + const tcpTypeMatch = match[11].match(/tcptype (\S+)/i); + if (tcpTypeMatch) { + candidate.tcpType = tcpTypeMatch[1]; + if ( + candidate.protocol != "tcp" || + !["active", "passive", "so"].includes(candidate.tcpType) + ) { + return null; + } + } + return candidate; +} + export class RTCIceCandidate { init(win) { this._win = win; @@ -229,6 +274,20 @@ export class RTCIceCandidate { ); } Object.assign(this, dict); + const candidate = parseCandidate(this.candidate); + if (!candidate) { + return; + } + Object.assign(this, candidate); + } + + toJSON() { + return { + candidate: this.candidate, + sdpMid: this.sdpMid, + sdpMLineIndex: this.sdpMLineIndex, + usernameFragment: this.usernameFragment, + }; } } diff --git a/dom/media/TimedPacketizer.h b/dom/media/TimedPacketizer.h new file mode 100644 index 0000000000..dbccc77e61 --- /dev/null +++ b/dom/media/TimedPacketizer.h @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_MEDIA_TIMEPACKETIZER_H_ +#define DOM_MEDIA_TIMEPACKETIZER_H_ + +#include "AudioPacketizer.h" +#include "TimeUnits.h" + +namespace mozilla { +/** + * This class wraps an AudioPacketizer and provides packets of audio with + * timestamps. + */ +template <typename InputType, typename OutputType> +class TimedPacketizer { + public: + TimedPacketizer(uint32_t aPacketSize, uint32_t aChannels, + int64_t aInitialTick, int64_t aBase) + : mPacketizer(aPacketSize, aChannels), + mTimeStamp(media::TimeUnit(aInitialTick, aBase)), + mBase(aBase) {} + + void Input(const InputType* aFrames, uint32_t aFrameCount) { + mPacketizer.Input(aFrames, aFrameCount); + } + + media::TimeUnit Output(OutputType* aOutputBuffer) { + MOZ_ASSERT(mPacketizer.PacketsAvailable()); + media::TimeUnit pts = mTimeStamp; + mPacketizer.Output(aOutputBuffer); + mTimeStamp += media::TimeUnit(mPacketizer.mPacketSize, mBase); + return pts; + } + + media::TimeUnit Drain(OutputType* aOutputBuffer, uint32_t& aWritten) { + MOZ_ASSERT(!mPacketizer.PacketsAvailable(), + "Consume all packets before calling drain"); + media::TimeUnit pts = mTimeStamp; + aWritten = mPacketizer.Output(aOutputBuffer); + mTimeStamp += media::TimeUnit(mPacketizer.mPacketSize, mBase); + return pts; + } + + // Call this when a discontinuity in input has been detected, e.g. based on + // the pts of input packets. + void Discontinuity(int64_t aNewTick) { + MOZ_ASSERT(!mPacketizer.FramesAvailable(), + "Drain before enqueueing discontinuous audio"); + mTimeStamp = media::TimeUnit(aNewTick, mBase); + } + + void Clear() { mPacketizer.Clear(); } + + uint32_t PacketSize() const { return mPacketizer.mPacketSize; } + + uint32_t ChannelCount() const { return mPacketizer.mChannels; } + + uint32_t PacketsAvailable() const { return mPacketizer.PacketsAvailable(); } + + uint32_t FramesAvailable() const { return mPacketizer.FramesAvailable(); } + + private: + AudioPacketizer<InputType, OutputType> mPacketizer; + media::TimeUnit mTimeStamp; + uint64_t mBase; +}; + +} // namespace mozilla + +#endif // DOM_MEDIA_TIMEPACKETIZER_H_ diff --git a/dom/media/VideoUtils.cpp b/dom/media/VideoUtils.cpp index 90057f7c83..24b1f0dd59 100644 --- a/dom/media/VideoUtils.cpp +++ b/dom/media/VideoUtils.cpp @@ -188,12 +188,13 @@ uint32_t DecideAudioPlaybackSampleRate(const AudioInfo& aInfo, rate = 48000; } else if (aInfo.mRate >= 44100) { // The original rate is of good quality and we want to minimize unecessary - // resampling, so we let cubeb decide how to resample (if needed). - rate = aInfo.mRate; + // resampling, so we let cubeb decide how to resample (if needed). Cap to + // 384kHz for good measure. + rate = std::min<unsigned>(aInfo.mRate, 384000u); } else { // We will resample all data to match cubeb's preferred sampling rate. rate = CubebUtils::PreferredSampleRate(aShouldResistFingerprinting); - if (rate > 384000) { + if (rate > 768000) { // bogus rate, fall back to something else; rate = 48000; } diff --git a/dom/media/VideoUtils.h b/dom/media/VideoUtils.h index a2fa7208f5..b1dbb0cf2b 100644 --- a/dom/media/VideoUtils.h +++ b/dom/media/VideoUtils.h @@ -194,34 +194,6 @@ enum class MediaThreadType { // for decoding streams. already_AddRefed<SharedThreadPool> GetMediaThreadPool(MediaThreadType aType); -enum H264_PROFILE { - H264_PROFILE_UNKNOWN = 0, - H264_PROFILE_BASE = 0x42, - H264_PROFILE_MAIN = 0x4D, - H264_PROFILE_EXTENDED = 0x58, - H264_PROFILE_HIGH = 0x64, -}; - -enum H264_LEVEL { - H264_LEVEL_1 = 10, - H264_LEVEL_1_b = 11, - H264_LEVEL_1_1 = 11, - H264_LEVEL_1_2 = 12, - H264_LEVEL_1_3 = 13, - H264_LEVEL_2 = 20, - H264_LEVEL_2_1 = 21, - H264_LEVEL_2_2 = 22, - H264_LEVEL_3 = 30, - H264_LEVEL_3_1 = 31, - H264_LEVEL_3_2 = 32, - H264_LEVEL_4 = 40, - H264_LEVEL_4_1 = 41, - H264_LEVEL_4_2 = 42, - H264_LEVEL_5 = 50, - H264_LEVEL_5_1 = 51, - H264_LEVEL_5_2 = 52 -}; - // Extracts the H.264/AVC profile and level from an H.264 codecs string. // H.264 codecs parameters have a type defined as avc1.PPCCLL, where // PP = profile_idc, CC = constraint_set flags, LL = level_idc. diff --git a/dom/media/autoplay/test/mochitest/mochitest.toml b/dom/media/autoplay/test/mochitest/mochitest.toml index 0b6f0a169f..76cb353ffe 100644 --- a/dom/media/autoplay/test/mochitest/mochitest.toml +++ b/dom/media/autoplay/test/mochitest/mochitest.toml @@ -3,7 +3,6 @@ subsuite = "media" tags = "autoplay" support-files = [ "../../../test/manifest.js", - "../../../test/320x240.ogv", "../../../test/bogus.duh", "../../../test/detodos-short.opus", "../../../test/flac-s24.flac", @@ -23,6 +22,7 @@ support-files = [ "../../../test/small-shot-mp3.mp4", "../../../test/small-shot.ogg", "../../../test/vp9-short.webm", + "../../../test/vp9.webm", "AutoplayTestUtils.js", "file_autoplay_gv_play_request_frame.html", "file_autoplay_gv_play_request_window.html", diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_play_before_loadedmetadata.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_play_before_loadedmetadata.html index b5f70be227..d76aa96348 100644 --- a/dom/media/autoplay/test/mochitest/test_autoplay_policy_play_before_loadedmetadata.html +++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_play_before_loadedmetadata.html @@ -28,12 +28,12 @@ let testCases = [ { - resource: "320x240.ogv", // Only video track. + resource: "vp9.webm", // Only video track. shouldPlay: false, muted: false, }, { - resource: "320x240.ogv", // Only video track. + resource: "vp9.webm", // Only video track. shouldPlay: true, muted: true, }, diff --git a/dom/media/eme/EMEUtils.cpp b/dom/media/eme/EMEUtils.cpp index 5a6b645df2..19639388da 100644 --- a/dom/media/eme/EMEUtils.cpp +++ b/dom/media/eme/EMEUtils.cpp @@ -10,8 +10,10 @@ #include "MediaData.h" #include "KeySystemConfig.h" #include "mozilla/StaticPrefs_media.h" +#include "mozilla/dom/Document.h" #include "mozilla/dom/KeySystemNames.h" #include "mozilla/dom/UnionTypes.h" +#include "nsContentUtils.h" #ifdef MOZ_WMF_CDM # include "mozilla/PMFCDM.h" @@ -49,37 +51,33 @@ bool IsWidevineKeySystem(const nsAString& aKeySystem) { } #ifdef MOZ_WMF_CDM -bool IsPlayReadyKeySystemAndSupported(const nsAString& aKeySystem) { - if (!StaticPrefs::media_eme_playready_enabled()) { - return false; - } +bool IsPlayReadyEnabled() { // 1=enabled encrypted and clear, 2=enabled encrytped. - if (StaticPrefs::media_wmf_media_engine_enabled() != 1 && - StaticPrefs::media_wmf_media_engine_enabled() != 2) { - return false; - } - return IsPlayReadyKeySystem(aKeySystem); + return StaticPrefs::media_eme_playready_enabled() && + (StaticPrefs::media_wmf_media_engine_enabled() == 1 || + StaticPrefs::media_wmf_media_engine_enabled() == 2); } -bool IsPlayReadyKeySystem(const nsAString& aKeySystem) { +bool IsPlayReadyKeySystemAndSupported(const nsAString& aKeySystem) { + if (!IsPlayReadyEnabled()) { + return false; + } return aKeySystem.EqualsLiteral(kPlayReadyKeySystemName) || aKeySystem.EqualsLiteral(kPlayReadyKeySystemHardware) || aKeySystem.EqualsLiteral(kPlayReadyHardwareClearLeadKeySystemName); } -bool IsWidevineExperimentKeySystemAndSupported(const nsAString& aKeySystem) { - if (!StaticPrefs::media_eme_widevine_experiment_enabled()) { - return false; - } +bool IsWidevineHardwareDecryptionEnabled() { // 1=enabled encrypted and clear, 2=enabled encrytped. - if (StaticPrefs::media_wmf_media_engine_enabled() != 1 && - StaticPrefs::media_wmf_media_engine_enabled() != 2) { - return false; - } - return IsWidevineExperimentKeySystem(aKeySystem); + return StaticPrefs::media_eme_widevine_experiment_enabled() && + (StaticPrefs::media_wmf_media_engine_enabled() == 1 || + StaticPrefs::media_wmf_media_engine_enabled() == 2); } -bool IsWidevineExperimentKeySystem(const nsAString& aKeySystem) { +bool IsWidevineExperimentKeySystemAndSupported(const nsAString& aKeySystem) { + if (!IsWidevineHardwareDecryptionEnabled()) { + return false; + } return aKeySystem.EqualsLiteral(kWidevineExperimentKeySystemName) || aKeySystem.EqualsLiteral(kWidevineExperiment2KeySystemName); } @@ -121,26 +119,6 @@ nsString KeySystemToProxyName(const nsAString& aKeySystem) { return u""_ns; } -#define ENUM_TO_STR(enumVal) \ - case enumVal: \ - return #enumVal - -const char* ToMediaKeyStatusStr(dom::MediaKeyStatus aStatus) { - switch (aStatus) { - ENUM_TO_STR(dom::MediaKeyStatus::Usable); - ENUM_TO_STR(dom::MediaKeyStatus::Expired); - ENUM_TO_STR(dom::MediaKeyStatus::Released); - ENUM_TO_STR(dom::MediaKeyStatus::Output_restricted); - ENUM_TO_STR(dom::MediaKeyStatus::Output_downscaled); - ENUM_TO_STR(dom::MediaKeyStatus::Status_pending); - ENUM_TO_STR(dom::MediaKeyStatus::Internal_error); - default: - return "Undefined MediaKeyStatus!"; - } -} - -#undef ENUM_TO_STR - bool IsHardwareDecryptionSupported( const dom::MediaKeySystemConfiguration& aConfig) { for (const auto& capabilities : aConfig.mAudioCapabilities) { @@ -223,7 +201,9 @@ void MFCDMCapabilitiesIPDLToKeySystemConfig( aKeySystemConfig.mEncryptionSchemes.AppendElement( NS_ConvertUTF8toUTF16(EncryptionSchemeStr(scheme))); } - aKeySystemConfig.mIsHDCP22Compatible = aCDMConfig.isHDCP22Compatible(); + aKeySystemConfig.mIsHDCP22Compatible = aCDMConfig.isHDCP22Compatible() + ? *aCDMConfig.isHDCP22Compatible() + : false; EME_LOG("New Capabilities=%s", NS_ConvertUTF16toUTF8(aKeySystemConfig.GetDebugInfo()).get()); } @@ -270,4 +250,21 @@ bool DoesKeySystemSupportHardwareDecryption(const nsAString& aKeySystem) { return false; } +void DeprecationWarningLog(const dom::Document* aDocument, + const char* aMsgName) { + if (!aDocument || !aMsgName) { + return; + } + EME_LOG("DeprecationWarning Logging deprecation warning '%s' to WebConsole.", + aMsgName); + nsTHashMap<nsCharPtrHashKey, bool> warnings; + warnings.InsertOrUpdate(aMsgName, true); + AutoTArray<nsString, 1> params; + nsString& uri = *params.AppendElement(); + Unused << aDocument->GetDocumentURI(uri); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Media"_ns, + aDocument, nsContentUtils::eDOM_PROPERTIES, + aMsgName, params); +} + } // namespace mozilla diff --git a/dom/media/eme/EMEUtils.h b/dom/media/eme/EMEUtils.h index 3fbf22f359..424346645c 100644 --- a/dom/media/eme/EMEUtils.h +++ b/dom/media/eme/EMEUtils.h @@ -23,7 +23,8 @@ struct KeySystemConfig; namespace dom { class ArrayBufferViewOrArrayBuffer; -} +class Document; +} // namespace dom #ifndef EME_LOG LogModule* GetEMELog(); @@ -61,14 +62,14 @@ bool IsClearkeyKeySystem(const nsAString& aKeySystem); bool IsWidevineKeySystem(const nsAString& aKeySystem); #ifdef MOZ_WMF_CDM +bool IsPlayReadyEnabled(); + bool IsPlayReadyKeySystemAndSupported(const nsAString& aKeySystem); -bool IsPlayReadyKeySystem(const nsAString& aKeySystem); +bool IsWidevineHardwareDecryptionEnabled(); bool IsWidevineExperimentKeySystemAndSupported(const nsAString& aKeySystem); -bool IsWidevineExperimentKeySystem(const nsAString& aKeySystem); - bool IsWMFClearKeySystemAndSupported(const nsAString& aKeySystem); #endif @@ -107,6 +108,9 @@ bool CheckIfHarewareDRMConfigExists( bool DoesKeySystemSupportHardwareDecryption(const nsAString& aKeySystem); +void DeprecationWarningLog(const dom::Document* aDocument, + const char* aMsgName); + } // namespace mozilla #endif // EME_LOG_H_ diff --git a/dom/media/eme/KeySystemConfig.cpp b/dom/media/eme/KeySystemConfig.cpp index 0cb5da1a56..8f3227ecf6 100644 --- a/dom/media/eme/KeySystemConfig.cpp +++ b/dom/media/eme/KeySystemConfig.cpp @@ -68,221 +68,262 @@ bool KeySystemConfig::Supports(const nsAString& aKeySystem) { return false; } -/* static */ -bool KeySystemConfig::CreateKeySystemConfigs( - const nsAString& aKeySystem, const DecryptionInfo aDecryption, +/* static */ void KeySystemConfig::CreateClearKeyKeySystemConfigs( + const KeySystemConfigRequest& aRequest, nsTArray<KeySystemConfig>& aOutConfigs) { - if (!Supports(aKeySystem)) { - return false; + KeySystemConfig* config = aOutConfigs.AppendElement(); + config->mKeySystem = aRequest.mKeySystem; + config->mInitDataTypes.AppendElement(u"cenc"_ns); + config->mInitDataTypes.AppendElement(u"keyids"_ns); + config->mInitDataTypes.AppendElement(u"webm"_ns); + config->mPersistentState = Requirement::Optional; + config->mDistinctiveIdentifier = Requirement::NotAllowed; + config->mSessionTypes.AppendElement(SessionType::Temporary); + config->mEncryptionSchemes.AppendElement(u"cenc"_ns); + config->mEncryptionSchemes.AppendElement(u"cbcs"_ns); + config->mEncryptionSchemes.AppendElement(u"cbcs-1-9"_ns); + if (StaticPrefs::media_clearkey_persistent_license_enabled()) { + config->mSessionTypes.AppendElement(SessionType::PersistentLicense); } - - if (IsClearkeyKeySystem(aKeySystem)) { - KeySystemConfig* config = aOutConfigs.AppendElement(); - config->mKeySystem = aKeySystem; - config->mInitDataTypes.AppendElement(u"cenc"_ns); - config->mInitDataTypes.AppendElement(u"keyids"_ns); - config->mInitDataTypes.AppendElement(u"webm"_ns); - config->mPersistentState = Requirement::Optional; - config->mDistinctiveIdentifier = Requirement::NotAllowed; - config->mSessionTypes.AppendElement(SessionType::Temporary); - config->mEncryptionSchemes.AppendElement(u"cenc"_ns); - config->mEncryptionSchemes.AppendElement(u"cbcs"_ns); - config->mEncryptionSchemes.AppendElement(u"cbcs-1-9"_ns); - if (StaticPrefs::media_clearkey_persistent_license_enabled()) { - config->mSessionTypes.AppendElement(SessionType::PersistentLicense); - } #if defined(XP_WIN) - // Clearkey CDM uses WMF's H.264 decoder on Windows. - if (WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::H264)) { - config->mMP4.SetCanDecryptAndDecode(EME_CODEC_H264); - } else { - config->mMP4.SetCanDecrypt(EME_CODEC_H264); - } -#else + // Clearkey CDM uses WMF's H.264 decoder on Windows. + if (WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::H264)) { + config->mMP4.SetCanDecryptAndDecode(EME_CODEC_H264); + } else { config->mMP4.SetCanDecrypt(EME_CODEC_H264); + } +#else + config->mMP4.SetCanDecrypt(EME_CODEC_H264); #endif - config->mMP4.SetCanDecrypt(EME_CODEC_AAC); - config->mMP4.SetCanDecrypt(EME_CODEC_FLAC); - config->mMP4.SetCanDecrypt(EME_CODEC_OPUS); - config->mMP4.SetCanDecrypt(EME_CODEC_VP9); + config->mMP4.SetCanDecrypt(EME_CODEC_AAC); + config->mMP4.SetCanDecrypt(EME_CODEC_FLAC); + config->mMP4.SetCanDecrypt(EME_CODEC_OPUS); + config->mMP4.SetCanDecrypt(EME_CODEC_VP9); #ifdef MOZ_AV1 - config->mMP4.SetCanDecrypt(EME_CODEC_AV1); + config->mMP4.SetCanDecrypt(EME_CODEC_AV1); #endif - config->mWebM.SetCanDecrypt(EME_CODEC_VORBIS); - config->mWebM.SetCanDecrypt(EME_CODEC_OPUS); - config->mWebM.SetCanDecrypt(EME_CODEC_VP8); - config->mWebM.SetCanDecrypt(EME_CODEC_VP9); + config->mWebM.SetCanDecrypt(EME_CODEC_VORBIS); + config->mWebM.SetCanDecrypt(EME_CODEC_OPUS); + config->mWebM.SetCanDecrypt(EME_CODEC_VP8); + config->mWebM.SetCanDecrypt(EME_CODEC_VP9); #ifdef MOZ_AV1 - config->mWebM.SetCanDecrypt(EME_CODEC_AV1); + config->mWebM.SetCanDecrypt(EME_CODEC_AV1); #endif - if (StaticPrefs::media_clearkey_test_key_systems_enabled()) { - // Add testing key systems. These offer the same capabilities as the - // base clearkey system, so just clone clearkey and change the name. - KeySystemConfig clearkeyWithProtectionQuery{*config}; - clearkeyWithProtectionQuery.mKeySystem.AssignLiteral( - kClearKeyWithProtectionQueryKeySystemName); - aOutConfigs.AppendElement(std::move(clearkeyWithProtectionQuery)); - } - return true; + if (StaticPrefs::media_clearkey_test_key_systems_enabled()) { + // Add testing key systems. These offer the same capabilities as the + // base clearkey system, so just clone clearkey and change the name. + KeySystemConfig clearkeyWithProtectionQuery{*config}; + clearkeyWithProtectionQuery.mKeySystem.AssignLiteral( + kClearKeyWithProtectionQueryKeySystemName); + aOutConfigs.AppendElement(std::move(clearkeyWithProtectionQuery)); } +} - if (IsWidevineKeySystem(aKeySystem)) { - KeySystemConfig* config = aOutConfigs.AppendElement(); - config->mKeySystem = aKeySystem; - config->mInitDataTypes.AppendElement(u"cenc"_ns); - config->mInitDataTypes.AppendElement(u"keyids"_ns); - config->mInitDataTypes.AppendElement(u"webm"_ns); - config->mPersistentState = Requirement::Optional; - config->mDistinctiveIdentifier = Requirement::NotAllowed; - config->mSessionTypes.AppendElement(SessionType::Temporary); +/* static */ void KeySystemConfig::CreateWivineL3KeySystemConfigs( + const KeySystemConfigRequest& aRequest, + nsTArray<KeySystemConfig>& aOutConfigs) { + KeySystemConfig* config = aOutConfigs.AppendElement(); + config->mKeySystem = aRequest.mKeySystem; + config->mInitDataTypes.AppendElement(u"cenc"_ns); + config->mInitDataTypes.AppendElement(u"keyids"_ns); + config->mInitDataTypes.AppendElement(u"webm"_ns); + config->mPersistentState = Requirement::Optional; + config->mDistinctiveIdentifier = Requirement::NotAllowed; + config->mSessionTypes.AppendElement(SessionType::Temporary); #ifdef MOZ_WIDGET_ANDROID - config->mSessionTypes.AppendElement(SessionType::PersistentLicense); + config->mSessionTypes.AppendElement(SessionType::PersistentLicense); #endif - config->mAudioRobustness.AppendElement(u"SW_SECURE_CRYPTO"_ns); - config->mVideoRobustness.AppendElement(u"SW_SECURE_CRYPTO"_ns); - config->mVideoRobustness.AppendElement(u"SW_SECURE_DECODE"_ns); - config->mEncryptionSchemes.AppendElement(u"cenc"_ns); - config->mEncryptionSchemes.AppendElement(u"cbcs"_ns); - config->mEncryptionSchemes.AppendElement(u"cbcs-1-9"_ns); + config->mAudioRobustness.AppendElement(u"SW_SECURE_CRYPTO"_ns); + config->mVideoRobustness.AppendElement(u"SW_SECURE_CRYPTO"_ns); + config->mVideoRobustness.AppendElement(u"SW_SECURE_DECODE"_ns); + config->mEncryptionSchemes.AppendElement(u"cenc"_ns); + config->mEncryptionSchemes.AppendElement(u"cbcs"_ns); + config->mEncryptionSchemes.AppendElement(u"cbcs-1-9"_ns); #if defined(MOZ_WIDGET_ANDROID) - // MediaDrm.isCryptoSchemeSupported only allows passing - // "video/mp4" or "video/webm" for mimetype string. - // See - // https://developer.android.com/reference/android/media/MediaDrm.html#isCryptoSchemeSupported(java.util.UUID, - // java.lang.String) for more detail. - typedef struct { - const nsCString& mMimeType; - const nsCString& mEMECodecType; - const char16_t* mCodecType; - KeySystemConfig::ContainerSupport* mSupportType; - } DataForValidation; + // MediaDrm.isCryptoSchemeSupported only allows passing + // "video/mp4" or "video/webm" for mimetype string. + // See + // https://developer.android.com/reference/android/media/MediaDrm.html#isCryptoSchemeSupported(java.util.UUID, + // java.lang.String) for more detail. + typedef struct { + const nsCString& mMimeType; + const nsCString& mEMECodecType; + const char16_t* mCodecType; + KeySystemConfig::ContainerSupport* mSupportType; + } DataForValidation; - DataForValidation validationList[] = { - {nsCString(VIDEO_MP4), EME_CODEC_H264, java::MediaDrmProxy::AVC, - &config->mMP4}, - {nsCString(VIDEO_MP4), EME_CODEC_VP9, java::MediaDrmProxy::AVC, - &config->mMP4}, + DataForValidation validationList[] = { + {nsCString(VIDEO_MP4), EME_CODEC_H264, java::MediaDrmProxy::AVC, + &config->mMP4}, + {nsCString(VIDEO_MP4), EME_CODEC_VP9, java::MediaDrmProxy::AVC, + &config->mMP4}, # ifdef MOZ_AV1 - {nsCString(VIDEO_MP4), EME_CODEC_AV1, java::MediaDrmProxy::AV1, - &config->mMP4}, + {nsCString(VIDEO_MP4), EME_CODEC_AV1, java::MediaDrmProxy::AV1, + &config->mMP4}, # endif - {nsCString(AUDIO_MP4), EME_CODEC_AAC, java::MediaDrmProxy::AAC, - &config->mMP4}, - {nsCString(AUDIO_MP4), EME_CODEC_FLAC, java::MediaDrmProxy::FLAC, - &config->mMP4}, - {nsCString(AUDIO_MP4), EME_CODEC_OPUS, java::MediaDrmProxy::OPUS, - &config->mMP4}, - {nsCString(VIDEO_WEBM), EME_CODEC_VP8, java::MediaDrmProxy::VP8, - &config->mWebM}, - {nsCString(VIDEO_WEBM), EME_CODEC_VP9, java::MediaDrmProxy::VP9, - &config->mWebM}, + {nsCString(AUDIO_MP4), EME_CODEC_AAC, java::MediaDrmProxy::AAC, + &config->mMP4}, + {nsCString(AUDIO_MP4), EME_CODEC_FLAC, java::MediaDrmProxy::FLAC, + &config->mMP4}, + {nsCString(AUDIO_MP4), EME_CODEC_OPUS, java::MediaDrmProxy::OPUS, + &config->mMP4}, + {nsCString(VIDEO_WEBM), EME_CODEC_VP8, java::MediaDrmProxy::VP8, + &config->mWebM}, + {nsCString(VIDEO_WEBM), EME_CODEC_VP9, java::MediaDrmProxy::VP9, + &config->mWebM}, # ifdef MOZ_AV1 - {nsCString(VIDEO_WEBM), EME_CODEC_AV1, java::MediaDrmProxy::AV1, - &config->mWebM}, + {nsCString(VIDEO_WEBM), EME_CODEC_AV1, java::MediaDrmProxy::AV1, + &config->mWebM}, # endif - {nsCString(AUDIO_WEBM), EME_CODEC_VORBIS, java::MediaDrmProxy::VORBIS, - &config->mWebM}, - {nsCString(AUDIO_WEBM), EME_CODEC_OPUS, java::MediaDrmProxy::OPUS, - &config->mWebM}, - }; + {nsCString(AUDIO_WEBM), EME_CODEC_VORBIS, java::MediaDrmProxy::VORBIS, + &config->mWebM}, + {nsCString(AUDIO_WEBM), EME_CODEC_OPUS, java::MediaDrmProxy::OPUS, + &config->mWebM}, + }; - for (const auto& data : validationList) { - if (java::MediaDrmProxy::IsCryptoSchemeSupported(kWidevineKeySystemName, - data.mMimeType)) { - if (!AndroidDecoderModule::SupportsMimeType(data.mMimeType).isEmpty()) { - data.mSupportType->SetCanDecryptAndDecode(data.mEMECodecType); - } else { - data.mSupportType->SetCanDecrypt(data.mEMECodecType); - } + for (const auto& data : validationList) { + if (java::MediaDrmProxy::IsCryptoSchemeSupported(kWidevineKeySystemName, + data.mMimeType)) { + if (!AndroidDecoderModule::SupportsMimeType(data.mMimeType).isEmpty()) { + data.mSupportType->SetCanDecryptAndDecode(data.mEMECodecType); + } else { + data.mSupportType->SetCanDecrypt(data.mEMECodecType); } } + } #else # if defined(XP_WIN) - // Widevine CDM doesn't include an AAC decoder. So if WMF can't - // decode AAC, and a codec wasn't specified, be conservative - // and reject the MediaKeys request, since we assume Widevine - // will be used with AAC. - if (WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::AAC)) { - config->mMP4.SetCanDecrypt(EME_CODEC_AAC); - } -# else + // Widevine CDM doesn't include an AAC decoder. So if WMF can't + // decode AAC, and a codec wasn't specified, be conservative + // and reject the MediaKeys request, since we assume Widevine + // will be used with AAC. + if (WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::AAC)) { config->mMP4.SetCanDecrypt(EME_CODEC_AAC); + } +# else + config->mMP4.SetCanDecrypt(EME_CODEC_AAC); # endif - config->mMP4.SetCanDecrypt(EME_CODEC_FLAC); - config->mMP4.SetCanDecrypt(EME_CODEC_OPUS); - config->mMP4.SetCanDecryptAndDecode(EME_CODEC_H264); - config->mMP4.SetCanDecryptAndDecode(EME_CODEC_VP9); + config->mMP4.SetCanDecrypt(EME_CODEC_FLAC); + config->mMP4.SetCanDecrypt(EME_CODEC_OPUS); + config->mMP4.SetCanDecryptAndDecode(EME_CODEC_H264); + config->mMP4.SetCanDecryptAndDecode(EME_CODEC_VP9); # ifdef MOZ_AV1 - config->mMP4.SetCanDecryptAndDecode(EME_CODEC_AV1); + config->mMP4.SetCanDecryptAndDecode(EME_CODEC_AV1); # endif - config->mWebM.SetCanDecrypt(EME_CODEC_VORBIS); - config->mWebM.SetCanDecrypt(EME_CODEC_OPUS); - config->mWebM.SetCanDecryptAndDecode(EME_CODEC_VP8); - config->mWebM.SetCanDecryptAndDecode(EME_CODEC_VP9); + config->mWebM.SetCanDecrypt(EME_CODEC_VORBIS); + config->mWebM.SetCanDecrypt(EME_CODEC_OPUS); + config->mWebM.SetCanDecryptAndDecode(EME_CODEC_VP8); + config->mWebM.SetCanDecryptAndDecode(EME_CODEC_VP9); # ifdef MOZ_AV1 - config->mWebM.SetCanDecryptAndDecode(EME_CODEC_AV1); + config->mWebM.SetCanDecryptAndDecode(EME_CODEC_AV1); # endif #endif - return true; - } +} + +/* static */ +RefPtr<KeySystemConfig::SupportedConfigsPromise> +KeySystemConfig::CreateKeySystemConfigs( + const nsTArray<KeySystemConfigRequest>& aRequests) { + // Create available configs for all supported key systems in the request, but + // some of them might not be created immediately. + + nsTArray<KeySystemConfig> outConfigs; + nsTArray<KeySystemConfigRequest> asyncRequests; + + for (const auto& request : aRequests) { + const nsAString& keySystem = request.mKeySystem; + if (!Supports(keySystem)) { + continue; + } + + if (IsClearkeyKeySystem(keySystem)) { + CreateClearKeyKeySystemConfigs(request, outConfigs); + } else if (IsWidevineKeySystem(keySystem)) { + CreateWivineL3KeySystemConfigs(request, outConfigs); + } #ifdef MOZ_WMF_CDM - if (IsPlayReadyKeySystemAndSupported(aKeySystem) || - IsWidevineExperimentKeySystemAndSupported(aKeySystem)) { - RefPtr<WMFCDMImpl> cdm = MakeRefPtr<WMFCDMImpl>(aKeySystem); - return cdm->GetCapabilities(aDecryption == DecryptionInfo::Hardware, - aOutConfigs); - } + else if (IsPlayReadyKeySystemAndSupported(keySystem) || + IsWidevineExperimentKeySystemAndSupported(keySystem)) { + asyncRequests.AppendElement(request); + } #endif - return false; -} + } -bool KeySystemConfig::IsSameKeySystem(const nsAString& aKeySystem) const { #ifdef MOZ_WMF_CDM - // We want to map Widevine experiment key system to normal Widevine key system - // as well. - if (IsWidevineExperimentKeySystemAndSupported(mKeySystem)) { - return mKeySystem.Equals(aKeySystem) || - aKeySystem.EqualsLiteral(kWidevineKeySystemName); + if (!asyncRequests.IsEmpty()) { + RefPtr<SupportedConfigsPromise::Private> promise = + new SupportedConfigsPromise::Private(__func__); + RefPtr<WMFCDMCapabilites> cdm = new WMFCDMCapabilites(); + cdm->GetCapabilities(asyncRequests) + ->Then(GetMainThreadSerialEventTarget(), __func__, + [syncConfigs = std::move(outConfigs), + promise](SupportedConfigsPromise::ResolveOrRejectValue&& + aResult) mutable { + // Return the capabilities we already know + if (aResult.IsReject()) { + promise->Resolve(std::move(syncConfigs), __func__); + return; + } + // Merge sync results with async results + auto& asyncConfigs = aResult.ResolveValue(); + asyncConfigs.AppendElements(std::move(syncConfigs)); + promise->Resolve(std::move(asyncConfigs), __func__); + }); + return promise; } #endif - return mKeySystem.Equals(aKeySystem); + return SupportedConfigsPromise::CreateAndResolve(std::move(outConfigs), + __func__); } /* static */ void KeySystemConfig::GetGMPKeySystemConfigs(dom::Promise* aPromise) { MOZ_ASSERT(aPromise); - nsTArray<KeySystemConfig> keySystemConfigs; + + // Generate config requests const nsTArray<nsString> keySystemNames{ NS_ConvertUTF8toUTF16(kClearKeyKeySystemName), NS_ConvertUTF8toUTF16(kWidevineKeySystemName), }; - FallibleTArray<dom::CDMInformation> cdmInfo; - for (const auto& name : keySystemNames) { + nsTArray<KeySystemConfigRequest> requests; + for (const auto& keySystem : keySystemNames) { #ifdef MOZ_WMF_CDM - if (IsWMFClearKeySystemAndSupported(name)) { + if (IsWMFClearKeySystemAndSupported(keySystem)) { // Using wmf clearkey, not gmp clearkey. continue; } #endif - if (KeySystemConfig::CreateKeySystemConfigs( - name, KeySystemConfig::DecryptionInfo::Software, - keySystemConfigs)) { - auto* info = cdmInfo.AppendElement(fallible); - if (!info) { - aPromise->MaybeReject(NS_ERROR_OUT_OF_MEMORY); - return; - } - MOZ_ASSERT(keySystemConfigs.Length() == cdmInfo.Length()); - info->mKeySystemName = name; - info->mCapabilities = keySystemConfigs.LastElement().GetDebugInfo(); - info->mClearlead = DoesKeySystemSupportClearLead(name); - // TODO : ask real CDM - info->mIsHDCP22Compatible = false; - } + requests.AppendElement( + KeySystemConfigRequest{keySystem, DecryptionInfo::Software}); } - aPromise->MaybeResolve(cdmInfo); + + // Get supported configs + KeySystemConfig::CreateKeySystemConfigs(requests)->Then( + GetMainThreadSerialEventTarget(), __func__, + [promise = RefPtr<dom::Promise>{aPromise}]( + const SupportedConfigsPromise::ResolveOrRejectValue& aResult) { + if (aResult.IsResolve()) { + // Generate CDMInformation from configs + FallibleTArray<dom::CDMInformation> cdmInfo; + for (const auto& config : aResult.ResolveValue()) { + auto* info = cdmInfo.AppendElement(fallible); + if (!info) { + promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY); + return; + } + info->mKeySystemName = config.mKeySystem; + info->mCapabilities = config.GetDebugInfo(); + info->mClearlead = DoesKeySystemSupportClearLead(config.mKeySystem); + // TODO : ask real CDM + info->mIsHDCP22Compatible = false; + } + promise->MaybeResolve(cdmInfo); + } else { + promise->MaybeReject(NS_ERROR_DOM_MEDIA_CDM_ERR); + } + }); } nsString KeySystemConfig::GetDebugInfo() const { diff --git a/dom/media/eme/KeySystemConfig.h b/dom/media/eme/KeySystemConfig.h index cc35ba76de..39027d4401 100644 --- a/dom/media/eme/KeySystemConfig.h +++ b/dom/media/eme/KeySystemConfig.h @@ -9,12 +9,23 @@ #include "nsString.h" #include "nsTArray.h" +#include "mozilla/MozPromise.h" #include "mozilla/dom/MediaKeysBinding.h" +#include "mozilla/dom/MediaKeySystemAccessBinding.h" namespace mozilla { +struct KeySystemConfigRequest; + struct KeySystemConfig { public: + using SupportedConfigsPromise = + MozPromise<nsTArray<KeySystemConfig>, bool /* aIgnored */, + /* IsExclusive = */ true>; + using KeySystemConfigPromise = + MozPromise<dom::MediaKeySystemConfiguration, bool /* aIgnored */, + /* IsExclusive = */ true>; + // EME MediaKeysRequirement: // https://www.w3.org/TR/encrypted-media/#dom-mediakeysrequirement enum class Requirement { @@ -129,9 +140,8 @@ struct KeySystemConfig { Software, Hardware, }; - static bool CreateKeySystemConfigs(const nsAString& aKeySystem, - const DecryptionInfo aDecryption, - nsTArray<KeySystemConfig>& aOutConfigs); + static RefPtr<SupportedConfigsPromise> CreateKeySystemConfigs( + const nsTArray<KeySystemConfigRequest>& aRequests); static void GetGMPKeySystemConfigs(dom::Promise* aPromise); KeySystemConfig() = default; @@ -169,10 +179,6 @@ struct KeySystemConfig { nsString GetDebugInfo() const; - // Return true if the given key system is equal to `mKeySystem`, or it can be - // mapped to the same key system - bool IsSameKeySystem(const nsAString& aKeySystem) const; - nsString mKeySystem; nsTArray<nsString> mInitDataTypes; Requirement mPersistentState = Requirement::NotAllowed; @@ -184,6 +190,22 @@ struct KeySystemConfig { ContainerSupport mMP4; ContainerSupport mWebM; bool mIsHDCP22Compatible = false; + + private: + static void CreateClearKeyKeySystemConfigs( + const KeySystemConfigRequest& aRequest, + nsTArray<KeySystemConfig>& aOutConfigs); + static void CreateWivineL3KeySystemConfigs( + const KeySystemConfigRequest& aRequest, + nsTArray<KeySystemConfig>& aOutConfigs); +}; + +struct KeySystemConfigRequest final { + KeySystemConfigRequest(const nsAString& aKeySystem, + KeySystemConfig::DecryptionInfo aDecryption) + : mKeySystem(aKeySystem), mDecryption(aDecryption) {} + const nsString mKeySystem; + const KeySystemConfig::DecryptionInfo mDecryption; }; KeySystemConfig::SessionType ConvertToKeySystemConfigSessionType( diff --git a/dom/media/eme/MediaKeySession.cpp b/dom/media/eme/MediaKeySession.cpp index 8a3a01dd5c..66ee77a2f5 100644 --- a/dom/media/eme/MediaKeySession.cpp +++ b/dom/media/eme/MediaKeySession.cpp @@ -250,17 +250,33 @@ already_AddRefed<Promise> MediaKeySession::GenerateRequest( // cdm implementation value does not support initDataType as an // Initialization Data Type, return a promise rejected with a // NotSupportedError. String comparison is case-sensitive. - if (!MediaKeySystemAccess::KeySystemSupportsInitDataType( - mKeySystem, aInitDataType, mHardwareDecryption)) { - promise->MaybeRejectWithNotSupportedError( - "Unsupported initDataType passed to MediaKeySession.generateRequest()"); - EME_LOG( - "MediaKeySession[%p,'%s'] GenerateRequest() failed, unsupported " - "initDataType", - this, NS_ConvertUTF16toUTF8(mSessionId).get()); - return promise.forget(); - } + MediaKeySystemAccess::KeySystemSupportsInitDataType(mKeySystem, aInitDataType, + mHardwareDecryption) + ->Then(GetMainThreadSerialEventTarget(), __func__, + [self = RefPtr<MediaKeySession>{this}, this, + initDataType = nsString{aInitDataType}, + initData = std::move(data), promise]( + const GenericPromise::ResolveOrRejectValue& aResult) mutable { + if (aResult.IsReject()) { + promise->MaybeRejectWithNotSupportedError( + "Unsupported initDataType passed to " + "MediaKeySession.generateRequest()"); + EME_LOG( + "MediaKeySession[%p,'%s'] GenerateRequest() failed, " + "unsupported " + "initDataType", + this, NS_ConvertUTF16toUTF8(mSessionId).get()); + return; + } + // Run rest of steps in the spec, starting from 6.6.2.7 + CompleteGenerateRequest(initDataType, initData, promise); + }); + return promise.forget(); +} +void MediaKeySession::CompleteGenerateRequest(const nsString& aInitDataType, + nsTArray<uint8_t>& aData, + DetailedPromise* aPromise) { // Let init data be a copy of the contents of the initData parameter. // Note: Handled by the CopyArrayBufferViewOrArrayBufferData call above. @@ -270,42 +286,41 @@ already_AddRefed<Promise> MediaKeySession::GenerateRequest( // Run the following steps in parallel: - // If the init data is not valid for initDataType, reject promise with - // a newly created TypeError. - if (!ValidateInitData(data, aInitDataType)) { + // If the init data is not valid for initDataType, reject promise with a newly + // created TypeError. + if (!ValidateInitData(aData, aInitDataType)) { // If the preceding step failed, reject promise with a newly created // TypeError. - promise->MaybeRejectWithTypeError( - "initData sanitization failed in MediaKeySession.generateRequest()"); + aPromise->MaybeRejectWithTypeError( + "initData sanitization failed in " + "MediaKeySession.generateRequest()"); EME_LOG( - "MediaKeySession[%p,'%s'] GenerateRequest() initData sanitization " + "MediaKeySession[%p,'%s'] GenerateRequest() initData " + "sanitization " "failed", this, NS_ConvertUTF16toUTF8(mSessionId).get()); - return promise.forget(); + return; } // Let sanitized init data be a validated and sanitized version of init data. // If sanitized init data is empty, reject promise with a NotSupportedError. - // Note: Remaining steps of generateRequest method continue in CDM. + // Note: Remaining steps of generateRequest method continue in CDM. // Convert initData to hex for easier logging. - // Note: CreateSession() std::move()s the data out of the array, so we have - // to copy it here. - nsAutoCString hexInitData(ToHexString(data)); - PromiseId pid = mKeys->StorePromise(promise); + // Note: CreateSession() std::move()s the data out of the array, so we have to + // copy it here. + nsAutoCString hexInitData(ToHexString(aData)); + PromiseId pid = mKeys->StorePromise(aPromise); mKeys->ConnectPendingPromiseIdWithToken(pid, Token()); mKeys->GetCDMProxy()->CreateSession(Token(), mSessionType, pid, aInitDataType, - data); - + aData); EME_LOG( "MediaKeySession[%p,'%s'] GenerateRequest() sent, " "promiseId=%d initData='%s' initDataType='%s'", this, NS_ConvertUTF16toUTF8(mSessionId).get(), pid, hexInitData.get(), NS_ConvertUTF16toUTF8(aInitDataType).get()); - - return promise.forget(); } already_AddRefed<Promise> MediaKeySession::Load(const nsAString& aSessionId, diff --git a/dom/media/eme/MediaKeySession.h b/dom/media/eme/MediaKeySession.h index 7204f99eef..b0edb16cf1 100644 --- a/dom/media/eme/MediaKeySession.h +++ b/dom/media/eme/MediaKeySession.h @@ -120,6 +120,12 @@ class MediaKeySession final : public DOMEventTargetHelper, already_AddRefed<DetailedPromise> MakePromise(ErrorResult& aRv, const nsACString& aName); + // EME spec, starting from 6.6.2.7 + // https://w3c.github.io/encrypted-media/ + void CompleteGenerateRequest(const nsString& aInitDataType, + nsTArray<uint8_t>& aData, + DetailedPromise* aPromise); + RefPtr<DetailedPromise> mClosed; RefPtr<MediaKeyError> mMediaKeyError; diff --git a/dom/media/eme/MediaKeySystemAccess.cpp b/dom/media/eme/MediaKeySystemAccess.cpp index d498c2a773..af9038d309 100644 --- a/dom/media/eme/MediaKeySystemAccess.cpp +++ b/dom/media/eme/MediaKeySystemAccess.cpp @@ -10,7 +10,6 @@ #include "DecoderDoctorDiagnostics.h" #include "DecoderTraits.h" -#include "KeySystemConfig.h" #include "MP4Decoder.h" #include "MediaContainerType.h" #include "WebMDecoder.h" @@ -19,6 +18,7 @@ #include "mozilla/Preferences.h" #include "mozilla/Services.h" #include "mozilla/StaticPrefs_media.h" +#include "mozilla/dom/Document.h" #include "mozilla/dom/KeySystemNames.h" #include "mozilla/dom/MediaKeySession.h" #include "mozilla/dom/MediaKeySystemAccessBinding.h" @@ -231,75 +231,83 @@ static KeySystemConfig::EMECodecString ToEMEAPICodecString( return ""_ns; } -static nsTArray<KeySystemConfig> GetSupportedKeySystems( - const nsAString& aKeySystem, bool aIsHardwareDecryption) { +static RefPtr<KeySystemConfig::SupportedConfigsPromise> +GetSupportedKeySystemConfigs(const nsAString& aKeySystem, + bool aIsHardwareDecryption) { using DecryptionInfo = KeySystemConfig::DecryptionInfo; - nsTArray<KeySystemConfig> keySystemConfigs; + nsTArray<KeySystemConfigRequest> requests; + + // Software Widevine and Clearkey if (IsWidevineKeySystem(aKeySystem) || IsClearkeyKeySystem(aKeySystem)) { - Unused << KeySystemConfig::CreateKeySystemConfigs( - aKeySystem, DecryptionInfo::Software, keySystemConfigs); + requests.AppendElement( + KeySystemConfigRequest{aKeySystem, DecryptionInfo::Software}); } #ifdef MOZ_WMF_CDM - if (IsPlayReadyKeySystem(aKeySystem)) { - Unused << KeySystemConfig::CreateKeySystemConfigs( - NS_ConvertUTF8toUTF16(kPlayReadyKeySystemName), - DecryptionInfo::Software, keySystemConfigs); - if (aIsHardwareDecryption) { - Unused << KeySystemConfig::CreateKeySystemConfigs( - NS_ConvertUTF8toUTF16(kPlayReadyKeySystemName), - DecryptionInfo::Hardware, keySystemConfigs); - Unused << KeySystemConfig::CreateKeySystemConfigs( - NS_ConvertUTF8toUTF16(kPlayReadyKeySystemHardware), - DecryptionInfo::Hardware, keySystemConfigs); - Unused << KeySystemConfig::CreateKeySystemConfigs( + if (IsPlayReadyEnabled()) { + // PlayReady software and hardware + if (aKeySystem.EqualsLiteral(kPlayReadyKeySystemName) || + aKeySystem.EqualsLiteral(kPlayReadyKeySystemHardware)) { + requests.AppendElement( + KeySystemConfigRequest{NS_ConvertUTF8toUTF16(kPlayReadyKeySystemName), + DecryptionInfo::Software}); + if (aIsHardwareDecryption) { + requests.AppendElement(KeySystemConfigRequest{ + NS_ConvertUTF8toUTF16(kPlayReadyKeySystemName), + DecryptionInfo::Hardware}); + requests.AppendElement(KeySystemConfigRequest{ + NS_ConvertUTF8toUTF16(kPlayReadyKeySystemHardware), + DecryptionInfo::Hardware}); + } + } + // PlayReady clearlead + if (aKeySystem.EqualsLiteral(kPlayReadyHardwareClearLeadKeySystemName)) { + requests.AppendElement(KeySystemConfigRequest{ NS_ConvertUTF8toUTF16(kPlayReadyHardwareClearLeadKeySystemName), - DecryptionInfo::Hardware, keySystemConfigs); + DecryptionInfo::Hardware}); } } - // If key system is kWidevineKeySystemName but with hardware decryption - // requirement, then we need to check those experiement key systems which are - // used for hardware decryption. - if (IsWidevineExperimentKeySystem(aKeySystem) || - (IsWidevineKeySystem(aKeySystem) && aIsHardwareDecryption)) { - Unused << KeySystemConfig::CreateKeySystemConfigs( - NS_ConvertUTF8toUTF16(kWidevineExperimentKeySystemName), - DecryptionInfo::Hardware, keySystemConfigs); - Unused << KeySystemConfig::CreateKeySystemConfigs( - NS_ConvertUTF8toUTF16(kWidevineExperiment2KeySystemName), - DecryptionInfo::Hardware, keySystemConfigs); - } -#endif - return keySystemConfigs; -} -static bool GetKeySystemConfigs( - const nsAString& aKeySystem, bool aIsHardwareDecryption, - nsTArray<KeySystemConfig>& aOutKeySystemConfig) { - bool foundConfigs = false; - for (auto& config : - GetSupportedKeySystems(aKeySystem, aIsHardwareDecryption)) { - if (config.IsSameKeySystem(aKeySystem)) { - aOutKeySystemConfig.AppendElement(std::move(config)); - foundConfigs = true; + if (IsWidevineHardwareDecryptionEnabled()) { + // Widevine hardware + if (aKeySystem.EqualsLiteral(kWidevineExperimentKeySystemName) || + (IsWidevineKeySystem(aKeySystem) && aIsHardwareDecryption)) { + requests.AppendElement(KeySystemConfigRequest{ + NS_ConvertUTF8toUTF16(kWidevineExperimentKeySystemName), + DecryptionInfo::Hardware}); + } + // Widevine clearlead + if (aKeySystem.EqualsLiteral(kWidevineExperiment2KeySystemName)) { + requests.AppendElement(KeySystemConfigRequest{ + NS_ConvertUTF8toUTF16(kWidevineExperiment2KeySystemName), + DecryptionInfo::Hardware}); } } - return foundConfigs; +#endif + return KeySystemConfig::CreateKeySystemConfigs(requests); } /* static */ -bool MediaKeySystemAccess::KeySystemSupportsInitDataType( +RefPtr<GenericPromise> MediaKeySystemAccess::KeySystemSupportsInitDataType( const nsAString& aKeySystem, const nsAString& aInitDataType, bool aIsHardwareDecryption) { - nsTArray<KeySystemConfig> implementations; - GetKeySystemConfigs(aKeySystem, aIsHardwareDecryption, implementations); - bool containInitType = false; - for (const auto& config : implementations) { - if (config.mInitDataTypes.Contains(aInitDataType)) { - containInitType = true; - break; - } - } - return containInitType; + RefPtr<GenericPromise::Private> promise = + new GenericPromise::Private(__func__); + GetSupportedKeySystemConfigs(aKeySystem, aIsHardwareDecryption) + ->Then(GetMainThreadSerialEventTarget(), __func__, + [promise, initDataType = nsString{std::move(aInitDataType)}]( + const KeySystemConfig::SupportedConfigsPromise:: + ResolveOrRejectValue& aResult) { + if (aResult.IsResolve()) { + for (const auto& config : aResult.ResolveValue()) { + if (config.mInitDataTypes.Contains(initDataType)) { + promise->Resolve(true, __func__); + return; + } + } + } + promise->Reject(NS_ERROR_DOM_MEDIA_CDM_ERR, __func__); + }); + return promise.forget(); } enum CodecType { Audio, Video, Invalid }; @@ -474,7 +482,7 @@ static Sequence<MediaKeySystemMediaCapability> GetSupportedCapabilities( const nsTArray<MediaKeySystemMediaCapability>& aRequestedCapabilities, const MediaKeySystemConfiguration& aPartialConfig, const KeySystemConfig& aKeySystem, DecoderDoctorDiagnostics* aDiagnostics, - const std::function<void(const char*)>& aDeprecationLogFn) { + const Document* aDocument) { // Let local accumulated configuration be a local copy of partial // configuration. (Note: It's not necessary for us to maintain a local copy, // as we don't need to test whether capabilites from previous calls to this @@ -609,7 +617,7 @@ static Sequence<MediaKeySystemMediaCapability> GetSupportedCapabilities( // If media types is empty: if (codecs.IsEmpty()) { // Log deprecation warning to encourage authors to not do this! - aDeprecationLogFn("MediaEMENoCodecsDeprecatedWarning"); + DeprecationWarningLog(aDocument, "MediaEMENoCodecsDeprecatedWarning"); // TODO: Remove this once we're sure it doesn't break the web. // If container normatively implies a specific set of codecs and codec // constraints: Let parameters be that set. @@ -808,12 +816,12 @@ static Sequence<nsString> UnboxSessionTypes( } // 3.1.1.2 Get Supported Configuration and Consent -static bool GetSupportedConfig( - const KeySystemConfig& aKeySystem, - const MediaKeySystemConfiguration& aCandidate, - MediaKeySystemConfiguration& aOutConfig, - DecoderDoctorDiagnostics* aDiagnostics, bool aInPrivateBrowsing, - const std::function<void(const char*)>& aDeprecationLogFn) { +static bool GetSupportedConfig(const KeySystemConfig& aKeySystem, + const MediaKeySystemConfiguration& aCandidate, + MediaKeySystemConfiguration& aOutConfig, + DecoderDoctorDiagnostics* aDiagnostics, + bool aInPrivateBrowsing, + const Document* aDocument) { EME_LOG("Compare implementation '%s'\n with request '%s'", NS_ConvertUTF16toUTF8(aKeySystem.GetDebugInfo()).get(), ToCString(aCandidate).get()); @@ -941,7 +949,7 @@ static bool GetSupportedConfig( // TODO: Most sites using EME still don't pass capabilities, so we // can't reject on it yet without breaking them. So add this later. // Log deprecation warning to encourage authors to not do this! - aDeprecationLogFn("MediaEMENoCapabilitiesDeprecatedWarning"); + DeprecationWarningLog(aDocument, "MediaEMENoCapabilitiesDeprecatedWarning"); } // If the videoCapabilities member in candidate configuration is non-empty: @@ -952,7 +960,7 @@ static bool GetSupportedConfig( // and restrictions. Sequence<MediaKeySystemMediaCapability> caps = GetSupportedCapabilities(Video, aCandidate.mVideoCapabilities, config, - aKeySystem, aDiagnostics, aDeprecationLogFn); + aKeySystem, aDiagnostics, aDocument); // If video capabilities is null, return NotSupported. if (caps.IsEmpty()) { EME_LOG( @@ -978,7 +986,7 @@ static bool GetSupportedConfig( // restrictions. Sequence<MediaKeySystemMediaCapability> caps = GetSupportedCapabilities(Audio, aCandidate.mAudioCapabilities, config, - aKeySystem, aDiagnostics, aDeprecationLogFn); + aKeySystem, aDiagnostics, aDocument); // If audio capabilities is null, return NotSupported. if (caps.IsEmpty()) { EME_LOG( @@ -1058,30 +1066,42 @@ static bool GetSupportedConfig( } /* static */ -bool MediaKeySystemAccess::GetSupportedConfig( - const nsAString& aKeySystem, - const Sequence<MediaKeySystemConfiguration>& aConfigs, - MediaKeySystemConfiguration& aOutConfig, - DecoderDoctorDiagnostics* aDiagnostics, bool aIsPrivateBrowsing, - const std::function<void(const char*)>& aDeprecationLogFn) { +RefPtr<KeySystemConfig::KeySystemConfigPromise> +MediaKeySystemAccess::GetSupportedConfig(MediaKeySystemAccessRequest* aRequest, + bool aIsPrivateBrowsing, + const Document* aDocument) { nsTArray<KeySystemConfig> implementations; const bool isHardwareDecryptionRequest = - CheckIfHarewareDRMConfigExists(aConfigs) || - DoesKeySystemSupportHardwareDecryption(aKeySystem); - if (!GetKeySystemConfigs(aKeySystem, isHardwareDecryptionRequest, - implementations)) { - return false; - } - for (const auto& implementation : implementations) { - for (const MediaKeySystemConfiguration& candidate : aConfigs) { - if (mozilla::dom::GetSupportedConfig( - implementation, candidate, aOutConfig, aDiagnostics, - aIsPrivateBrowsing, aDeprecationLogFn)) { - return true; - } - } - } - return false; + CheckIfHarewareDRMConfigExists(aRequest->mConfigs) || + DoesKeySystemSupportHardwareDecryption(aRequest->mKeySystem); + + RefPtr<KeySystemConfig::KeySystemConfigPromise::Private> promise = + new KeySystemConfig::KeySystemConfigPromise::Private(__func__); + GetSupportedKeySystemConfigs(aRequest->mKeySystem, + isHardwareDecryptionRequest) + ->Then(GetMainThreadSerialEventTarget(), __func__, + [promise, aRequest, aIsPrivateBrowsing, + document = RefPtr<const Document>{aDocument}]( + const KeySystemConfig::SupportedConfigsPromise:: + ResolveOrRejectValue& aResult) { + if (aResult.IsResolve()) { + MediaKeySystemConfiguration outConfig; + for (const auto& implementation : aResult.ResolveValue()) { + for (const MediaKeySystemConfiguration& candidate : + aRequest->mConfigs) { + if (mozilla::dom::GetSupportedConfig( + implementation, candidate, outConfig, + &aRequest->mDiagnostics, aIsPrivateBrowsing, + document)) { + promise->Resolve(std::move(outConfig), __func__); + return; + } + } + } + } + promise->Reject(false, __func__); + }); + return promise.forget(); } /* static */ diff --git a/dom/media/eme/MediaKeySystemAccess.h b/dom/media/eme/MediaKeySystemAccess.h index 18eec47008..5f3309766d 100644 --- a/dom/media/eme/MediaKeySystemAccess.h +++ b/dom/media/eme/MediaKeySystemAccess.h @@ -14,6 +14,7 @@ #include "mozilla/dom/Promise.h" #include "mozilla/dom/MediaKeySystemAccessBinding.h" #include "mozilla/dom/MediaKeysRequestStatusBinding.h" +#include "mozilla/KeySystemConfig.h" #include "js/TypeDecls.h" @@ -59,16 +60,13 @@ class MediaKeySystemAccess final : public nsISupports, public nsWrapperCache { const nsAString& aKeySystem, MediaKeySystemStatus aStatus); - static bool GetSupportedConfig( - const nsAString& aKeySystem, - const Sequence<MediaKeySystemConfiguration>& aConfigs, - MediaKeySystemConfiguration& aOutConfig, - DecoderDoctorDiagnostics* aDiagnostics, bool aIsPrivateBrowsing, - const std::function<void(const char*)>& aDeprecationLogFn); + static RefPtr<KeySystemConfig::KeySystemConfigPromise> GetSupportedConfig( + MediaKeySystemAccessRequest* aRequest, bool aIsPrivateBrowsing, + const Document* aDocument); - static bool KeySystemSupportsInitDataType(const nsAString& aKeySystem, - const nsAString& aInitDataType, - bool aIsHardwareDecryption); + static RefPtr<GenericPromise> KeySystemSupportsInitDataType( + const nsAString& aKeySystem, const nsAString& aInitDataType, + bool aIsHardwareDecryption); static nsCString ToCString( const Sequence<MediaKeySystemConfiguration>& aConfig); diff --git a/dom/media/eme/MediaKeySystemAccessManager.cpp b/dom/media/eme/MediaKeySystemAccessManager.cpp index 8ebe7ceee7..84389d1db0 100644 --- a/dom/media/eme/MediaKeySystemAccessManager.cpp +++ b/dom/media/eme/MediaKeySystemAccessManager.cpp @@ -368,8 +368,6 @@ void MediaKeySystemAccessManager::RequestMediaKeySystemAccess( // 5. Let promise be a new promise. // 6. Run the following steps in parallel: - DecoderDoctorDiagnostics diagnostics; - // 1. If keySystem is not one of the Key Systems supported by the user // agent, reject promise with a NotSupportedError. String comparison is // case-sensitive. @@ -383,7 +381,7 @@ void MediaKeySystemAccessManager::RequestMediaKeySystemAccess( // supported. aRequest->RejectPromiseWithNotSupportedError( "Key system is unsupported"_ns); - diagnostics.StoreMediaKeySystemAccess( + aRequest->mDiagnostics.StoreMediaKeySystemAccess( mWindow->GetExtantDoc(), aRequest->mKeySystem, false, __func__); return; } @@ -399,7 +397,7 @@ void MediaKeySystemAccessManager::RequestMediaKeySystemAccess( MediaKeySystemStatus::Api_disabled); } aRequest->RejectPromiseWithNotSupportedError("EME has been preffed off"_ns); - diagnostics.StoreMediaKeySystemAccess( + aRequest->mDiagnostics.StoreMediaKeySystemAccess( mWindow->GetExtantDoc(), aRequest->mKeySystem, false, __func__); return; } @@ -439,7 +437,7 @@ void MediaKeySystemAccessManager::RequestMediaKeySystemAccess( // "I can't play, updating" notification. aRequest->RejectPromiseWithNotSupportedError( "Timed out while waiting for a CDM update"_ns); - diagnostics.StoreMediaKeySystemAccess( + aRequest->mDiagnostics.StoreMediaKeySystemAccess( mWindow->GetExtantDoc(), aRequest->mKeySystem, false, __func__); return; } @@ -453,6 +451,7 @@ void MediaKeySystemAccessManager::RequestMediaKeySystemAccess( keySystem = NS_ConvertUTF8toUTF16(kWidevineExperimentKeySystemName); } #endif + auto& diagnostics = aRequest->mDiagnostics; if (AwaitInstall(std::move(aRequest))) { // Notify chrome that we're going to wait for the CDM to download/update. EME_LOG("Await %s for installation", @@ -480,25 +479,6 @@ void MediaKeySystemAccessManager::RequestMediaKeySystemAccess( return; } - nsCOMPtr<Document> doc = mWindow->GetExtantDoc(); - nsTHashMap<nsCharPtrHashKey, bool> warnings; - std::function<void(const char*)> deprecationWarningLogFn = - [&](const char* aMsgName) { - EME_LOG( - "MediaKeySystemAccessManager::DeprecationWarningLambda Logging " - "deprecation warning '%s' to WebConsole.", - aMsgName); - warnings.InsertOrUpdate(aMsgName, true); - AutoTArray<nsString, 1> params; - nsString& uri = *params.AppendElement(); - if (doc) { - Unused << doc->GetDocumentURI(uri); - } - nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Media"_ns, - doc, nsContentUtils::eDOM_PROPERTIES, - aMsgName, params); - }; - bool isPrivateBrowsing = mWindow->GetExtantDoc() && mWindow->GetExtantDoc()->NodePrincipal()->GetPrivateBrowsingId() > 0; @@ -517,23 +497,28 @@ void MediaKeySystemAccessManager::RequestMediaKeySystemAccess( // 3. Let the cdm implementation value be implementation. // 2. Resolve promise with access and abort the parallel steps of this // algorithm. - MediaKeySystemConfiguration config; - if (MediaKeySystemAccess::GetSupportedConfig( - aRequest->mKeySystem, aRequest->mConfigs, config, &diagnostics, - isPrivateBrowsing, deprecationWarningLogFn)) { - aRequest->mSupportedConfig = Some(config); - // The app gets the final say on if we provide access or not. - CheckDoesAppAllowProtectedMedia(std::move(aRequest)); - return; - } - // 4. Reject promise with a NotSupportedError. - - // Not to inform user, because nothing to do if the corresponding keySystem - // configuration is not supported. - aRequest->RejectPromiseWithNotSupportedError( - "Key system configuration is not supported"_ns); - diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), - aRequest->mKeySystem, false, __func__); + MediaKeySystemAccess::GetSupportedConfig(aRequest.get(), isPrivateBrowsing, + mWindow->GetExtantDoc()) + ->Then(GetMainThreadSerialEventTarget(), __func__, + [self = RefPtr<MediaKeySystemAccessManager>{this}, this, + request = UniquePtr<PendingRequest>{std::move(aRequest)}]( + const KeySystemConfig::KeySystemConfigPromise:: + ResolveOrRejectValue& aResult) mutable { + if (aResult.IsResolve()) { + request->mSupportedConfig = Some(aResult.ResolveValue()); + // The app gets the final say on if we provide access or not. + CheckDoesAppAllowProtectedMedia(std::move(request)); + } else { + // 4. Reject promise with a NotSupportedError. + // Not to inform user, because nothing to do if the + // corresponding keySystem configuration is not supported. + request->RejectPromiseWithNotSupportedError( + "Key system configuration is not supported"_ns); + request->mDiagnostics.StoreMediaKeySystemAccess( + mWindow->GetExtantDoc(), request->mKeySystem, false, + __func__); + } + }); } void MediaKeySystemAccessManager::ProvideAccess( diff --git a/dom/media/eme/MediaKeySystemAccessManager.h b/dom/media/eme/MediaKeySystemAccessManager.h index 77feded701..9ea621df84 100644 --- a/dom/media/eme/MediaKeySystemAccessManager.h +++ b/dom/media/eme/MediaKeySystemAccessManager.h @@ -5,6 +5,7 @@ #ifndef DOM_MEDIA_MEDIAKEYSYSTEMACCESSMANAGER_H_ #define DOM_MEDIA_MEDIAKEYSYSTEMACCESSMANAGER_H_ +#include "DecoderDoctorDiagnostics.h" #include "mozilla/dom/MediaKeySystemAccess.h" #include "mozilla/MozPromise.h" #include "nsCycleCollectionParticipant.h" @@ -95,6 +96,7 @@ struct MediaKeySystemAccessRequest { const nsString mKeySystem; // The config(s) passed for this request. const Sequence<MediaKeySystemConfiguration> mConfigs; + DecoderDoctorDiagnostics mDiagnostics; }; class MediaKeySystemAccessManager final : public nsIObserver, public nsINamed { diff --git a/dom/media/eme/mediafoundation/WMFCDMImpl.cpp b/dom/media/eme/mediafoundation/WMFCDMImpl.cpp index add978f755..983a7c00f2 100644 --- a/dom/media/eme/mediafoundation/WMFCDMImpl.cpp +++ b/dom/media/eme/mediafoundation/WMFCDMImpl.cpp @@ -10,81 +10,12 @@ #include "mozilla/AppShutdown.h" #include "mozilla/ClearOnShutdown.h" +#include "mozilla/ScopeExit.h" #include "mozilla/dom/MediaKeySession.h" #include "mozilla/dom/KeySystemNames.h" namespace mozilla { -bool WMFCDMImpl::GetCapabilities(bool aIsHardwareDecryption, - nsTArray<KeySystemConfig>& aOutConfigs) { - MOZ_ASSERT(NS_IsMainThread()); - if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { - return false; - } - - static std::unordered_map<std::string, nsTArray<KeySystemConfig>> - sKeySystemConfigs{}; - static bool sSetRunOnShutdown = false; - if (!sSetRunOnShutdown) { - GetMainThreadSerialEventTarget()->Dispatch( - NS_NewRunnableFunction("WMFCDMImpl::GetCapabilities", [&] { - RunOnShutdown([&] { sKeySystemConfigs.clear(); }, - ShutdownPhase::XPCOMShutdown); - })); - sSetRunOnShutdown = true; - } - - // Retrieve result from our cached key system - auto keySystem = std::string{NS_ConvertUTF16toUTF8(mKeySystem).get()}; - if (auto rv = sKeySystemConfigs.find(keySystem); - rv != sKeySystemConfigs.end()) { - for (const auto& config : rv->second) { - if (IsHardwareDecryptionSupported(config) == aIsHardwareDecryption) { - EME_LOG("Return cached capabilities for %s (%s)", keySystem.c_str(), - NS_ConvertUTF16toUTF8(config.GetDebugInfo()).get()); - aOutConfigs.AppendElement(config); - return true; - } - } - } - - // Not cached result, ask the remote process. - nsCOMPtr<nsISerialEventTarget> backgroundTaskQueue; - NS_CreateBackgroundTaskQueue(__func__, getter_AddRefs(backgroundTaskQueue)); - if (!mCDM) { - mCDM = MakeRefPtr<MFCDMChild>(mKeySystem); - } - bool ok = false; - media::Await( - do_AddRef(backgroundTaskQueue), - mCDM->GetCapabilities(aIsHardwareDecryption), - [&ok, &aOutConfigs, keySystem, - aIsHardwareDecryption](const MFCDMCapabilitiesIPDL& capabilities) { - EME_LOG("capabilities: keySystem=%s (hw-secure=%d)", keySystem.c_str(), - aIsHardwareDecryption); - for (const auto& v : capabilities.videoCapabilities()) { - EME_LOG("capabilities: video=%s", - NS_ConvertUTF16toUTF8(v.contentType()).get()); - } - for (const auto& a : capabilities.audioCapabilities()) { - EME_LOG("capabilities: audio=%s", - NS_ConvertUTF16toUTF8(a.contentType()).get()); - } - for (const auto& v : capabilities.encryptionSchemes()) { - EME_LOG("capabilities: encryptionScheme=%s", EncryptionSchemeStr(v)); - } - KeySystemConfig* config = aOutConfigs.AppendElement(); - MFCDMCapabilitiesIPDLToKeySystemConfig(capabilities, *config); - sKeySystemConfigs[keySystem].AppendElement(*config); - ok = true; - }, - [](nsresult rv) { - EME_LOG("Fail to get key system capabilities. rv=%x", uint32_t(rv)); - }); - - return ok; -} - RefPtr<WMFCDMImpl::InitPromise> WMFCDMImpl::Init( const WMFCDMImpl::InitParams& aParams) { if (!mCDM) { @@ -111,4 +42,88 @@ RefPtr<WMFCDMImpl::InitPromise> WMFCDMImpl::Init( return mInitPromiseHolder.Ensure(__func__); } +RefPtr<KeySystemConfig::SupportedConfigsPromise> +WMFCDMCapabilites::GetCapabilities( + const nsTArray<KeySystemConfigRequest>& aRequests) { + MOZ_ASSERT(NS_IsMainThread()); + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return SupportedConfigsPromise::CreateAndReject(false, __func__); + } + + if (!mCapabilitiesPromiseHolder.IsEmpty()) { + return mCapabilitiesPromiseHolder.Ensure(__func__); + } + + using CapabilitiesPromise = MFCDMChild::CapabilitiesPromise; + nsTArray<RefPtr<CapabilitiesPromise>> promises; + for (const auto& request : aRequests) { + RefPtr<MFCDMChild> cdm = new MFCDMChild(request.mKeySystem); + promises.AppendElement(cdm->GetCapabilities(MFCDMCapabilitiesRequest{ + nsString{request.mKeySystem}, + request.mDecryption == KeySystemConfig::DecryptionInfo::Hardware})); + mCDMs.AppendElement(std::move(cdm)); + } + + CapabilitiesPromise::AllSettled(GetCurrentSerialEventTarget(), promises) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [self = RefPtr<WMFCDMCapabilites>(this), this]( + CapabilitiesPromise::AllSettledPromiseType::ResolveOrRejectValue&& + aResult) { + mCapabilitiesPromisesRequest.Complete(); + + // Reset cdm + auto exit = MakeScopeExit([&] { + for (auto& cdm : mCDMs) { + cdm->Shutdown(); + } + mCDMs.Clear(); + }); + + nsTArray<KeySystemConfig> outConfigs; + for (const auto& promiseRv : aResult.ResolveValue()) { + if (promiseRv.IsReject()) { + continue; + } + const MFCDMCapabilitiesIPDL& capabilities = + promiseRv.ResolveValue(); + EME_LOG("capabilities: keySystem=%s (hw-secure=%d)", + NS_ConvertUTF16toUTF8(capabilities.keySystem()).get(), + capabilities.isHardwareDecryption()); + for (const auto& v : capabilities.videoCapabilities()) { + EME_LOG("capabilities: video=%s", + NS_ConvertUTF16toUTF8(v.contentType()).get()); + } + for (const auto& a : capabilities.audioCapabilities()) { + EME_LOG("capabilities: audio=%s", + NS_ConvertUTF16toUTF8(a.contentType()).get()); + } + for (const auto& v : capabilities.encryptionSchemes()) { + EME_LOG("capabilities: encryptionScheme=%s", + EncryptionSchemeStr(v)); + } + KeySystemConfig* config = outConfigs.AppendElement(); + MFCDMCapabilitiesIPDLToKeySystemConfig(capabilities, *config); + } + if (outConfigs.IsEmpty()) { + EME_LOG( + "Failed on getting capabilities from all settled promise"); + mCapabilitiesPromiseHolder.Reject(false, __func__); + return; + } + mCapabilitiesPromiseHolder.Resolve(std::move(outConfigs), __func__); + }) + ->Track(mCapabilitiesPromisesRequest); + + return mCapabilitiesPromiseHolder.Ensure(__func__); +} + +WMFCDMCapabilites::~WMFCDMCapabilites() { + mCapabilitiesPromisesRequest.DisconnectIfExists(); + mCapabilitiesPromiseHolder.RejectIfExists(false, __func__); + for (auto& cdm : mCDMs) { + cdm->Shutdown(); + } +} + } // namespace mozilla diff --git a/dom/media/eme/mediafoundation/WMFCDMImpl.h b/dom/media/eme/mediafoundation/WMFCDMImpl.h index b7e6308848..c5bf8234af 100644 --- a/dom/media/eme/mediafoundation/WMFCDMImpl.h +++ b/dom/media/eme/mediafoundation/WMFCDMImpl.h @@ -34,10 +34,6 @@ class WMFCDMImpl final { explicit WMFCDMImpl(const nsAString& aKeySystem) : mKeySystem(aKeySystem) {} - // TODO: make this async? - bool GetCapabilities(bool aIsHardwareDecryption, - nsTArray<KeySystemConfig>& aOutConfigs); - using InitPromise = GenericPromise; struct InitParams { nsString mOrigin; @@ -119,6 +115,26 @@ class WMFCDMImpl final { MozPromiseHolder<InitPromise> mInitPromiseHolder; }; +// A helper class to get multiple capabilities from different key systems. +class WMFCDMCapabilites final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WMFCDMCapabilites); + WMFCDMCapabilites() = default; + + using SupportedConfigsPromise = KeySystemConfig::SupportedConfigsPromise; + RefPtr<SupportedConfigsPromise> GetCapabilities( + const nsTArray<KeySystemConfigRequest>& aRequests); + + private: + ~WMFCDMCapabilites(); + + nsTArray<RefPtr<MFCDMChild>> mCDMs; + MozPromiseHolder<SupportedConfigsPromise> mCapabilitiesPromiseHolder; + MozPromiseRequestHolder< + MFCDMChild::CapabilitiesPromise::AllSettledPromiseType> + mCapabilitiesPromisesRequest; +}; + } // namespace mozilla #endif // DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMIMPL_H_ diff --git a/dom/media/eme/mediafoundation/WMFCDMProxy.cpp b/dom/media/eme/mediafoundation/WMFCDMProxy.cpp index f7e05dfb6a..5fd73c2dcf 100644 --- a/dom/media/eme/mediafoundation/WMFCDMProxy.cpp +++ b/dom/media/eme/mediafoundation/WMFCDMProxy.cpp @@ -158,7 +158,7 @@ void WMFCDMProxy::ResolvePromiseWithKeyStatus( RETURN_IF_SHUTDOWN(); EME_LOG("WMFCDMProxy::ResolvePromiseWithKeyStatus(this=%p, pid=%" PRIu32 ", status=%s)", - this, aId, ToMediaKeyStatusStr(aStatus)); + this, aId, dom::GetEnumString(aStatus).get()); if (!mKeys.IsNull()) { mKeys->ResolvePromiseWithKeyStatus(aId, aStatus); } else { diff --git a/dom/media/eme/metrics.yaml b/dom/media/eme/metrics.yaml new file mode 100644 index 0000000000..9a8cb0783a --- /dev/null +++ b/dom/media/eme/metrics.yaml @@ -0,0 +1,42 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Core :: Audio/Video' + +mediadrm: + eme_playback: + type: event + description: > + Record the EME play time with the video codec and resolutions. + metadata: + tags: + - 'Core :: Audio/Video: Playback' + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1882567 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1882567#3 + data_sensitivity: + - technical + notification_emails: + - media-alerts@mozilla.com + extra_keys: + key_system: + description: The key system used for the EME playback + type: string + played_time: + description: How many second the EME content has been played since last record + type: quantity + resolution: + description: The video resolution used for EME playback + type: string + video_codec: + description: The video codec used for EME playback + type: string + expires: never diff --git a/dom/media/gmp/ChromiumCDMChild.cpp b/dom/media/gmp/ChromiumCDMChild.cpp index 4592f2e291..343622d37f 100644 --- a/dom/media/gmp/ChromiumCDMChild.cpp +++ b/dom/media/gmp/ChromiumCDMChild.cpp @@ -450,7 +450,7 @@ mozilla::ipc::IPCResult ChromiumCDMChild::RecvCreateSessionAndGenerateRequest( "pid=%" PRIu32 ", sessionType=%" PRIu32 ", initDataType=%" PRIu32 ") initDataLen=%zu", aPromiseId, aSessionType, aInitDataType, aInitData.Length()); - MOZ_ASSERT(aSessionType <= cdm::SessionType::kPersistentUsageRecord); + MOZ_ASSERT(aSessionType <= cdm::SessionType::kPersistentLicense); MOZ_ASSERT(aInitDataType <= cdm::InitDataType::kWebM); if (mCDM) { mCDM->CreateSessionAndGenerateRequest( diff --git a/dom/media/gmp/ChromiumCDMProxy.cpp b/dom/media/gmp/ChromiumCDMProxy.cpp index 566b386b0b..43ce3e50af 100644 --- a/dom/media/gmp/ChromiumCDMProxy.cpp +++ b/dom/media/gmp/ChromiumCDMProxy.cpp @@ -7,6 +7,7 @@ #include "ChromiumCDMProxy.h" #include "ChromiumCDMCallbackProxy.h" #include "MediaResult.h" +#include "mozilla/StaticPrefs_media.h" #include "mozilla/dom/MediaKeySession.h" #include "mozilla/dom/MediaKeysBinding.h" #include "GMPUtils.h" @@ -382,13 +383,19 @@ void ChromiumCDMProxy::NotifyOutputProtectionStatus( } uint32_t linkMask{}; - uint32_t protectionMask{}; // Unused/always zeroed. + uint32_t protectionMask{}; if (aCheckStatus == OutputProtectionCheckStatus::CheckSuccessful && aCaptureStatus == OutputProtectionCaptureStatus::CapturePossilbe) { // The result indicates the capture is possible, so set the mask // to indicate this. linkMask |= cdm::OutputLinkTypes::kLinkTypeNetwork; } + // `kProtectionNone` can cause playback to stop if HDCP_V1 is required. Report + // HDCP protection if there's no potential capturing. + if (linkMask == cdm::OutputLinkTypes::kLinkTypeNone && + StaticPrefs::media_widevine_hdcp_protection_mask()) { + protectionMask = cdm::OutputProtectionMethods::kProtectionHDCP; + } mGMPThread->Dispatch(NewRunnableMethod<bool, uint32_t, uint32_t>( "gmp::ChromiumCDMParent::NotifyOutputProtectionStatus", cdm, &gmp::ChromiumCDMParent::NotifyOutputProtectionStatus, diff --git a/dom/media/gmp/GMPChild.cpp b/dom/media/gmp/GMPChild.cpp index d543d46387..ca8b6a4ed4 100644 --- a/dom/media/gmp/GMPChild.cpp +++ b/dom/media/gmp/GMPChild.cpp @@ -41,6 +41,7 @@ #include "nsXULAppAPI.h" #include "nsIXULRuntime.h" #include "nsXPCOM.h" +#include "nsXPCOMPrivate.h" // for XUL_DLL #include "prio.h" #ifdef XP_WIN # include <stdlib.h> // for _exit() @@ -348,15 +349,11 @@ static bool IsFileLeafEqualToASCII(const nsCOMPtr<nsIFile>& aFile, #endif #if defined(XP_WIN) -# define FIREFOX_FILE u"firefox.exe"_ns -# define XUL_LIB_FILE u"xul.dll"_ns -#elif defined(XP_MACOSX) -# define FIREFOX_FILE u"firefox"_ns -# define XUL_LIB_FILE u"XUL"_ns +# define FIREFOX_FILE MOZ_APP_NAME u".exe"_ns #else -# define FIREFOX_FILE u"firefox"_ns -# define XUL_LIB_FILE u"libxul.so"_ns +# define FIREFOX_FILE MOZ_APP_NAME u""_ns #endif +#define XUL_LIB_FILE XUL_DLL u""_ns static nsCOMPtr<nsIFile> GetFirefoxAppPath( nsCOMPtr<nsIFile> aPluginContainerPath) { diff --git a/dom/media/gmp/moz.build b/dom/media/gmp/moz.build index 3c34021506..744209fa1c 100644 --- a/dom/media/gmp/moz.build +++ b/dom/media/gmp/moz.build @@ -129,6 +129,8 @@ PREPROCESSED_IPDL_SOURCES += [ if CONFIG["TARGET_OS"] in ["WINNT", "OSX"]: DEFINES["SUPPORT_STORAGE_ID"] = 1 +DEFINES["MOZ_APP_NAME"] = '"%s"' % CONFIG["MOZ_APP_NAME"] + include("/ipc/chromium/chromium-config.mozbuild") if CONFIG["MOZ_SANDBOX"]: diff --git a/dom/media/gmp/mozIGeckoMediaPluginChromeService.idl b/dom/media/gmp/mozIGeckoMediaPluginChromeService.idl index 51dc545092..bc782978c9 100644 --- a/dom/media/gmp/mozIGeckoMediaPluginChromeService.idl +++ b/dom/media/gmp/mozIGeckoMediaPluginChromeService.idl @@ -27,7 +27,7 @@ interface mozIGeckoMediaPluginChromeService : nsISupports * @note Main-thread API. */ void removeAndDeletePluginDirectory(in AString directory, - [optional] in bool defer); + [optional] in boolean defer); /** * Clears storage data associated with the site and the originAttributes @@ -48,7 +48,7 @@ interface mozIGeckoMediaPluginChromeService : nsISupports * persistently on disk. Private Browsing and local content are not * allowed to store persistent data. */ - bool isPersistentStorageAllowed(in ACString nodeId); + boolean isPersistentStorageAllowed(in ACString nodeId); /** * Returns the directory to use as the base for storing data about GMPs. diff --git a/dom/media/gmp/widevine-adapter/content_decryption_module.h b/dom/media/gmp/widevine-adapter/content_decryption_module.h index 68fee35195..f9101fbaf0 100644 --- a/dom/media/gmp/widevine-adapter/content_decryption_module.h +++ b/dom/media/gmp/widevine-adapter/content_decryption_module.h @@ -411,7 +411,6 @@ CHECK_TYPE(InitDataType, 4, 4); enum SessionType : uint32_t { kTemporary = 0, kPersistentLicense = 1, - kPersistentUsageRecord = 2 }; CHECK_TYPE(SessionType, 4, 4); diff --git a/dom/media/gtest/TestAudioInputProcessing.cpp b/dom/media/gtest/TestAudioInputProcessing.cpp index 82c1831e84..d21c37a900 100644 --- a/dom/media/gtest/TestAudioInputProcessing.cpp +++ b/dom/media/gtest/TestAudioInputProcessing.cpp @@ -30,11 +30,21 @@ class MockGraph : public MediaTrackGraphImpl { void Init(uint32_t aChannels) { MediaTrackGraphImpl::Init(OFFLINE_THREAD_DRIVER, DIRECT_DRIVER, aChannels); - // Remove this graph's driver since it holds a ref. If no AppendMessage - // takes place, the driver never starts. This will also make sure no-one - // tries to use it. We are still kept alive by the self-ref. Destroy() must - // be called to break that cycle. - SetCurrentDriver(nullptr); + + MonitorAutoLock lock(mMonitor); + // We don't need a graph driver. Advance to + // LIFECYCLE_WAITING_FOR_TRACK_DESTRUCTION so that the driver never + // starts. Graph control messages run as in shutdown, synchronously. + // This permits the main thread part of track initialization through + // AudioProcessingTrack::Create(). + mLifecycleState = LIFECYCLE_WAITING_FOR_TRACK_DESTRUCTION; +#ifdef DEBUG + mCanRunMessagesSynchronously = true; +#endif + // Remove this graph's driver since it holds a ref. We are still kept + // alive by the self-ref. Destroy() must be called to break that cycle if + // no tracks are created and destroyed. + mDriver = nullptr; } MOCK_CONST_METHOD0(OnGraphThread, bool()); @@ -53,6 +63,7 @@ TEST(TestAudioInputProcessing, Buffering) const uint32_t channels = 1; auto graph = MakeRefPtr<NiceMock<MockGraph>>(rate); graph->Init(channels); + RefPtr track = AudioProcessingTrack::Create(graph); auto aip = MakeRefPtr<AudioInputProcessing>(channels); @@ -62,19 +73,26 @@ TEST(TestAudioInputProcessing, Buffering) GraphTime processedTime; GraphTime nextTime; AudioSegment output; + MediaEnginePrefs settings; + settings.mChannels = channels; + // pref "media.getusermedia.agc2_forced" defaults to true. + // mAgc would need to be set to something other than kAdaptiveAnalog + // for mobile, as asserted in AudioInputProcessing::ConfigForPrefs, + // if gain_controller1 were used. + settings.mAgc2Forced = true; // Toggle pass-through mode without starting { - EXPECT_EQ(aip->PassThrough(graph), false); - EXPECT_EQ(aip->NumBufferedFrames(graph), 0); - - aip->SetPassThrough(graph, true); + EXPECT_EQ(aip->IsPassThrough(graph), true); EXPECT_EQ(aip->NumBufferedFrames(graph), 0); - aip->SetPassThrough(graph, false); + settings.mAgcOn = true; + aip->ApplySettings(graph, nullptr, settings); + EXPECT_EQ(aip->IsPassThrough(graph), false); EXPECT_EQ(aip->NumBufferedFrames(graph), 0); - aip->SetPassThrough(graph, true); + settings.mAgcOn = false; + aip->ApplySettings(graph, nullptr, settings); EXPECT_EQ(aip->NumBufferedFrames(graph), 0); } @@ -88,14 +106,15 @@ TEST(TestAudioInputProcessing, Buffering) AudioSegment input; generator.Generate(input, nextTime - processedTime); - aip->Process(graph, processedTime, nextTime, &input, &output); + aip->Process(track, processedTime, nextTime, &input, &output); EXPECT_EQ(input.GetDuration(), nextTime - processedTime); EXPECT_EQ(output.GetDuration(), nextTime); EXPECT_EQ(aip->NumBufferedFrames(graph), 0); } // Set aip to processing/non-pass-through mode - aip->SetPassThrough(graph, false); + settings.mAgcOn = true; + aip->ApplySettings(graph, nullptr, settings); { // Need (nextTime - processedTime) = 256 - 128 = 128 frames this round. // aip has not started yet, so output will be filled with silence data @@ -106,37 +125,35 @@ TEST(TestAudioInputProcessing, Buffering) AudioSegment input; generator.Generate(input, nextTime - processedTime); - aip->Process(graph, processedTime, nextTime, &input, &output); + aip->Process(track, processedTime, nextTime, &input, &output); EXPECT_EQ(input.GetDuration(), nextTime - processedTime); EXPECT_EQ(output.GetDuration(), nextTime); EXPECT_EQ(aip->NumBufferedFrames(graph), 0); } - // aip has been started and set to processing mode so it will insert 80 frames - // into aip's internal buffer as pre-buffering. + // aip has been set to processing mode and is started. aip->Start(graph); { // Need (nextTime - processedTime) = 256 - 256 = 0 frames this round. - // The Process() aip will take 0 frames from input, packetize and process - // these frames into 0 80-frame packet(0 frames left in packetizer), insert - // packets into aip's internal buffer, then move 0 frames the internal - // buffer to output, leaving 80 + 0 - 0 = 80 frames in aip's internal - // buffer. + // Process() will return early on 0 frames of input. + // Pre-buffering is not triggered. processedTime = nextTime; nextTime = MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(3 * frames); AudioSegment input; generator.Generate(input, nextTime - processedTime); - aip->Process(graph, processedTime, nextTime, &input, &output); + aip->Process(track, processedTime, nextTime, &input, &output); EXPECT_EQ(input.GetDuration(), nextTime - processedTime); EXPECT_EQ(output.GetDuration(), nextTime); - EXPECT_EQ(aip->NumBufferedFrames(graph), 80); + EXPECT_EQ(aip->NumBufferedFrames(graph), 0); } { // Need (nextTime - processedTime) = 384 - 256 = 128 frames this round. - // The Process() aip will take 128 frames from input, packetize and process + // On receipt of the these first frames, aip will insert 80 frames + // into its internal buffer as pre-buffering. + // Process() will take 128 frames from input, packetize and process // these frames into floor(128/80) = 1 80-frame packet (48 frames left in // packetizer), insert packets into aip's internal buffer, then move 128 // frames the internal buffer to output, leaving 80 + 80 - 128 = 32 frames @@ -147,7 +164,7 @@ TEST(TestAudioInputProcessing, Buffering) AudioSegment input; generator.Generate(input, nextTime - processedTime); - aip->Process(graph, processedTime, nextTime, &input, &output); + aip->Process(track, processedTime, nextTime, &input, &output); EXPECT_EQ(input.GetDuration(), nextTime - processedTime); EXPECT_EQ(output.GetDuration(), nextTime); EXPECT_EQ(aip->NumBufferedFrames(graph), 32); @@ -161,7 +178,7 @@ TEST(TestAudioInputProcessing, Buffering) AudioSegment input; generator.Generate(input, nextTime - processedTime); - aip->Process(graph, processedTime, nextTime, &input, &output); + aip->Process(track, processedTime, nextTime, &input, &output); EXPECT_EQ(input.GetDuration(), nextTime - processedTime); EXPECT_EQ(output.GetDuration(), nextTime); EXPECT_EQ(aip->NumBufferedFrames(graph), 32); @@ -180,13 +197,15 @@ TEST(TestAudioInputProcessing, Buffering) AudioSegment input; generator.Generate(input, nextTime - processedTime); - aip->Process(graph, processedTime, nextTime, &input, &output); + aip->Process(track, processedTime, nextTime, &input, &output); EXPECT_EQ(input.GetDuration(), nextTime - processedTime); EXPECT_EQ(output.GetDuration(), nextTime); EXPECT_EQ(aip->NumBufferedFrames(graph), 64); } - aip->SetPassThrough(graph, true); + // Set aip to pass-through mode + settings.mAgcOn = false; + aip->ApplySettings(graph, nullptr, settings); { // Need (nextTime - processedTime) = 512 - 512 = 0 frames this round. // No buffering in pass-through mode @@ -196,14 +215,14 @@ TEST(TestAudioInputProcessing, Buffering) AudioSegment input; generator.Generate(input, nextTime - processedTime); - aip->Process(graph, processedTime, nextTime, &input, &output); + aip->Process(track, processedTime, nextTime, &input, &output); EXPECT_EQ(input.GetDuration(), nextTime - processedTime); EXPECT_EQ(output.GetDuration(), processedTime); EXPECT_EQ(aip->NumBufferedFrames(graph), 0); } aip->Stop(graph); - graph->Destroy(); + track->Destroy(); } TEST(TestAudioInputProcessing, ProcessDataWithDifferentPrincipals) @@ -212,6 +231,7 @@ TEST(TestAudioInputProcessing, ProcessDataWithDifferentPrincipals) const uint32_t channels = 2; auto graph = MakeRefPtr<NiceMock<MockGraph>>(rate); graph->Init(channels); + RefPtr track = AudioProcessingTrack::Create(graph); auto aip = MakeRefPtr<AudioInputProcessing>(channels); AudioGenerator<AudioDataValue> generator(channels, rate); @@ -271,26 +291,30 @@ TEST(TestAudioInputProcessing, ProcessDataWithDifferentPrincipals) }; // Check the principals in audio-processing mode. - EXPECT_EQ(aip->PassThrough(graph), false); + MediaEnginePrefs settings; + settings.mChannels = channels; + settings.mAgcOn = true; + settings.mAgc2Forced = true; + aip->ApplySettings(graph, nullptr, settings); + EXPECT_EQ(aip->IsPassThrough(graph), false); aip->Start(graph); { - EXPECT_EQ(aip->NumBufferedFrames(graph), 480); AudioSegment output; { - // Trim the prebuffering silence. - AudioSegment data; - aip->Process(graph, 0, 4800, &input, &data); + aip->Process(track, 0, 4800, &input, &data); EXPECT_EQ(input.GetDuration(), 4800); EXPECT_EQ(data.GetDuration(), 4800); + // Extract another 480 frames to account for delay from pre-buffering. + EXPECT_EQ(aip->NumBufferedFrames(graph), 480); AudioSegment dummy; dummy.AppendNullData(480); - aip->Process(graph, 0, 480, &dummy, &data); + aip->Process(track, 0, 480, &dummy, &data); EXPECT_EQ(dummy.GetDuration(), 480); EXPECT_EQ(data.GetDuration(), 480 + 4800); - // Ignore the pre-buffering data + // Ignore the pre-buffering silence. output.AppendSlice(data, 480, 480 + 4800); } @@ -298,10 +322,12 @@ TEST(TestAudioInputProcessing, ProcessDataWithDifferentPrincipals) } // Check the principals in pass-through mode. - aip->SetPassThrough(graph, true); + settings.mAgcOn = false; + aip->ApplySettings(graph, nullptr, settings); + EXPECT_EQ(aip->IsPassThrough(graph), true); { AudioSegment output; - aip->Process(graph, 0, 4800, &input, &output); + aip->Process(track, 0, 4800, &input, &output); EXPECT_EQ(input.GetDuration(), 4800); EXPECT_EQ(output.GetDuration(), 4800); @@ -309,7 +335,7 @@ TEST(TestAudioInputProcessing, ProcessDataWithDifferentPrincipals) } aip->Stop(graph); - graph->Destroy(); + track->Destroy(); } TEST(TestAudioInputProcessing, Downmixing) @@ -318,6 +344,7 @@ TEST(TestAudioInputProcessing, Downmixing) const uint32_t channels = 4; auto graph = MakeRefPtr<NiceMock<MockGraph>>(rate); graph->Init(channels); + RefPtr track = AudioProcessingTrack::Create(graph); auto aip = MakeRefPtr<AudioInputProcessing>(channels); @@ -327,7 +354,12 @@ TEST(TestAudioInputProcessing, Downmixing) GraphTime processedTime; GraphTime nextTime; - aip->SetPassThrough(graph, false); + MediaEnginePrefs settings; + settings.mChannels = channels; + settings.mAgcOn = true; + settings.mAgc2Forced = true; + aip->ApplySettings(graph, nullptr, settings); + EXPECT_EQ(aip->IsPassThrough(graph), false); aip->Start(graph); processedTime = 0; @@ -347,7 +379,7 @@ TEST(TestAudioInputProcessing, Downmixing) // downmix to mono, scaling the input by 1/4 in the process. // We can't compare the input and output signal because the sine is going to // be mangledui - aip->Process(graph, processedTime, nextTime, &input, &output); + aip->Process(track, processedTime, nextTime, &input, &output); EXPECT_EQ(input.GetDuration(), nextTime - processedTime); EXPECT_EQ(output.GetDuration(), nextTime); EXPECT_EQ(output.MaxChannelCount(), 1u); @@ -367,15 +399,18 @@ TEST(TestAudioInputProcessing, Downmixing) } } - // Now, repeat the test, checking we get the unmodified 4 channels. - aip->SetPassThrough(graph, true); + // Now, repeat the test in pass-through mode, checking we get the unmodified + // 4 channels. + settings.mAgcOn = false; + aip->ApplySettings(graph, nullptr, settings); + EXPECT_EQ(aip->IsPassThrough(graph), true); AudioSegment input, output; processedTime = nextTime; nextTime += MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(frames); generator.Generate(input, nextTime - processedTime); - aip->Process(graph, processedTime, nextTime, &input, &output); + aip->Process(track, processedTime, nextTime, &input, &output); EXPECT_EQ(input.GetDuration(), nextTime - processedTime); EXPECT_EQ(output.GetDuration(), nextTime - processedTime); // This time, no downmix: 4 channels of input, 4 channels of output @@ -391,5 +426,5 @@ TEST(TestAudioInputProcessing, Downmixing) } aip->Stop(graph); - graph->Destroy(); + track->Destroy(); } diff --git a/dom/media/gtest/TestAudioPacketizer.cpp b/dom/media/gtest/TestAudioPacketizer.cpp index 96a2d6f08c..2c9f86bb14 100644 --- a/dom/media/gtest/TestAudioPacketizer.cpp +++ b/dom/media/gtest/TestAudioPacketizer.cpp @@ -7,6 +7,7 @@ #include <math.h> #include <memory> #include "../AudioPacketizer.h" +#include "../TimedPacketizer.h" #include "gtest/gtest.h" using namespace mozilla; @@ -25,16 +26,15 @@ class AutoBuffer { int16_t Sequence(int16_t* aBuffer, uint32_t aSize, uint32_t aStart = 0) { uint32_t i; for (i = 0; i < aSize; i++) { - aBuffer[i] = aStart + i; + aBuffer[i] = (aStart + i) % INT16_MAX; } return aStart + i; } -void IsSequence(std::unique_ptr<int16_t[]> aBuffer, uint32_t aSize, - uint32_t aStart = 0) { +void IsSequence(int16_t* aBuffer, uint32_t aSize, uint32_t aStart = 0) { for (uint32_t i = 0; i < aSize; i++) { - ASSERT_TRUE(aBuffer[i] == static_cast<int64_t>(aStart + i)) - << "Buffer is not a sequence at offset " << i << '\n'; + ASSERT_EQ(aBuffer[i], static_cast<int64_t>((aStart + i) % INT16_MAX)) + << "Buffer is not a sequence at offset " << i << '\n'; } // Buffer is a sequence. } @@ -70,7 +70,7 @@ TEST(AudioPacketizer, Test) seqEnd = Sequence(b.Get(), channels * 441, prevEnd); ap.Input(b.Get(), 441); std::unique_ptr<int16_t[]> out(ap.Output()); - IsSequence(std::move(out), 441 * channels, prevEnd); + IsSequence(out.get(), 441 * channels, prevEnd); } } // Simple test, with input/output buffer size aligned on the packet size, @@ -89,8 +89,8 @@ TEST(AudioPacketizer, Test) ap.Input(b1.Get(), 441); std::unique_ptr<int16_t[]> out(ap.Output()); std::unique_ptr<int16_t[]> out2(ap.Output()); - IsSequence(std::move(out), 441 * channels, prevEnd0); - IsSequence(std::move(out2), 441 * channels, prevEnd1); + IsSequence(out.get(), 441 * channels, prevEnd0); + IsSequence(out2.get(), 441 * channels, prevEnd1); } } // Input/output buffer size not aligned on the packet size, @@ -108,9 +108,9 @@ TEST(AudioPacketizer, Test) ap.Input(b1.Get(), 480); std::unique_ptr<int16_t[]> out(ap.Output()); std::unique_ptr<int16_t[]> out2(ap.Output()); - IsSequence(std::move(out), 441 * channels, prevEnd); + IsSequence(out.get(), 441 * channels, prevEnd); prevEnd += 441 * channels; - IsSequence(std::move(out2), 441 * channels, prevEnd); + IsSequence(out2.get(), 441 * channels, prevEnd); prevEnd += 441 * channels; } printf("Available: %d\n", ap.PacketsAvailable()); @@ -161,3 +161,34 @@ TEST(AudioPacketizer, Test) } } } + +TEST(TimedPacketizer, Test) +{ + const int channels = 2; + const int64_t rate = 48000; + const int64_t inputPacketSize = 240; + const int64_t packetSize = 96; + TimedPacketizer<int16_t, int16_t> tp(packetSize, channels, 0, rate); + int16_t prevEnd = 0; + int16_t prevSeq = 0; + nsTArray<int16_t> packet; + uint64_t tsCheck = 0; + packet.SetLength(tp.PacketSize() * channels); + for (int16_t i = 0; i < 10; i++) { + AutoBuffer<int16_t> b(inputPacketSize * channels); + prevSeq = Sequence(b.Get(), inputPacketSize * channels, prevSeq); + tp.Input(b.Get(), inputPacketSize); + while (tp.PacketsAvailable()) { + media::TimeUnit ts = tp.Output(packet.Elements()); + IsSequence(packet.Elements(), packetSize * channels, prevEnd); + EXPECT_EQ(ts, media::TimeUnit(tsCheck, rate)); + prevEnd += packetSize * channels; + tsCheck += packetSize; + } + } + EXPECT_TRUE(!tp.PacketsAvailable()); + uint32_t drained; + media::TimeUnit ts = tp.Drain(packet.Elements(), drained); + EXPECT_EQ(ts, media::TimeUnit(tsCheck, rate)); + EXPECT_LE(drained, packetSize); +} diff --git a/dom/media/gtest/TestAudioTrackGraph.cpp b/dom/media/gtest/TestAudioTrackGraph.cpp index 1bd255bed1..7be1224ab9 100644 --- a/dom/media/gtest/TestAudioTrackGraph.cpp +++ b/dom/media/gtest/TestAudioTrackGraph.cpp @@ -59,39 +59,27 @@ struct StopInputProcessing : public ControlMessage { void Run() override { mInputProcessing->Stop(mTrack->Graph()); } }; -struct SetPassThrough : public ControlMessage { - const RefPtr<AudioInputProcessing> mInputProcessing; - const bool mPassThrough; - - SetPassThrough(MediaTrack* aTrack, AudioInputProcessing* aInputProcessing, - bool aPassThrough) - : ControlMessage(aTrack), - mInputProcessing(aInputProcessing), - mPassThrough(aPassThrough) {} - void Run() override { - EXPECT_EQ(mInputProcessing->PassThrough(mTrack->Graph()), !mPassThrough); - mInputProcessing->SetPassThrough(mTrack->Graph(), mPassThrough); - } -}; - -struct SetRequestedInputChannelCount : public ControlMessage { - const CubebUtils::AudioDeviceID mDeviceId; - const RefPtr<AudioInputProcessing> mInputProcessing; - const uint32_t mChannelCount; +void QueueApplySettings(AudioProcessingTrack* aTrack, + AudioInputProcessing* aInputProcessing, + const MediaEnginePrefs& aSettings) { + aTrack->QueueControlMessageWithNoShutdown( + [inputProcessing = RefPtr{aInputProcessing}, aSettings, + // If the track is not connected to a device then the particular + // AudioDeviceID (nullptr) passed to ReevaluateInputDevice() is not + // important. + deviceId = aTrack->DeviceId().valueOr(nullptr), + graph = aTrack->Graph()] { + inputProcessing->ApplySettings(graph, deviceId, aSettings); + }); +} - SetRequestedInputChannelCount(MediaTrack* aTrack, - CubebUtils::AudioDeviceID aDeviceId, - AudioInputProcessing* aInputProcessing, - uint32_t aChannelCount) - : ControlMessage(aTrack), - mDeviceId(aDeviceId), - mInputProcessing(aInputProcessing), - mChannelCount(aChannelCount) {} - void Run() override { - mInputProcessing->SetRequestedInputChannelCount(mTrack->Graph(), mDeviceId, - mChannelCount); - } -}; +void QueueExpectIsPassThrough(AudioProcessingTrack* aTrack, + AudioInputProcessing* aInputProcessing) { + aTrack->QueueControlMessageWithNoShutdown( + [inputProcessing = RefPtr{aInputProcessing}, graph = aTrack->Graph()] { + EXPECT_EQ(inputProcessing->IsPassThrough(graph), true); + }); +} #endif // MOZ_WEBRTC class GoFaster : public ControlMessage { @@ -557,8 +545,7 @@ class TestDeviceInputConsumerTrack : public DeviceInputConsumerTrack { } else { MOZ_ASSERT(mInputs.Length() == 1); AudioSegment data; - DeviceInputConsumerTrack::GetInputSourceData(data, mInputs[0], aFrom, - aTo); + DeviceInputConsumerTrack::GetInputSourceData(data, aFrom, aTo); GetData<AudioSegment>()->AppendFrom(&data); } }; @@ -620,8 +607,8 @@ TEST(TestAudioTrackGraph, DeviceChangedCallback) TestDeviceInputConsumerTrack::Create(graphImpl); track1->ConnectDeviceInput(device1, listener1.get(), PRINCIPAL_HANDLE_NONE); - EXPECT_TRUE(track1->ConnectToNativeDevice()); - EXPECT_FALSE(track1->ConnectToNonNativeDevice()); + EXPECT_TRUE(track1->ConnectedToNativeDevice()); + EXPECT_FALSE(track1->ConnectedToNonNativeDevice()); auto started = Invoke([&] { return graphImpl->NotifyWhenDeviceStarted(nullptr); }); RefPtr<SmartMockCubebStream> stream1 = WaitFor(cubeb->StreamInitEvent()); @@ -637,8 +624,8 @@ TEST(TestAudioTrackGraph, DeviceChangedCallback) TestDeviceInputConsumerTrack::Create(graphImpl); track2->ConnectDeviceInput(device2, listener2.get(), PRINCIPAL_HANDLE_NONE); - EXPECT_FALSE(track2->ConnectToNativeDevice()); - EXPECT_TRUE(track2->ConnectToNonNativeDevice()); + EXPECT_FALSE(track2->ConnectedToNativeDevice()); + EXPECT_TRUE(track2->ConnectedToNonNativeDevice()); RefPtr<SmartMockCubebStream> stream2 = WaitFor(cubeb->StreamInitEvent()); EXPECT_TRUE(stream2->mHasInput); EXPECT_FALSE(stream2->mHasOutput); @@ -852,8 +839,8 @@ TEST(TestAudioTrackGraph, RestartAudioIfMaxChannelCountChanged) track1->ConnectDeviceInput(nativeDevice, listener1.get(), PRINCIPAL_HANDLE_NONE); - EXPECT_TRUE(track1->ConnectToNativeDevice()); - EXPECT_FALSE(track1->ConnectToNonNativeDevice()); + EXPECT_TRUE(track1->ConnectedToNativeDevice()); + EXPECT_FALSE(track1->ConnectedToNonNativeDevice()); auto started = Invoke([&] { return graphImpl->NotifyWhenDeviceStarted(nullptr); }); nativeStream = WaitFor(cubeb->StreamInitEvent()); @@ -891,8 +878,8 @@ TEST(TestAudioTrackGraph, RestartAudioIfMaxChannelCountChanged) TestDeviceInputConsumerTrack::Create(graphImpl); track3->ConnectDeviceInput(nonNativeDevice, listener3.get(), PRINCIPAL_HANDLE_NONE); - EXPECT_FALSE(track3->ConnectToNativeDevice()); - EXPECT_TRUE(track3->ConnectToNonNativeDevice()); + EXPECT_FALSE(track3->ConnectedToNativeDevice()); + EXPECT_TRUE(track3->ConnectedToNonNativeDevice()); RefPtr<SmartMockCubebStream> nonNativeStream = WaitFor(cubeb->StreamInitEvent()); @@ -1176,8 +1163,7 @@ TEST(TestAudioTrackGraph, ErrorCallback) auto started = Invoke([&] { processingTrack = AudioProcessingTrack::Create(graph); listener = new AudioInputProcessing(2); - processingTrack->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(processingTrack, listener, true)); + QueueExpectIsPassThrough(processingTrack, listener); processingTrack->SetInputProcessing(listener); processingTrack->GraphImpl()->AppendMessage( MakeUnique<StartInputProcessing>(processingTrack, listener)); @@ -1247,8 +1233,7 @@ TEST(TestAudioTrackGraph, AudioProcessingTrack) port = outputTrack->AllocateInputPort(processingTrack); /* Primary graph: Open Audio Input through SourceMediaTrack */ listener = new AudioInputProcessing(2); - processingTrack->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(processingTrack, listener, true)); + QueueExpectIsPassThrough(processingTrack, listener); processingTrack->SetInputProcessing(listener); processingTrack->GraphImpl()->AppendMessage( MakeUnique<StartInputProcessing>(processingTrack, listener)); @@ -1336,12 +1321,22 @@ TEST(TestAudioTrackGraph, ReConnectDeviceInput) outputTrack->QueueSetAutoend(false); outputTrack->AddAudioOutput(reinterpret_cast<void*>(1), nullptr); port = outputTrack->AllocateInputPort(processingTrack); - listener = new AudioInputProcessing(2); + + const int32_t channelCount = 2; + listener = new AudioInputProcessing(channelCount); processingTrack->SetInputProcessing(listener); processingTrack->GraphImpl()->AppendMessage( MakeUnique<StartInputProcessing>(processingTrack, listener)); processingTrack->ConnectDeviceInput(deviceId, listener, PRINCIPAL_HANDLE_NONE); + MediaEnginePrefs settings; + settings.mChannels = channelCount; + settings.mAgcOn = true; // Turn off pass-through. + // AGC1 Mode 0 interferes with AudioVerifier's frequency estimation + // through zero-crossing counts. + settings.mAgc2Forced = true; + QueueApplySettings(processingTrack, listener, settings); + return graph->NotifyWhenDeviceStarted(nullptr); }); @@ -1494,8 +1489,7 @@ TEST(TestAudioTrackGraph, AudioProcessingTrackDisabling) port = outputTrack->AllocateInputPort(processingTrack); /* Primary graph: Open Audio Input through SourceMediaTrack */ listener = new AudioInputProcessing(2); - processingTrack->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(processingTrack, listener, true)); + QueueExpectIsPassThrough(processingTrack, listener); processingTrack->SetInputProcessing(listener); processingTrack->ConnectDeviceInput(deviceId, listener, PRINCIPAL_HANDLE_NONE); @@ -1511,32 +1505,40 @@ TEST(TestAudioTrackGraph, AudioProcessingTrackDisabling) stream->SetOutputRecordingEnabled(true); // Wait for a second worth of audio data. - uint32_t totalFrames = 0; - WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) { - totalFrames += aFrames; - return totalFrames > static_cast<uint32_t>(graph->GraphRate()); - }); + uint64_t targetPosition = graph->GraphRate(); + auto AdvanceToTargetPosition = [&] { + DispatchFunction([&] { + processingTrack->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb)); + }); + WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) { + // Position() gives a more up-to-date indication than summing aFrames if + // multiple events are queued. + if (stream->Position() < targetPosition) { + return false; + } + cubeb->DontGoFaster(); + return true; + }); + }; + AdvanceToTargetPosition(); const uint32_t ITERATION_COUNT = 5; uint32_t iterations = ITERATION_COUNT; - DisabledTrackMode currentMode = DisabledTrackMode::SILENCE_BLACK; + DisabledTrackMode nextMode = DisabledTrackMode::SILENCE_BLACK; while (iterations--) { // toggle the track enabled mode, wait a second, do this ITERATION_COUNT // times DispatchFunction([&] { - processingTrack->SetDisabledTrackMode(currentMode); - if (currentMode == DisabledTrackMode::SILENCE_BLACK) { - currentMode = DisabledTrackMode::ENABLED; + processingTrack->SetDisabledTrackMode(nextMode); + if (nextMode == DisabledTrackMode::SILENCE_BLACK) { + nextMode = DisabledTrackMode::ENABLED; } else { - currentMode = DisabledTrackMode::SILENCE_BLACK; + nextMode = DisabledTrackMode::SILENCE_BLACK; } }); - totalFrames = 0; - WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) { - totalFrames += aFrames; - return totalFrames > static_cast<uint32_t>(graph->GraphRate()); - }); + targetPosition += graph->GraphRate(); + AdvanceToTargetPosition(); } // Clean up. @@ -1595,8 +1597,7 @@ TEST(TestAudioTrackGraph, SetRequestedInputChannelCount) RefPtr<AudioProcessingTrack> track1 = AudioProcessingTrack::Create(graph); RefPtr<AudioInputProcessing> listener1 = new AudioInputProcessing(2); track1->SetInputProcessing(listener1); - track1->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(track1, listener1, true)); + QueueExpectIsPassThrough(track1, listener1); track1->GraphImpl()->AppendMessage( MakeUnique<StartInputProcessing>(track1, listener1)); track1->ConnectDeviceInput(device1, listener1, PRINCIPAL_HANDLE_NONE); @@ -1617,8 +1618,7 @@ TEST(TestAudioTrackGraph, SetRequestedInputChannelCount) RefPtr<AudioProcessingTrack> track2 = AudioProcessingTrack::Create(graph); RefPtr<AudioInputProcessing> listener2 = new AudioInputProcessing(1); track2->SetInputProcessing(listener2); - track2->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(track2, listener2, true)); + QueueExpectIsPassThrough(track2, listener2); track2->GraphImpl()->AppendMessage( MakeUnique<StartInputProcessing>(track2, listener2)); track2->ConnectDeviceInput(device2, listener2, PRINCIPAL_HANDLE_NONE); @@ -1635,7 +1635,7 @@ TEST(TestAudioTrackGraph, SetRequestedInputChannelCount) auto setNewChannelCount = [&](const RefPtr<AudioProcessingTrack> aTrack, const RefPtr<AudioInputProcessing>& aListener, RefPtr<SmartMockCubebStream>& aStream, - uint32_t aChannelCount) { + int32_t aChannelCount) { bool destroyed = false; MediaEventListener destroyListener = cubeb->StreamDestroyEvent().Connect( AbstractThread::GetCurrent(), @@ -1650,11 +1650,9 @@ TEST(TestAudioTrackGraph, SetRequestedInputChannelCount) newStream = aCreated; }); - DispatchFunction([&] { - aTrack->GraphImpl()->AppendMessage( - MakeUnique<SetRequestedInputChannelCount>(aTrack, *aTrack->DeviceId(), - aListener, aChannelCount)); - }); + MediaEnginePrefs settings; + settings.mChannels = aChannelCount; + QueueApplySettings(aTrack, aListener, settings); SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>( "TEST(TestAudioTrackGraph, SetRequestedInputChannelCount)"_ns, @@ -1726,14 +1724,12 @@ TEST(TestAudioTrackGraph, RestartAudioIfProcessingMaxChannelCountChanged) auto setNewChannelCount = [&](const RefPtr<AudioProcessingTrack>& aTrack, const RefPtr<AudioInputProcessing>& aListener, RefPtr<SmartMockCubebStream>& aStream, - uint32_t aChannelCount) { + int32_t aChannelCount) { ASSERT_TRUE(!!aTrack); ASSERT_TRUE(!!aListener); ASSERT_TRUE(!!aStream); ASSERT_TRUE(aStream->mHasInput); - ASSERT_NE(aChannelCount, 0U); - - const CubebUtils::AudioDeviceID device = *aTrack->DeviceId(); + ASSERT_NE(aChannelCount, 0); bool destroyed = false; MediaEventListener destroyListener = cubeb->StreamDestroyEvent().Connect( @@ -1749,11 +1745,9 @@ TEST(TestAudioTrackGraph, RestartAudioIfProcessingMaxChannelCountChanged) newStream = aCreated; }); - DispatchFunction([&] { - aTrack->GraphImpl()->AppendMessage( - MakeUnique<SetRequestedInputChannelCount>(aTrack, device, aListener, - aChannelCount)); - }); + MediaEnginePrefs settings; + settings.mChannels = aChannelCount; + QueueApplySettings(aTrack, aListener, settings); SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>( "TEST(TestAudioTrackGraph, RestartAudioIfProcessingMaxChannelCountChanged) #1"_ns, @@ -1794,8 +1788,7 @@ TEST(TestAudioTrackGraph, RestartAudioIfProcessingMaxChannelCountChanged) aTrack = AudioProcessingTrack::Create(graph); aListener = new AudioInputProcessing(aChannelCount); aTrack->SetInputProcessing(aListener); - aTrack->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(aTrack, aListener, true)); + QueueExpectIsPassThrough(aTrack, aListener); aTrack->GraphImpl()->AppendMessage( MakeUnique<StartInputProcessing>(aTrack, aListener)); @@ -1829,8 +1822,7 @@ TEST(TestAudioTrackGraph, RestartAudioIfProcessingMaxChannelCountChanged) track1 = AudioProcessingTrack::Create(graph); listener1 = new AudioInputProcessing(1); track1->SetInputProcessing(listener1); - track1->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(track1, listener1, true)); + QueueExpectIsPassThrough(track1, listener1); track1->GraphImpl()->AppendMessage( MakeUnique<StartInputProcessing>(track1, listener1)); track1->ConnectDeviceInput(nativeDevice, listener1, PRINCIPAL_HANDLE_NONE); @@ -1873,8 +1865,7 @@ TEST(TestAudioTrackGraph, RestartAudioIfProcessingMaxChannelCountChanged) RefPtr<AudioProcessingTrack> track3 = AudioProcessingTrack::Create(graph); RefPtr<AudioInputProcessing> listener3 = new AudioInputProcessing(1); track3->SetInputProcessing(listener3); - track3->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(track3, listener3, true)); + QueueExpectIsPassThrough(track3, listener3); track3->GraphImpl()->AppendMessage( MakeUnique<StartInputProcessing>(track3, listener3)); track3->ConnectDeviceInput(nonNativeDevice, listener3, @@ -1992,12 +1983,13 @@ TEST(TestAudioTrackGraph, SetInputChannelCountBeforeAudioCallbackDriver) DispatchFunction([&] { track = AudioProcessingTrack::Create(graph); listener = new AudioInputProcessing(2); - track->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(track, listener, true)); + QueueExpectIsPassThrough(track, listener); track->SetInputProcessing(listener); - track->GraphImpl()->AppendMessage( - MakeUnique<SetRequestedInputChannelCount>(track, deviceId, listener, - 1)); + + MediaEnginePrefs settings; + settings.mChannels = 1; + QueueApplySettings(track, listener, settings); + track->GraphImpl()->AppendMessage( MakeUnique<GuardMessage>(track, std::move(h))); }); @@ -2058,8 +2050,7 @@ TEST(TestAudioTrackGraph, StartAudioDeviceBeforeStartingAudioProcessing) DispatchFunction([&] { track = AudioProcessingTrack::Create(graph); listener = new AudioInputProcessing(2); - track->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(track, listener, true)); + QueueExpectIsPassThrough(track, listener); track->SetInputProcessing(listener); // Start audio device without starting audio processing. track->ConnectDeviceInput(deviceId, listener, PRINCIPAL_HANDLE_NONE); @@ -2124,8 +2115,7 @@ TEST(TestAudioTrackGraph, StopAudioProcessingBeforeStoppingAudioDevice) DispatchFunction([&] { track = AudioProcessingTrack::Create(graph); listener = new AudioInputProcessing(2); - track->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(track, listener, true)); + QueueExpectIsPassThrough(track, listener); track->SetInputProcessing(listener); track->GraphImpl()->AppendMessage( MakeUnique<StartInputProcessing>(track, listener)); @@ -2260,8 +2250,7 @@ TEST(TestAudioTrackGraph, SwitchNativeAudioProcessingTrack) RefPtr<AudioProcessingTrack> track1 = AudioProcessingTrack::Create(graph); RefPtr<AudioInputProcessing> listener1 = new AudioInputProcessing(1); track1->SetInputProcessing(listener1); - track1->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(track1, listener1, true)); + QueueExpectIsPassThrough(track1, listener1); track1->GraphImpl()->AppendMessage( MakeUnique<StartInputProcessing>(track1, listener1)); track1->ConnectDeviceInput(device1, listener1, PRINCIPAL_HANDLE_NONE); @@ -2284,8 +2273,7 @@ TEST(TestAudioTrackGraph, SwitchNativeAudioProcessingTrack) RefPtr<AudioProcessingTrack> track2 = AudioProcessingTrack::Create(graph); RefPtr<AudioInputProcessing> listener2 = new AudioInputProcessing(2); track2->SetInputProcessing(listener2); - track2->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(track2, listener2, true)); + QueueExpectIsPassThrough(track2, listener2); track2->GraphImpl()->AppendMessage( MakeUnique<StartInputProcessing>(track2, listener2)); track2->ConnectDeviceInput(device2, listener2, PRINCIPAL_HANDLE_NONE); @@ -2304,8 +2292,7 @@ TEST(TestAudioTrackGraph, SwitchNativeAudioProcessingTrack) RefPtr<AudioProcessingTrack> track3 = AudioProcessingTrack::Create(graph); RefPtr<AudioInputProcessing> listener3 = new AudioInputProcessing(1); track3->SetInputProcessing(listener3); - track3->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(track3, listener3, true)); + QueueExpectIsPassThrough(track3, listener3); track3->GraphImpl()->AppendMessage( MakeUnique<StartInputProcessing>(track3, listener3)); track3->ConnectDeviceInput(device3, listener3, PRINCIPAL_HANDLE_NONE); @@ -2410,8 +2397,7 @@ void TestCrossGraphPort(uint32_t aInputRate, uint32_t aOutputRate, /* Primary graph: Create input track and open it */ processingTrack = AudioProcessingTrack::Create(primary); listener = new AudioInputProcessing(2); - processingTrack->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(processingTrack, listener, true)); + QueueExpectIsPassThrough(processingTrack, listener); processingTrack->SetInputProcessing(listener); processingTrack->GraphImpl()->AppendMessage( MakeUnique<StartInputProcessing>(processingTrack, listener)); @@ -2632,8 +2618,7 @@ TEST(TestAudioTrackGraph, SecondaryOutputDevice) /* Create an input track and connect it to a device */ processingTrack = AudioProcessingTrack::Create(graph); listener = new AudioInputProcessing(2); - processingTrack->GraphImpl()->AppendMessage( - MakeUnique<SetPassThrough>(processingTrack, listener, true)); + QueueExpectIsPassThrough(processingTrack, listener); processingTrack->SetInputProcessing(listener); processingTrack->GraphImpl()->AppendMessage( MakeUnique<StartInputProcessing>(processingTrack, listener)); @@ -2719,6 +2704,116 @@ TEST(TestAudioTrackGraph, SecondaryOutputDevice) }); WaitFor(primaryStream->OutputVerificationEvent()); } + +// Test when AudioInputProcessing expects clock drift +TEST(TestAudioInputProcessing, ClockDriftExpectation) +{ + MockCubeb* cubeb = new MockCubeb(); + CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext()); + + const TrackRate rate = 44100; + + MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance( + MediaTrackGraph::SYSTEM_THREAD_DRIVER, + /*Window ID*/ 1, rate, nullptr, GetMainThreadSerialEventTarget()); + + auto createInputProcessing = + [&](CubebUtils::AudioDeviceID aDeviceID, + RefPtr<AudioProcessingTrack>* aProcessingTrack, + RefPtr<AudioInputProcessing>* aInputProcessing) { + /* Create an input track and connect it to a device */ + const int32_t channelCount = 2; + RefPtr processingTrack = AudioProcessingTrack::Create(graph); + RefPtr inputProcessing = new AudioInputProcessing(channelCount); + processingTrack->SetInputProcessing(inputProcessing); + MediaEnginePrefs settings; + settings.mChannels = channelCount; + settings.mAecOn = true; + QueueApplySettings(processingTrack, inputProcessing, settings); + processingTrack->GraphImpl()->AppendMessage( + MakeUnique<StartInputProcessing>(processingTrack, inputProcessing)); + processingTrack->ConnectDeviceInput(aDeviceID, inputProcessing, + PRINCIPAL_HANDLE_NONE); + aProcessingTrack->swap(processingTrack); + aInputProcessing->swap(inputProcessing); + }; + + // Native input, which uses a duplex stream + RefPtr<AudioProcessingTrack> processingTrack1; + RefPtr<AudioInputProcessing> inputProcessing1; + DispatchFunction([&] { + createInputProcessing(nullptr, &processingTrack1, &inputProcessing1); + }); + // Non-native input + const auto* nonNativeInputDeviceID = CubebUtils::AudioDeviceID(1); + RefPtr<AudioProcessingTrack> processingTrack2; + RefPtr<AudioInputProcessing> inputProcessing2; + DispatchFunction([&] { + createInputProcessing(nonNativeInputDeviceID, &processingTrack2, + &inputProcessing2); + processingTrack2->AddAudioOutput(nullptr, nullptr, rate); + }); + + RefPtr<SmartMockCubebStream> primaryStream; + RefPtr<SmartMockCubebStream> nonNativeInputStream; + WaitUntil(cubeb->StreamInitEvent(), + [&](RefPtr<SmartMockCubebStream>&& stream) { + if (stream->OutputChannels() > 0) { + primaryStream = std::move(stream); + return false; + } + nonNativeInputStream = std::move(stream); + return true; + }); + EXPECT_EQ(nonNativeInputStream->GetInputDeviceID(), nonNativeInputDeviceID); + + // Wait until non-native input signal reaches the output, when input + // processing has run and so has been configured. + WaitFor(primaryStream->FramesVerifiedEvent()); + + const void* secondaryOutputDeviceID = CubebUtils::AudioDeviceID(2); + DispatchFunction([&] { + // Check input processing config with output to primary device. + processingTrack1->QueueControlMessageWithNoShutdown([&] { + EXPECT_FALSE(inputProcessing1->HadAECAndDrift()); + EXPECT_TRUE(inputProcessing2->HadAECAndDrift()); + }); + + // Switch output to a secondary device. + processingTrack2->RemoveAudioOutput(nullptr); + processingTrack2->AddAudioOutput(nullptr, secondaryOutputDeviceID, rate); + }); + + RefPtr<SmartMockCubebStream> secondaryOutputStream = + WaitFor(cubeb->StreamInitEvent()); + EXPECT_EQ(secondaryOutputStream->GetOutputDeviceID(), + secondaryOutputDeviceID); + + WaitFor(secondaryOutputStream->FramesVerifiedEvent()); + DispatchFunction([&] { + // Check input processing config with output to secondary device. + processingTrack1->QueueControlMessageWithNoShutdown([&] { + EXPECT_TRUE(inputProcessing1->HadAECAndDrift()); + EXPECT_TRUE(inputProcessing2->HadAECAndDrift()); + }); + }); + + auto destroyInputProcessing = [&](AudioProcessingTrack* aProcessingTrack, + AudioInputProcessing* aInputProcessing) { + aProcessingTrack->GraphImpl()->AppendMessage( + MakeUnique<StopInputProcessing>(aProcessingTrack, aInputProcessing)); + aProcessingTrack->DisconnectDeviceInput(); + aProcessingTrack->Destroy(); + }; + + DispatchFunction([&] { + // Clean up + destroyInputProcessing(processingTrack1, inputProcessing1); + destroyInputProcessing(processingTrack2, inputProcessing2); + }); + // Wait for stream stop to ensure that expectations have been checked. + WaitFor(nonNativeInputStream->OutputVerificationEvent()); +} #endif // MOZ_WEBRTC #undef Invoke diff --git a/dom/media/gtest/TestDeviceInputTrack.cpp b/dom/media/gtest/TestDeviceInputTrack.cpp index 6eb8c08774..14b5227f9d 100644 --- a/dom/media/gtest/TestDeviceInputTrack.cpp +++ b/dom/media/gtest/TestDeviceInputTrack.cpp @@ -150,16 +150,16 @@ TEST_F(TestDeviceInputTrack, DeviceInputConsumerTrack) { RefPtr<TestDeviceInputConsumerTrack> track1 = TestDeviceInputConsumerTrack::Create(mGraph); track1->ConnectDeviceInput(device1, listener1.get(), testPrincipal); - EXPECT_TRUE(track1->ConnectToNativeDevice()); - EXPECT_FALSE(track1->ConnectToNonNativeDevice()); + EXPECT_TRUE(track1->ConnectedToNativeDevice()); + EXPECT_FALSE(track1->ConnectedToNonNativeDevice()); const CubebUtils::AudioDeviceID device2 = (void*)2; RefPtr<TestAudioDataListener> listener2 = new TestAudioDataListener(2, false); RefPtr<TestDeviceInputConsumerTrack> track2 = TestDeviceInputConsumerTrack::Create(mGraph); track2->ConnectDeviceInput(device2, listener2.get(), testPrincipal); - EXPECT_FALSE(track2->ConnectToNativeDevice()); - EXPECT_TRUE(track2->ConnectToNonNativeDevice()); + EXPECT_FALSE(track2->ConnectedToNativeDevice()); + EXPECT_TRUE(track2->ConnectedToNonNativeDevice()); track2->Destroy(); mGraph->RemoveTrackGraphThread(track2); diff --git a/dom/media/gtest/TestMediaDataEncoder.cpp b/dom/media/gtest/TestMediaDataEncoder.cpp index 27a6b7cd07..39c92fb19c 100644 --- a/dom/media/gtest/TestMediaDataEncoder.cpp +++ b/dom/media/gtest/TestMediaDataEncoder.cpp @@ -33,7 +33,7 @@ #define FRAME_RATE 30 #define FRAME_DURATION (1000000 / FRAME_RATE) #define BIT_RATE (1000 * 1000) // 1Mbps -#define BIT_RATE_MODE MediaDataEncoder::BitrateMode::Variable +#define BIT_RATE_MODE BitrateMode::Variable #define KEYFRAME_INTERVAL FRAME_RATE // 1 keyframe per second using namespace mozilla; @@ -156,9 +156,8 @@ class MediaDataEncoderTest : public testing::Test { template <typename T> already_AddRefed<MediaDataEncoder> CreateVideoEncoder( - CodecType aCodec, MediaDataEncoder::Usage aUsage, - MediaDataEncoder::PixelFormat aPixelFormat, int32_t aWidth, int32_t aHeight, - MediaDataEncoder::ScalabilityMode aScalabilityMode, + CodecType aCodec, Usage aUsage, dom::ImageBitmapFormat aPixelFormat, + int32_t aWidth, int32_t aHeight, ScalabilityMode aScalabilityMode, const Maybe<T>& aSpecific) { RefPtr<PEMFactory> f(new PEMFactory()); @@ -171,13 +170,7 @@ already_AddRefed<MediaDataEncoder> CreateVideoEncoder( "TestMediaDataEncoder")); RefPtr<MediaDataEncoder> e; -#ifdef MOZ_WIDGET_ANDROID - const MediaDataEncoder::HardwarePreference pref = - MediaDataEncoder::HardwarePreference::None; -#else - const MediaDataEncoder::HardwarePreference pref = - MediaDataEncoder::HardwarePreference::None; -#endif + const HardwarePreference pref = HardwarePreference::None; e = f->CreateEncoder( EncoderConfig(aCodec, gfx::IntSize{aWidth, aHeight}, aUsage, aPixelFormat, aPixelFormat, FRAME_RATE /* FPS */, @@ -190,12 +183,10 @@ already_AddRefed<MediaDataEncoder> CreateVideoEncoder( } static already_AddRefed<MediaDataEncoder> CreateH264Encoder( - MediaDataEncoder::Usage aUsage = MediaDataEncoder::Usage::Realtime, - MediaDataEncoder::PixelFormat aPixelFormat = - MediaDataEncoder::PixelFormat::YUV420P, + Usage aUsage = Usage::Realtime, + dom::ImageBitmapFormat aPixelFormat = dom::ImageBitmapFormat::YUV420P, int32_t aWidth = WIDTH, int32_t aHeight = HEIGHT, - MediaDataEncoder::ScalabilityMode aScalabilityMode = - MediaDataEncoder::ScalabilityMode::None, + ScalabilityMode aScalabilityMode = ScalabilityMode::None, const Maybe<H264Specific>& aSpecific = Some(kH264SpecificAnnexB)) { return CreateVideoEncoder(CodecType::H264, aUsage, aPixelFormat, aWidth, aHeight, aScalabilityMode, aSpecific); @@ -234,10 +225,7 @@ static bool EnsureInit(const RefPtr<MediaDataEncoder>& aEncoder) { bool succeeded; media::Await( GetMediaThreadPool(MediaThreadType::SUPERVISOR), aEncoder->Init(), - [&succeeded](TrackInfo::TrackType t) { - EXPECT_EQ(TrackInfo::TrackType::kVideoTrack, t); - succeeded = true; - }, + [&succeeded](bool) { succeeded = true; }, [&succeeded](const MediaResult& r) { succeeded = false; }); return succeeded; } @@ -246,9 +234,8 @@ TEST_F(MediaDataEncoderTest, H264Inits) { RUN_IF_SUPPORTED(CodecType::H264, []() { // w/o codec specific: should fail for h264. RefPtr<MediaDataEncoder> e = - CreateH264Encoder(MediaDataEncoder::Usage::Realtime, - MediaDataEncoder::PixelFormat::YUV420P, WIDTH, HEIGHT, - MediaDataEncoder::ScalabilityMode::None, Nothing()); + CreateH264Encoder(Usage::Realtime, dom::ImageBitmapFormat::YUV420P, + WIDTH, HEIGHT, ScalabilityMode::None, Nothing()); EXPECT_FALSE(e); // w/ codec specific @@ -319,9 +306,8 @@ TEST_F(MediaDataEncoderTest, H264Encodes) { WaitForShutdown(e); // Encode one frame and output in avcC format. - e = CreateH264Encoder(MediaDataEncoder::Usage::Record, - MediaDataEncoder::PixelFormat::YUV420P, WIDTH, HEIGHT, - MediaDataEncoder::ScalabilityMode::None, + e = CreateH264Encoder(Usage::Record, dom::ImageBitmapFormat::YUV420P, WIDTH, + HEIGHT, ScalabilityMode::None, Some(kH264SpecificAVCC)); EnsureInit(e); output = Encode(e, NUM_FRAMES, mData); @@ -349,22 +335,19 @@ TEST_F(MediaDataEncoderTest, H264Duration) { TEST_F(MediaDataEncoderTest, InvalidSize) { RUN_IF_SUPPORTED(CodecType::H264, []() { - RefPtr<MediaDataEncoder> e0x0 = CreateH264Encoder( - MediaDataEncoder::Usage::Realtime, - MediaDataEncoder::PixelFormat::YUV420P, 0, 0, - MediaDataEncoder::ScalabilityMode::None, Some(kH264SpecificAnnexB)); + RefPtr<MediaDataEncoder> e0x0 = + CreateH264Encoder(Usage::Realtime, dom::ImageBitmapFormat::YUV420P, 0, + 0, ScalabilityMode::None, Some(kH264SpecificAnnexB)); EXPECT_EQ(e0x0, nullptr); - RefPtr<MediaDataEncoder> e0x1 = CreateH264Encoder( - MediaDataEncoder::Usage::Realtime, - MediaDataEncoder::PixelFormat::YUV420P, 0, 1, - MediaDataEncoder::ScalabilityMode::None, Some(kH264SpecificAnnexB)); + RefPtr<MediaDataEncoder> e0x1 = + CreateH264Encoder(Usage::Realtime, dom::ImageBitmapFormat::YUV420P, 0, + 1, ScalabilityMode::None, Some(kH264SpecificAnnexB)); EXPECT_EQ(e0x1, nullptr); - RefPtr<MediaDataEncoder> e1x0 = CreateH264Encoder( - MediaDataEncoder::Usage::Realtime, - MediaDataEncoder::PixelFormat::YUV420P, 1, 0, - MediaDataEncoder::ScalabilityMode::None, Some(kH264SpecificAnnexB)); + RefPtr<MediaDataEncoder> e1x0 = + CreateH264Encoder(Usage::Realtime, dom::ImageBitmapFormat::YUV420P, 1, + 0, ScalabilityMode::None, Some(kH264SpecificAnnexB)); EXPECT_EQ(e1x0, nullptr); }); } @@ -372,10 +355,9 @@ TEST_F(MediaDataEncoderTest, InvalidSize) { #ifdef MOZ_WIDGET_ANDROID TEST_F(MediaDataEncoderTest, AndroidNotSupportedSize) { RUN_IF_SUPPORTED(CodecType::H264, []() { - RefPtr<MediaDataEncoder> e = CreateH264Encoder( - MediaDataEncoder::Usage::Realtime, - MediaDataEncoder::PixelFormat::YUV420P, 1, 1, - MediaDataEncoder::ScalabilityMode::None, Some(kH264SpecificAnnexB)); + RefPtr<MediaDataEncoder> e = + CreateH264Encoder(Usage::Realtime, dom::ImageBitmapFormat::YUV420P, 1, + 1, ScalabilityMode::None, Some(kH264SpecificAnnexB)); EXPECT_NE(e, nullptr); EXPECT_FALSE(EnsureInit(e)); }); @@ -387,9 +369,8 @@ TEST_F(MediaDataEncoderTest, H264AVCC) { RUN_IF_SUPPORTED(CodecType::H264, [this]() { // Encod frames in avcC format. RefPtr<MediaDataEncoder> e = CreateH264Encoder( - MediaDataEncoder::Usage::Record, MediaDataEncoder::PixelFormat::YUV420P, - WIDTH, HEIGHT, MediaDataEncoder::ScalabilityMode::None, - Some(kH264SpecificAVCC)); + Usage::Record, dom::ImageBitmapFormat::YUV420P, WIDTH, HEIGHT, + ScalabilityMode::None, Some(kH264SpecificAVCC)); EnsureInit(e); MediaDataEncoder::EncodedData output = Encode(e, NUM_FRAMES, mData); EXPECT_EQ(output.Length(), NUM_FRAMES); @@ -412,24 +393,20 @@ TEST_F(MediaDataEncoderTest, H264AVCC) { #endif static already_AddRefed<MediaDataEncoder> CreateVP8Encoder( - MediaDataEncoder::Usage aUsage = MediaDataEncoder::Usage::Realtime, - MediaDataEncoder::PixelFormat aPixelFormat = - MediaDataEncoder::PixelFormat::YUV420P, + Usage aUsage = Usage::Realtime, + dom::ImageBitmapFormat aPixelFormat = dom::ImageBitmapFormat::YUV420P, int32_t aWidth = WIDTH, int32_t aHeight = HEIGHT, - MediaDataEncoder::ScalabilityMode aScalabilityMode = - MediaDataEncoder::ScalabilityMode::None, + ScalabilityMode aScalabilityMode = ScalabilityMode::None, const Maybe<VP8Specific>& aSpecific = Some(VP8Specific())) { return CreateVideoEncoder(CodecType::VP8, aUsage, aPixelFormat, aWidth, aHeight, aScalabilityMode, aSpecific); } static already_AddRefed<MediaDataEncoder> CreateVP9Encoder( - MediaDataEncoder::Usage aUsage = MediaDataEncoder::Usage::Realtime, - MediaDataEncoder::PixelFormat aPixelFormat = - MediaDataEncoder::PixelFormat::YUV420P, + Usage aUsage = Usage::Realtime, + dom::ImageBitmapFormat aPixelFormat = dom::ImageBitmapFormat::YUV420P, int32_t aWidth = WIDTH, int32_t aHeight = HEIGHT, - MediaDataEncoder::ScalabilityMode aScalabilityMode = - MediaDataEncoder::ScalabilityMode::None, + ScalabilityMode aScalabilityMode = ScalabilityMode::None, const Maybe<VP9Specific>& aSpecific = Some(VP9Specific())) { return CreateVideoEncoder(CodecType::VP9, aUsage, aPixelFormat, aWidth, aHeight, aScalabilityMode, aSpecific); @@ -447,9 +424,8 @@ TEST_F(MediaDataEncoderTest, VP8Inits) { RUN_IF_SUPPORTED(CodecType::VP8, []() { // w/o codec specific. RefPtr<MediaDataEncoder> e = - CreateVP8Encoder(MediaDataEncoder::Usage::Realtime, - MediaDataEncoder::PixelFormat::YUV420P, WIDTH, HEIGHT, - MediaDataEncoder::ScalabilityMode::None, Nothing()); + CreateVP8Encoder(Usage::Realtime, dom::ImageBitmapFormat::YUV420P, + WIDTH, HEIGHT, ScalabilityMode::None, Nothing()); EXPECT_TRUE(EnsureInit(e)); WaitForShutdown(e); @@ -551,10 +527,9 @@ TEST_F(MediaDataEncoderTest, VP8EncodeWithScalabilityModeL1T2) { false, /* mAutoResize */ false /* mFrameDropping */ ); - RefPtr<MediaDataEncoder> e = CreateVP8Encoder( - MediaDataEncoder::Usage::Realtime, - MediaDataEncoder::PixelFormat::YUV420P, WIDTH, HEIGHT, - MediaDataEncoder::ScalabilityMode::L1T2, Some(specific)); + RefPtr<MediaDataEncoder> e = + CreateVP8Encoder(Usage::Realtime, dom::ImageBitmapFormat::YUV420P, + WIDTH, HEIGHT, ScalabilityMode::L1T2, Some(specific)); EnsureInit(e); const nsTArray<uint8_t> pattern({0, 1}); @@ -580,10 +555,9 @@ TEST_F(MediaDataEncoderTest, VP8EncodeWithScalabilityModeL1T3) { false, /* mAutoResize */ false /* mFrameDropping */ ); - RefPtr<MediaDataEncoder> e = CreateVP8Encoder( - MediaDataEncoder::Usage::Realtime, - MediaDataEncoder::PixelFormat::YUV420P, WIDTH, HEIGHT, - MediaDataEncoder::ScalabilityMode::L1T3, Some(specific)); + RefPtr<MediaDataEncoder> e = + CreateVP8Encoder(Usage::Realtime, dom::ImageBitmapFormat::YUV420P, + WIDTH, HEIGHT, ScalabilityMode::L1T3, Some(specific)); EnsureInit(e); const nsTArray<uint8_t> pattern({0, 2, 1, 2}); @@ -613,9 +587,8 @@ TEST_F(MediaDataEncoderTest, VP9Inits) { RUN_IF_SUPPORTED(CodecType::VP9, []() { // w/o codec specific. RefPtr<MediaDataEncoder> e = - CreateVP9Encoder(MediaDataEncoder::Usage::Realtime, - MediaDataEncoder::PixelFormat::YUV420P, WIDTH, HEIGHT, - MediaDataEncoder::ScalabilityMode::None, Nothing()); + CreateVP9Encoder(Usage::Realtime, dom::ImageBitmapFormat::YUV420P, + WIDTH, HEIGHT, ScalabilityMode::None, Nothing()); EXPECT_TRUE(EnsureInit(e)); WaitForShutdown(e); @@ -719,10 +692,9 @@ TEST_F(MediaDataEncoderTest, VP9EncodeWithScalabilityModeL1T2) { false /* mFlexible */ ); - RefPtr<MediaDataEncoder> e = CreateVP9Encoder( - MediaDataEncoder::Usage::Realtime, - MediaDataEncoder::PixelFormat::YUV420P, WIDTH, HEIGHT, - MediaDataEncoder::ScalabilityMode::L1T2, Some(specific)); + RefPtr<MediaDataEncoder> e = + CreateVP9Encoder(Usage::Realtime, dom::ImageBitmapFormat::YUV420P, + WIDTH, HEIGHT, ScalabilityMode::L1T2, Some(specific)); EnsureInit(e); const nsTArray<uint8_t> pattern({0, 1}); @@ -751,10 +723,9 @@ TEST_F(MediaDataEncoderTest, VP9EncodeWithScalabilityModeL1T3) { false /* mFlexible */ ); - RefPtr<MediaDataEncoder> e = CreateVP9Encoder( - MediaDataEncoder::Usage::Realtime, - MediaDataEncoder::PixelFormat::YUV420P, WIDTH, HEIGHT, - MediaDataEncoder::ScalabilityMode::L1T3, Some(specific)); + RefPtr<MediaDataEncoder> e = + CreateVP9Encoder(Usage::Realtime, dom::ImageBitmapFormat::YUV420P, + WIDTH, HEIGHT, ScalabilityMode::L1T3, Some(specific)); EnsureInit(e); const nsTArray<uint8_t> pattern({0, 2, 1, 2}); diff --git a/dom/media/hls/HLSDecoder.cpp b/dom/media/hls/HLSDecoder.cpp index 99bf4c0ff6..dbf2339bef 100644 --- a/dom/media/hls/HLSDecoder.cpp +++ b/dom/media/hls/HLSDecoder.cpp @@ -18,9 +18,11 @@ #include "mozilla/java/GeckoHLSResourceWrapperNatives.h" #include "nsContentUtils.h" #include "nsIChannel.h" +#include "nsIURL.h" #include "nsNetUtil.h" #include "nsThreadUtils.h" #include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/glean/GleanMetrics.h" #include "mozilla/NullPrincipal.h" #include "mozilla/StaticPrefs_media.h" @@ -169,7 +171,8 @@ nsresult HLSDecoder::Load(nsIChannel* aChannel) { mChannel = aChannel; nsCString spec; Unused << mURI->GetSpec(spec); - ; + mUsageRecorded = false; + HLSResourceCallbacksSupport::Init(); mJavaCallbacks = java::GeckoHLSResourceWrapper::Callbacks::New(); mCallbackSupport = new HLSResourceCallbacksSupport(this); @@ -253,13 +256,36 @@ void HLSDecoder::NotifyDataArrived() { void HLSDecoder::NotifyLoad(nsCString aMediaUrl) { MOZ_ASSERT(NS_IsMainThread()); MOZ_DIAGNOSTIC_ASSERT(!IsShutdown()); - UpdateCurrentPrincipal(aMediaUrl); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aMediaUrl.Data()); + NS_ENSURE_SUCCESS_VOID(rv); + + RecordMediaUsage(uri); + UpdateCurrentPrincipal(uri); +} + +void HLSDecoder::RecordMediaUsage(nsIURI* aMediaUri) { + if (mUsageRecorded) { + return; + } + + nsresult rv; + nsCOMPtr<nsIURL> url = do_QueryInterface(aMediaUri, &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + // TODO: get hostname. See bug 1887053. + nsAutoCString mediaExt; + Unused << url->GetFileExtension(mediaExt); + glean::hls::MediaLoadExtra extra = {.mediaExtension = Some(mediaExt.get())}; + glean::hls::media_load.Record(Some(extra)); + mUsageRecorded = true; } // Should be called when the decoder loads media from a URL to ensure the // principal of the media element is appropriately set for CORS. -void HLSDecoder::UpdateCurrentPrincipal(nsCString aMediaUrl) { - nsCOMPtr<nsIPrincipal> principal = GetContentPrincipal(aMediaUrl); +void HLSDecoder::UpdateCurrentPrincipal(nsIURI* aMediaUri) { + nsCOMPtr<nsIPrincipal> principal = GetContentPrincipal(aMediaUri); MOZ_DIAGNOSTIC_ASSERT(principal); // Check the subsumption of old and new principals. Should be either @@ -280,12 +306,8 @@ void HLSDecoder::UpdateCurrentPrincipal(nsCString aMediaUrl) { } already_AddRefed<nsIPrincipal> HLSDecoder::GetContentPrincipal( - nsCString aMediaUrl) { - nsCOMPtr<nsIURI> uri; - nsresult rv = NS_NewURI(getter_AddRefs(uri), aMediaUrl.Data()); - NS_ENSURE_SUCCESS(rv, nullptr); + nsIURI* aMediaUri) { RefPtr<dom::HTMLMediaElement> element = GetOwner()->GetMediaElement(); - NS_ENSURE_SUCCESS(rv, nullptr); nsSecurityFlags securityFlags = element->ShouldCheckAllowOrigin() ? nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT @@ -294,9 +316,9 @@ already_AddRefed<nsIPrincipal> HLSDecoder::GetContentPrincipal( securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; } nsCOMPtr<nsIChannel> channel; - rv = NS_NewChannel(getter_AddRefs(channel), uri, - static_cast<dom::Element*>(element), securityFlags, - nsIContentPolicy::TYPE_INTERNAL_VIDEO); + nsresult rv = NS_NewChannel( + getter_AddRefs(channel), aMediaUri, static_cast<dom::Element*>(element), + securityFlags, nsIContentPolicy::TYPE_INTERNAL_VIDEO); NS_ENSURE_SUCCESS(rv, nullptr); nsCOMPtr<nsIPrincipal> principal; nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); diff --git a/dom/media/hls/HLSDecoder.h b/dom/media/hls/HLSDecoder.h index 0f65457765..3624a8c3f4 100644 --- a/dom/media/hls/HLSDecoder.h +++ b/dom/media/hls/HLSDecoder.h @@ -47,6 +47,8 @@ class HLSDecoder final : public MediaDecoder { // Called when Exoplayer start to load media. Main thread only. void NotifyLoad(nsCString aMediaUrl); + bool IsHLSDecoder() const override { return true; } + private: friend class HLSResourceCallbacksSupport; @@ -61,8 +63,9 @@ class HLSDecoder final : public MediaDecoder { return true; } - void UpdateCurrentPrincipal(nsCString aMediaUrl); - already_AddRefed<nsIPrincipal> GetContentPrincipal(nsCString aMediaUrl); + void UpdateCurrentPrincipal(nsIURI* aMediaUri); + already_AddRefed<nsIPrincipal> GetContentPrincipal(nsIURI* aMediaUri); + void RecordMediaUsage(nsIURI* aMediaUri); static size_t sAllocatedInstances; // Access only in the main thread. @@ -72,6 +75,9 @@ class HLSDecoder final : public MediaDecoder { java::GeckoHLSResourceWrapper::Callbacks::GlobalRef mJavaCallbacks; RefPtr<HLSResourceCallbacksSupport> mCallbackSupport; nsCOMPtr<nsIPrincipal> mContentPrincipal; + // There can be multiple media files loaded for one HLS content. Use this flag + // to ensure we only record once per content. + bool mUsageRecorded; }; } // namespace mozilla diff --git a/dom/media/hls/metrics.yaml b/dom/media/hls/metrics.yaml new file mode 100644 index 0000000000..ea27b358ff --- /dev/null +++ b/dom/media/hls/metrics.yaml @@ -0,0 +1,70 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Core :: Audio/Video' + +hls: + canplay_requested: + type: counter + description: > + Record when a page requests canPlayType for a HLS media type. + metadata: + tags: + - 'Core :: Audio/Video: Playback' + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1672751 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1672751#8 + data_sensitivity: + - technical + notification_emails: + - media-alerts@mozilla.com + expires: 132 + + canplay_supported: + type: counter + description: > + Record when a canPlayType request supports HLS. + metadata: + tags: + - 'Core :: Audio/Video: Playback' + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1672751 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1672751#8 + data_sensitivity: + - technical + notification_emails: + - media-alerts@mozilla.com + expires: 132 + + media_load: + type: event + description: > + Record the information about the HLS playback on Android using ExoPlayer. + The value of this event contains the media format. + metadata: + tags: + - 'Core :: Audio/Video: Playback' + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1672751 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1672751#8 + data_sensitivity: + - technical + notification_emails: + - media-alerts@mozilla.com + extra_keys: + media_extension: + description: > + The extension in the media file name, could be 'ts' (for MPEG-TS), 'mp4', + 'aac', 'mp3', ... + type: string + expires: 132 diff --git a/dom/media/ipc/MFCDMChild.cpp b/dom/media/ipc/MFCDMChild.cpp index aedae5bc36..2ba2bdaf4e 100644 --- a/dom/media/ipc/MFCDMChild.cpp +++ b/dom/media/ipc/MFCDMChild.cpp @@ -148,7 +148,7 @@ void MFCDMChild::Shutdown() { } RefPtr<MFCDMChild::CapabilitiesPromise> MFCDMChild::GetCapabilities( - bool aIsHWSecured) { + MFCDMCapabilitiesRequest&& aRequest) { MOZ_ASSERT(mManagerThread); if (mShutdown) { @@ -160,23 +160,21 @@ RefPtr<MFCDMChild::CapabilitiesPromise> MFCDMChild::GetCapabilities( return CapabilitiesPromise::CreateAndReject(mState, __func__); } - auto doSend = [self = RefPtr{this}, aIsHWSecured, this]() { - SendGetCapabilities(aIsHWSecured) - ->Then( - mManagerThread, __func__, - [self, this](MFCDMCapabilitiesResult&& aResult) { - if (aResult.type() == MFCDMCapabilitiesResult::Tnsresult) { - mCapabilitiesPromiseHolder.RejectIfExists( - aResult.get_nsresult(), __func__); - return; - } - mCapabilitiesPromiseHolder.ResolveIfExists( - std::move(aResult.get_MFCDMCapabilitiesIPDL()), __func__); - }, - [self, this](const mozilla::ipc::ResponseRejectReason& aReason) { - mCapabilitiesPromiseHolder.RejectIfExists(NS_ERROR_FAILURE, - __func__); - }); + auto doSend = [self = RefPtr{this}, request = std::move(aRequest), this]() { + SendGetCapabilities(request)->Then( + mManagerThread, __func__, + [self, this](MFCDMCapabilitiesResult&& aResult) { + if (aResult.type() == MFCDMCapabilitiesResult::Tnsresult) { + mCapabilitiesPromiseHolder.RejectIfExists(aResult.get_nsresult(), + __func__); + return; + } + mCapabilitiesPromiseHolder.ResolveIfExists( + std::move(aResult.get_MFCDMCapabilitiesIPDL()), __func__); + }, + [self, this](const mozilla::ipc::ResponseRejectReason& aReason) { + mCapabilitiesPromiseHolder.RejectIfExists(NS_ERROR_FAILURE, __func__); + }); }; return InvokeAsync(doSend, __func__, mCapabilitiesPromiseHolder); diff --git a/dom/media/ipc/MFCDMChild.h b/dom/media/ipc/MFCDMChild.h index e62f2b7184..3396b0c790 100644 --- a/dom/media/ipc/MFCDMChild.h +++ b/dom/media/ipc/MFCDMChild.h @@ -25,7 +25,8 @@ class MFCDMChild final : public PMFCDMChild { explicit MFCDMChild(const nsAString& aKeySystem); using CapabilitiesPromise = MozPromise<MFCDMCapabilitiesIPDL, nsresult, true>; - RefPtr<CapabilitiesPromise> GetCapabilities(bool aIsHWSecured); + RefPtr<CapabilitiesPromise> GetCapabilities( + MFCDMCapabilitiesRequest&& aRequest); template <typename PromiseType> already_AddRefed<PromiseType> InvokeAsync( diff --git a/dom/media/ipc/MFCDMParent.cpp b/dom/media/ipc/MFCDMParent.cpp index 2e91048b88..4570fbe838 100644 --- a/dom/media/ipc/MFCDMParent.cpp +++ b/dom/media/ipc/MFCDMParent.cpp @@ -5,6 +5,7 @@ #include "MFCDMParent.h" #include <mfmediaengine.h> +#include <unknwnbase.h> #include <wtypes.h> #define INITGUID // Enable DEFINE_PROPERTYKEY() #include <propkeydef.h> // For DEFINE_PROPERTYKEY() definition @@ -92,6 +93,8 @@ StaticMutex sFactoryMutex; static nsTHashMap<nsStringHashKey, ComPtr<IMFContentDecryptionModuleFactory>> sFactoryMap; static CopyableTArray<MFCDMCapabilitiesIPDL> sCapabilities; +StaticMutex sCapabilitesMutex; +static ComPtr<IUnknown> sMediaEngineClassFactory; // RAIIized PROPVARIANT. See // third_party/libwebrtc/modules/audio_device/win/core_audio_utility_win.h @@ -166,6 +169,11 @@ static nsString GetHdcpPolicy(const dom::HDCPVersion& aMinHdcpVersion) { return nsString(u"hdcp=1"); } +static bool RequireClearLead(const nsString& aKeySystem) { + return aKeySystem.EqualsLiteral(kWidevineExperiment2KeySystemName) || + aKeySystem.EqualsLiteral(kPlayReadyHardwareClearLeadKeySystemName); +} + static void BuildCapabilitiesArray( const nsTArray<MFCDMMediaCapability>& aCapabilities, AutoPropVar& capabilitiesPropOut) { @@ -464,8 +472,10 @@ LPCWSTR MFCDMParent::GetCDMLibraryName(const nsString& aKeySystem) { /* static */ void MFCDMParent::Shutdown() { + StaticMutexAutoLock lock(sCapabilitesMutex); sFactoryMap.Clear(); sCapabilities.Clear(); + sMediaEngineClassFactory.Reset(); } /* static */ @@ -500,10 +510,13 @@ HRESULT MFCDMParent::LoadFactory( NS_ConvertUTF16toUTF8(aKeySystem).get()); ComPtr<IMFContentDecryptionModuleFactory> cdmFactory; if (loadFromPlatform) { + if (!sMediaEngineClassFactory) { + MFCDM_RETURN_IF_FAILED(CoCreateInstance( + CLSID_MFMediaEngineClassFactory, nullptr, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&sMediaEngineClassFactory))); + } ComPtr<IMFMediaEngineClassFactory4> clsFactory; - MFCDM_RETURN_IF_FAILED(CoCreateInstance(CLSID_MFMediaEngineClassFactory, - nullptr, CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(&clsFactory))); + MFCDM_RETURN_IF_FAILED(sMediaEngineClassFactory.As(&clsFactory)); MFCDM_RETURN_IF_FAILED(clsFactory->CreateContentDecryptionModuleFactory( MapKeySystem(aKeySystem).get(), IID_PPV_ARGS(&cdmFactory))); aFactoryOut.Swap(cdmFactory); @@ -617,12 +630,8 @@ static bool FactorySupports(ComPtr<IMFContentDecryptionModuleFactory>& aFactory, // use another way to check the capabilities. if (IsPlayReadyKeySystemAndSupported(aKeySystem) && StaticPrefs::media_eme_playready_istypesupportedex()) { - ComPtr<IMFMediaEngineClassFactory> spFactory; ComPtr<IMFExtendedDRMTypeSupport> spDrmTypeSupport; - MFCDM_RETURN_BOOL_IF_FAILED( - CoCreateInstance(CLSID_MFMediaEngineClassFactory, NULL, - CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&spFactory))); - MFCDM_RETURN_BOOL_IF_FAILED(spFactory.As(&spDrmTypeSupport)); + MFCDM_RETURN_BOOL_IF_FAILED(sMediaEngineClassFactory.As(&spDrmTypeSupport)); BSTR keySystem = aIsHWSecure ? CreateBSTRFromConstChar(kPlayReadyKeySystemHardware) : CreateBSTRFromConstChar(kPlayReadyKeySystemName); @@ -699,46 +708,55 @@ MFCDMParent::GetAllKeySystemsCapabilities() { new CapabilitiesPromise::Private(__func__); Unused << backgroundTaskQueue->Dispatch(NS_NewRunnableFunction(__func__, [p] { MFCDM_PARENT_SLOG("GetAllKeySystemsCapabilities"); - if (sCapabilities.IsEmpty()) { - enum SecureLevel : bool { - Software = false, - Hardware = true, - }; - const nsTArray<std::pair<nsString, SecureLevel>> kKeySystems{ - std::pair<nsString, SecureLevel>( - NS_ConvertUTF8toUTF16(kPlayReadyKeySystemName), - SecureLevel::Software), - std::pair<nsString, SecureLevel>( - NS_ConvertUTF8toUTF16(kPlayReadyKeySystemHardware), - SecureLevel::Hardware), - std::pair<nsString, SecureLevel>( - NS_ConvertUTF8toUTF16(kPlayReadyHardwareClearLeadKeySystemName), - SecureLevel::Hardware), - std::pair<nsString, SecureLevel>( - NS_ConvertUTF8toUTF16(kWidevineExperimentKeySystemName), - SecureLevel::Hardware), - std::pair<nsString, SecureLevel>( - NS_ConvertUTF8toUTF16(kWidevineExperiment2KeySystemName), - SecureLevel::Hardware), - }; - for (const auto& keySystem : kKeySystems) { - // Only check the capabilites if the relative prefs for the key system - // are ON. - if (IsPlayReadyKeySystemAndSupported(keySystem.first) || - IsWidevineExperimentKeySystemAndSupported(keySystem.first)) { - MFCDMCapabilitiesIPDL* c = sCapabilities.AppendElement(); - GetCapabilities(keySystem.first, keySystem.second, nullptr, *c); + enum SecureLevel : bool { + Software = false, + Hardware = true, + }; + const nsTArray<std::pair<nsString, SecureLevel>> kKeySystems{ + std::pair<nsString, SecureLevel>( + NS_ConvertUTF8toUTF16(kPlayReadyKeySystemName), + SecureLevel::Software), + std::pair<nsString, SecureLevel>( + NS_ConvertUTF8toUTF16(kPlayReadyKeySystemHardware), + SecureLevel::Hardware), + std::pair<nsString, SecureLevel>( + NS_ConvertUTF8toUTF16(kPlayReadyHardwareClearLeadKeySystemName), + SecureLevel::Hardware), + std::pair<nsString, SecureLevel>( + NS_ConvertUTF8toUTF16(kWidevineExperimentKeySystemName), + SecureLevel::Hardware), + std::pair<nsString, SecureLevel>( + NS_ConvertUTF8toUTF16(kWidevineExperiment2KeySystemName), + SecureLevel::Hardware), + }; + + CopyableTArray<MFCDMCapabilitiesIPDL> capabilitiesArr; + for (const auto& keySystem : kKeySystems) { + // Only check the capabilites if the relative prefs for the key system + // are ON. + if (IsPlayReadyKeySystemAndSupported(keySystem.first) || + IsWidevineExperimentKeySystemAndSupported(keySystem.first)) { + MFCDMCapabilitiesIPDL* c = capabilitiesArr.AppendElement(); + CapabilitesFlagSet flags; + if (keySystem.second == SecureLevel::Hardware) { + flags += CapabilitesFlag::HarewareDecryption; + } + flags += CapabilitesFlag::NeedHDCPCheck; + if (RequireClearLead(keySystem.first)) { + flags += CapabilitesFlag::NeedClearLeadCheck; } + GetCapabilities(keySystem.first, flags, nullptr, *c); } } - p->Resolve(sCapabilities, __func__); + + p->Resolve(std::move(capabilitiesArr), __func__); })); return p; } /* static */ void MFCDMParent::GetCapabilities(const nsString& aKeySystem, - const bool aIsHWSecure, + const CapabilitesFlagSet& aFlags, IMFContentDecryptionModuleFactory* aFactory, MFCDMCapabilitiesIPDL& aCapabilitiesOut) { aCapabilitiesOut.keySystem() = aKeySystem; @@ -747,9 +765,12 @@ void MFCDMParent::GetCapabilities(const nsString& aKeySystem, aCapabilitiesOut.persistentState() = KeySystemConfig::Requirement::Required; aCapabilitiesOut.distinctiveID() = KeySystemConfig::Requirement::Required; + const bool isHardwareDecryption = + aFlags.contains(CapabilitesFlag::HarewareDecryption); + aCapabilitiesOut.isHardwareDecryption() = isHardwareDecryption; // Return empty capabilites for SWDRM on Windows 10 because it has the process // leaking problem. - if (!IsWin11OrLater() && !aIsHWSecure) { + if (!IsWin11OrLater() && !isHardwareDecryption) { return; } @@ -758,6 +779,30 @@ void MFCDMParent::GetCapabilities(const nsString& aKeySystem, RETURN_VOID_IF_FAILED(GetOrCreateFactory(aKeySystem, factory)); } + StaticMutexAutoLock lock(sCapabilitesMutex); + for (auto& capabilities : sCapabilities) { + if (capabilities.keySystem().Equals(aKeySystem) && + capabilities.isHardwareDecryption() == isHardwareDecryption) { + MFCDM_PARENT_SLOG( + "Return cached capabilities for %s (hardwareDecryption=%d)", + NS_ConvertUTF16toUTF8(aKeySystem).get(), isHardwareDecryption); + if (capabilities.isHDCP22Compatible().isNothing() && + aFlags.contains(CapabilitesFlag::NeedHDCPCheck)) { + const bool rv = IsHDCPVersionSupported(factory, aKeySystem, + dom::HDCPVersion::_2_2) == NS_OK; + MFCDM_PARENT_SLOG( + "Check HDCP 2.2 compatible (%d) for the cached capabilites", rv); + capabilities.isHDCP22Compatible() = Some(rv); + } + aCapabilitiesOut = capabilities; + return; + } + } + + MFCDM_PARENT_SLOG( + "Query capabilities for %s from the factory (hardwareDecryption=%d)", + NS_ConvertUTF16toUTF8(aKeySystem).get(), isHardwareDecryption); + // Widevine requires codec type to be four CC, PlayReady is fine with both. static auto convertCodecToFourCC = [](const KeySystemConfig::EMECodecString& aCodec) { @@ -809,12 +854,12 @@ void MFCDMParent::GetCapabilities(const nsString& aKeySystem, } if (FactorySupports(factory, aKeySystem, convertCodecToFourCC(codec), KeySystemConfig::EMECodecString(""), nsString(u""), - aIsHWSecure)) { + isHardwareDecryption)) { MFCDMMediaCapability* c = aCapabilitiesOut.videoCapabilities().AppendElement(); c->contentType() = NS_ConvertUTF8toUTF16(codec); c->robustness() = - GetRobustnessStringForKeySystem(aKeySystem, aIsHWSecure); + GetRobustnessStringForKeySystem(aKeySystem, isHardwareDecryption); MFCDM_PARENT_SLOG("%s: +video:%s", __func__, codec.get()); supportedVideoCodecs.AppendElement(codec); } @@ -831,52 +876,51 @@ void MFCDMParent::GetCapabilities(const nsString& aKeySystem, KeySystemConfig::EME_CODEC_VORBIS, }); for (const auto& codec : kAudioCodecs) { - if (FactorySupports( - factory, aKeySystem, convertCodecToFourCC(supportedVideoCodecs[0]), - convertCodecToFourCC(codec), nsString(u""), aIsHWSecure)) { + // Hardware decryption is usually only used for video, so we can just check + // the software capabilities for audio in order to save some time. As the + // media foundation would create a new D3D device everytime when we check + // hardware decryption, which takes way longer time. + if (FactorySupports(factory, aKeySystem, + convertCodecToFourCC(supportedVideoCodecs[0]), + convertCodecToFourCC(codec), nsString(u""), + false /* aIsHWSecure */)) { MFCDMMediaCapability* c = aCapabilitiesOut.audioCapabilities().AppendElement(); c->contentType() = NS_ConvertUTF8toUTF16(codec); - c->robustness() = GetRobustnessStringForKeySystem(aKeySystem, aIsHWSecure, - false /* isVideo */); + c->robustness() = GetRobustnessStringForKeySystem( + aKeySystem, false /* aIsHWSecure */, false /* isVideo */); MFCDM_PARENT_SLOG("%s: +audio:%s", __func__, codec.get()); } } - // Collect schemes supported by all video codecs. - static nsTArray<std::pair<CryptoScheme, nsDependentString>> kSchemes = { - std::pair<CryptoScheme, nsDependentString>( - CryptoScheme::Cenc, u"encryption-type=cenc,encryption-iv-size=8,"), - std::pair<CryptoScheme, nsDependentString>( - CryptoScheme::Cbcs, u"encryption-type=cbcs,encryption-iv-size=16,")}; - for (auto& scheme : kSchemes) { - bool ok = true; - for (auto& codec : supportedVideoCodecs) { - ok &= FactorySupports( - factory, aKeySystem, convertCodecToFourCC(codec), nsCString(""), - scheme.second /* additional feature */, aIsHWSecure); - if (!ok) { - break; - } - } - if (ok) { - aCapabilitiesOut.encryptionSchemes().AppendElement(scheme.first); - MFCDM_PARENT_SLOG("%s: +scheme:%s", __func__, - scheme.first == CryptoScheme::Cenc ? "cenc" : "cbcs"); - } + // 'If value is unspecified, default value of "cenc" is used.' See + // https://learn.microsoft.com/en-us/windows/win32/api/mfmediaengine/nf-mfmediaengine-imfextendeddrmtypesupport-istypesupportedex + if (!supportedVideoCodecs.IsEmpty()) { + aCapabilitiesOut.encryptionSchemes().AppendElement(CryptoScheme::Cenc); + MFCDM_PARENT_SLOG("%s: +scheme:cenc", __func__); } - static auto RequireClearLead = [](const nsString& aKeySystem) { - if (aKeySystem.EqualsLiteral(kWidevineExperiment2KeySystemName) || - aKeySystem.EqualsLiteral(kPlayReadyHardwareClearLeadKeySystemName)) { - return true; + // Check another scheme "cbcs" + static std::pair<CryptoScheme, nsDependentString> kCbcs = + std::pair<CryptoScheme, nsDependentString>( + CryptoScheme::Cbcs, u"encryption-type=cbcs,encryption-iv-size=16,"); + bool ok = true; + for (const auto& codec : supportedVideoCodecs) { + ok &= FactorySupports(factory, aKeySystem, convertCodecToFourCC(codec), + nsCString(""), kCbcs.second /* additional feature */, + isHardwareDecryption); + if (!ok) { + break; } - return false; - }; + } + if (ok) { + aCapabilitiesOut.encryptionSchemes().AppendElement(kCbcs.first); + MFCDM_PARENT_SLOG("%s: +scheme:cbcs", __func__); + } // For key system requires clearlead, every codec needs to have clear support. // If not, then we will remove the codec from supported codec. - if (RequireClearLead(aKeySystem)) { + if (aFlags.contains(CapabilitesFlag::NeedClearLeadCheck)) { for (const auto& scheme : aCapabilitiesOut.encryptionSchemes()) { nsTArray<KeySystemConfig::EMECodecString> noClearLeadCodecs; for (const auto& codec : supportedVideoCodecs) { @@ -894,9 +938,9 @@ void MFCDMParent::GetCapabilities(const nsString& aKeySystem, } else { additionalFeature.AppendLiteral(u"cbcs-clearlead,"); } - bool rv = - FactorySupports(factory, aKeySystem, convertCodecToFourCC(codec), - nsCString(""), additionalFeature, aIsHWSecure); + bool rv = FactorySupports(factory, aKeySystem, + convertCodecToFourCC(codec), nsCString(""), + additionalFeature, isHardwareDecryption); MFCDM_PARENT_SLOG("clearlead %s IV 8 bytes %s %s", CryptoSchemeToString(scheme), codec.get(), rv ? "supported" : "not supported"); @@ -906,7 +950,8 @@ void MFCDMParent::GetCapabilities(const nsString& aKeySystem, // Try 16 bytes IV. additionalFeature.AppendLiteral(u"encryption-iv-size=16,"); rv = FactorySupports(factory, aKeySystem, convertCodecToFourCC(codec), - nsCString(""), additionalFeature, aIsHWSecure); + nsCString(""), additionalFeature, + isHardwareDecryption); MFCDM_PARENT_SLOG("clearlead %s IV 16 bytes %s %s", CryptoSchemeToString(scheme), codec.get(), rv ? "supported" : "not supported"); @@ -926,9 +971,14 @@ void MFCDMParent::GetCapabilities(const nsString& aKeySystem, } } - if (IsHDCPVersionSupported(factory, aKeySystem, dom::HDCPVersion::_2_2) == - NS_OK) { - aCapabilitiesOut.isHDCP22Compatible() = true; + // Only perform HDCP if necessary, "The hdcp query (item 4) has a + // computationally expensive first invocation cost". See + // https://learn.microsoft.com/en-us/windows/win32/api/mfmediaengine/nf-mfmediaengine-imfextendeddrmtypesupport-istypesupportedex + if (aFlags.contains(CapabilitesFlag::NeedHDCPCheck) && + IsHDCPVersionSupported(factory, aKeySystem, dom::HDCPVersion::_2_2) == + NS_OK) { + MFCDM_PARENT_SLOG("Capabilites is compatible with HDCP 2.2"); + aCapabilitiesOut.isHDCP22Compatible() = Some(true); } // TODO: don't hardcode @@ -938,13 +988,24 @@ void MFCDMParent::GetCapabilities(const nsString& aKeySystem, KeySystemConfig::SessionType::Temporary); aCapabilitiesOut.sessionTypes().AppendElement( KeySystemConfig::SessionType::PersistentLicense); + + // Cache capabilities for reuse. + sCapabilities.AppendElement(aCapabilitiesOut); } mozilla::ipc::IPCResult MFCDMParent::RecvGetCapabilities( - const bool aIsHWSecure, GetCapabilitiesResolver&& aResolver) { + const MFCDMCapabilitiesRequest& aRequest, + GetCapabilitiesResolver&& aResolver) { MFCDM_REJECT_IF(!mFactory, NS_ERROR_DOM_NOT_SUPPORTED_ERR); MFCDMCapabilitiesIPDL capabilities; - GetCapabilities(mKeySystem, aIsHWSecure, mFactory.Get(), capabilities); + CapabilitesFlagSet flags; + if (aRequest.isHardwareDecryption()) { + flags += CapabilitesFlag::HarewareDecryption; + } + if (RequireClearLead(aRequest.keySystem())) { + flags += CapabilitesFlag::NeedClearLeadCheck; + } + GetCapabilities(aRequest.keySystem(), flags, mFactory.Get(), capabilities); aResolver(std::move(capabilities)); return IPC_OK(); } diff --git a/dom/media/ipc/MFCDMParent.h b/dom/media/ipc/MFCDMParent.h index b4ef1b831b..921d86be73 100644 --- a/dom/media/ipc/MFCDMParent.h +++ b/dom/media/ipc/MFCDMParent.h @@ -52,7 +52,8 @@ class MFCDMParent final : public PMFCDMParent { uint64_t Id() const { return mId; } mozilla::ipc::IPCResult RecvGetCapabilities( - const bool aIsHWSecured, GetCapabilitiesResolver&& aResolver); + const MFCDMCapabilitiesRequest& aRequest, + GetCapabilitiesResolver&& aResolver); mozilla::ipc::IPCResult RecvInit(const MFCDMInitParamsIPDL& aParams, InitResolver&& aResolver); @@ -97,6 +98,13 @@ class MFCDMParent final : public PMFCDMParent { private: ~MFCDMParent(); + enum class CapabilitesFlag { + HarewareDecryption, + NeedHDCPCheck, + NeedClearLeadCheck, + }; + using CapabilitesFlagSet = EnumSet<CapabilitesFlag, uint8_t>; + static LPCWSTR GetCDMLibraryName(const nsString& aKeySystem); static HRESULT GetOrCreateFactory( @@ -108,7 +116,7 @@ class MFCDMParent final : public PMFCDMParent { Microsoft::WRL::ComPtr<IMFContentDecryptionModuleFactory>& aFactoryOut); static void GetCapabilities(const nsString& aKeySystem, - const bool aIsHWSecure, + const CapabilitesFlagSet& aFlags, IMFContentDecryptionModuleFactory* aFactory, MFCDMCapabilitiesIPDL& aCapabilitiesOut); diff --git a/dom/media/ipc/MFMediaEngineChild.cpp b/dom/media/ipc/MFMediaEngineChild.cpp index 02013056d5..cc32d15ea4 100644 --- a/dom/media/ipc/MFMediaEngineChild.cpp +++ b/dom/media/ipc/MFMediaEngineChild.cpp @@ -47,20 +47,22 @@ MFMediaEngineChild::MFMediaEngineChild(MFMediaEngineWrapper* aOwner, } RefPtr<GenericNonExclusivePromise> MFMediaEngineChild::Init( - bool aShouldPreload) { + const MediaInfo& aInfo, bool aShouldPreload) { if (!mManagerThread) { return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_FAILURE, __func__); } - CLOG("Init"); + CLOG("Init, hasAudio=%d, hasVideo=%d, encrypted=%d", aInfo.HasAudio(), + aInfo.HasVideo(), aInfo.IsEncrypted()); + MOZ_ASSERT(mMediaEngineId == 0); RefPtr<MFMediaEngineChild> self = this; RemoteDecoderManagerChild::LaunchUtilityProcessIfNeeded( RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM) ->Then( mManagerThread, __func__, - [self, this, aShouldPreload](bool) { + [self, this, aShouldPreload, info = aInfo](bool) { RefPtr<RemoteDecoderManagerChild> manager = RemoteDecoderManagerChild::GetSingleton( RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM); @@ -72,8 +74,13 @@ RefPtr<GenericNonExclusivePromise> MFMediaEngineChild::Init( mIPDLSelfRef = this; Unused << manager->SendPMFMediaEngineConstructor(this); - MediaEngineInfoIPDL info(aShouldPreload); - SendInitMediaEngine(info) + + MediaInfoIPDL mediaInfo( + info.HasAudio() ? Some(info.mAudio) : Nothing(), + info.HasVideo() ? Some(info.mVideo) : Nothing()); + + MediaEngineInfoIPDL initInfo(mediaInfo, aShouldPreload); + SendInitMediaEngine(initInfo) ->Then( mManagerThread, __func__, [self, this](uint64_t aId) { @@ -256,9 +263,9 @@ MFMediaEngineWrapper::MFMediaEngineWrapper(ExternalEngineStateMachine* aOwner, mCurrentTimeInSecond(0.0) {} RefPtr<GenericNonExclusivePromise> MFMediaEngineWrapper::Init( - bool aShouldPreload) { + const MediaInfo& aInfo, bool aShouldPreload) { WLOG("Init"); - return mEngine->Init(aShouldPreload); + return mEngine->Init(aInfo, aShouldPreload); } MFMediaEngineWrapper::~MFMediaEngineWrapper() { mEngine->OwnerDestroyed(); } @@ -335,18 +342,6 @@ void MFMediaEngineWrapper::NotifyEndOfStream(TrackInfo::TrackType aType) { [engine = mEngine, aType] { engine->SendNotifyEndOfStream(aType); })); } -void MFMediaEngineWrapper::SetMediaInfo(const MediaInfo& aInfo) { - WLOG("SetMediaInfo, hasAudio=%d, hasVideo=%d, encrypted=%d", aInfo.HasAudio(), - aInfo.HasVideo(), aInfo.IsEncrypted()); - MOZ_ASSERT(IsInited()); - Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction( - "MFMediaEngineWrapper::SetMediaInfo", [engine = mEngine, aInfo] { - MediaInfoIPDL info(aInfo.HasAudio() ? Some(aInfo.mAudio) : Nothing(), - aInfo.HasVideo() ? Some(aInfo.mVideo) : Nothing()); - engine->SendNotifyMediaInfo(info); - })); -} - bool MFMediaEngineWrapper::SetCDMProxy(CDMProxy* aProxy) { #ifdef MOZ_WMF_CDM WMFCDMProxy* proxy = aProxy->AsWMFCDMProxy(); diff --git a/dom/media/ipc/MFMediaEngineChild.h b/dom/media/ipc/MFMediaEngineChild.h index 92de3b9483..13b837b7d8 100644 --- a/dom/media/ipc/MFMediaEngineChild.h +++ b/dom/media/ipc/MFMediaEngineChild.h @@ -32,7 +32,8 @@ class MFMediaEngineChild final : public PMFMediaEngineChild { void OwnerDestroyed(); void IPDLActorDestroyed(); - RefPtr<GenericNonExclusivePromise> Init(bool aShouldPreload); + RefPtr<GenericNonExclusivePromise> Init(const MediaInfo& aInfo, + bool aShouldPreload); void Shutdown(); // Methods for PMFMediaEngineChild @@ -99,7 +100,8 @@ class MFMediaEngineWrapper final : public ExternalPlaybackEngine { ~MFMediaEngineWrapper(); // Methods for ExternalPlaybackEngine - RefPtr<GenericNonExclusivePromise> Init(bool aShouldPreload) override; + RefPtr<GenericNonExclusivePromise> Init(const MediaInfo& aInfo, + bool aShouldPreload) override; void Play() override; void Pause() override; void Seek(const media::TimeUnit& aTargetTime) override; @@ -111,7 +113,7 @@ class MFMediaEngineWrapper final : public ExternalPlaybackEngine { media::TimeUnit GetCurrentPosition() override; void NotifyEndOfStream(TrackInfo::TrackType aType) override; uint64_t Id() const override { return mEngine->Id(); } - void SetMediaInfo(const MediaInfo& aInfo) override; + bool IsInited() const { return mEngine->Id() != 0; } bool SetCDMProxy(CDMProxy* aProxy) override; void NotifyResizing(uint32_t aWidth, uint32_t aHeight) override; @@ -121,7 +123,6 @@ class MFMediaEngineWrapper final : public ExternalPlaybackEngine { private: friend class MFMediaEngineChild; - bool IsInited() const { return mEngine->Id() != 0; } void UpdateCurrentTime(double aCurrentTimeInSecond); void NotifyEvent(ExternalEngineEvent aEvent); void NotifyError(const MediaResult& aError); diff --git a/dom/media/ipc/MFMediaEngineParent.cpp b/dom/media/ipc/MFMediaEngineParent.cpp index 5ed1b71160..3a9670f330 100644 --- a/dom/media/ipc/MFMediaEngineParent.cpp +++ b/dom/media/ipc/MFMediaEngineParent.cpp @@ -338,17 +338,17 @@ mozilla::ipc::IPCResult MFMediaEngineParent::RecvInitMediaEngine( // TODO : really need this? Unused << mMediaEngine->SetPreload(MF_MEDIA_ENGINE_PRELOAD_AUTOMATIC); } + RETURN_PARAM_IF_FAILED(SetMediaInfo(aInfo.mediaInfo()), IPC_OK()); aResolver(mMediaEngineId); return IPC_OK(); } -mozilla::ipc::IPCResult MFMediaEngineParent::RecvNotifyMediaInfo( - const MediaInfoIPDL& aInfo) { +HRESULT MFMediaEngineParent::SetMediaInfo(const MediaInfoIPDL& aInfo) { AssertOnManagerThread(); MOZ_ASSERT(mIsCreatedMediaEngine, "Hasn't created media engine?"); MOZ_ASSERT(!mMediaSource); - LOG("RecvNotifyMediaInfo"); + LOG("SetMediaInfo"); auto errorExit = MakeScopeExit([&] { MediaResult error(NS_ERROR_DOM_MEDIA_FATAL_ERR, @@ -378,9 +378,8 @@ mozilla::ipc::IPCResult MFMediaEngineParent::RecvNotifyMediaInfo( if (aInfo.videoInfo()) { ComPtr<IMFMediaEngineEx> mediaEngineEx; - RETURN_PARAM_IF_FAILED(mMediaEngine.As(&mediaEngineEx), IPC_OK()); - RETURN_PARAM_IF_FAILED(mediaEngineEx->EnableWindowlessSwapchainMode(true), - IPC_OK()); + RETURN_IF_FAILED(mMediaEngine.As(&mediaEngineEx)); + RETURN_IF_FAILED(mediaEngineEx->EnableWindowlessSwapchainMode(true)); LOG("Enabled dcomp swap chain mode"); ENGINE_MARKER("MFMediaEngineParent,EnabledSwapChain"); } @@ -392,7 +391,7 @@ mozilla::ipc::IPCResult MFMediaEngineParent::RecvNotifyMediaInfo( #ifdef MOZ_WMF_CDM if (isEncryted && !mContentProtectionManager) { // We will set the source later when the CDM proxy is ready. - return IPC_OK(); + return S_OK; } if (isEncryted && mContentProtectionManager) { @@ -403,7 +402,7 @@ mozilla::ipc::IPCResult MFMediaEngineParent::RecvNotifyMediaInfo( #endif SetMediaSourceOnEngine(); - return IPC_OK(); + return S_OK; } void MFMediaEngineParent::SetMediaSourceOnEngine() { diff --git a/dom/media/ipc/MFMediaEngineParent.h b/dom/media/ipc/MFMediaEngineParent.h index f606d3c44d..843ac91aaf 100644 --- a/dom/media/ipc/MFMediaEngineParent.h +++ b/dom/media/ipc/MFMediaEngineParent.h @@ -52,7 +52,6 @@ class MFMediaEngineParent final : public PMFMediaEngineParent { // Methods for PMFMediaEngineParent mozilla::ipc::IPCResult RecvInitMediaEngine( const MediaEngineInfoIPDL& aInfo, InitMediaEngineResolver&& aResolver); - mozilla::ipc::IPCResult RecvNotifyMediaInfo(const MediaInfoIPDL& aInfo); mozilla::ipc::IPCResult RecvPlay(); mozilla::ipc::IPCResult RecvPause(); mozilla::ipc::IPCResult RecvSeek(double aTargetTimeInSecond); @@ -69,6 +68,7 @@ class MFMediaEngineParent final : public PMFMediaEngineParent { ~MFMediaEngineParent(); void CreateMediaEngine(); + HRESULT SetMediaInfo(const MediaInfoIPDL& aInfo); void InitializeDXGIDeviceManager(); diff --git a/dom/media/ipc/MediaIPCUtils.h b/dom/media/ipc/MediaIPCUtils.h index fecf41c325..9598e1557f 100644 --- a/dom/media/ipc/MediaIPCUtils.h +++ b/dom/media/ipc/MediaIPCUtils.h @@ -15,6 +15,7 @@ #include "ipc/EnumSerializer.h" #include "mozilla/EnumSet.h" #include "mozilla/GfxMessageUtils.h" +#include "mozilla/dom/WebGLIpdl.h" #include "mozilla/gfx/Rect.h" #include "mozilla/dom/MFCDMSerializers.h" @@ -139,7 +140,7 @@ struct ParamTraits<mozilla::FlacCodecSpecificData> { template <> struct ParamTraits<mozilla::Mp3CodecSpecificData> - : public PlainOldDataSerializer<mozilla::Mp3CodecSpecificData> {}; + : public ParamTraits_TiedFields<mozilla::Mp3CodecSpecificData> {}; template <> struct ParamTraits<mozilla::OpusCodecSpecificData> { diff --git a/dom/media/ipc/PMFCDM.ipdl b/dom/media/ipc/PMFCDM.ipdl index e86b94c217..793cfa3808 100644 --- a/dom/media/ipc/PMFCDM.ipdl +++ b/dom/media/ipc/PMFCDM.ipdl @@ -12,6 +12,7 @@ using mozilla::CryptoScheme from "MediaData.h"; using mozilla::dom::MediaKeyMessageType from "mozilla/dom/MediaKeyMessageEventBinding.h"; using mozilla::dom::MediaKeyStatus from "mozilla/dom/MediaKeyStatusMapBinding.h"; using mozilla::dom::HDCPVersion from "mozilla/dom/MediaKeysBinding.h"; +using mozilla::KeySystemConfigRequest from "mozilla/KeySystemConfig.h"; namespace mozilla { @@ -57,7 +58,8 @@ struct MFCDMCapabilitiesIPDL { CryptoScheme[] encryptionSchemes; Requirement distinctiveID; Requirement persistentState; - bool isHDCP22Compatible; + bool? isHDCP22Compatible; + bool isHardwareDecryption; }; union MFCDMCapabilitiesResult { @@ -95,12 +97,17 @@ union MFCDMSessionResult { nsresult; }; +struct MFCDMCapabilitiesRequest { + nsString keySystem; + bool isHardwareDecryption; +}; + [ManualDealloc] async protocol PMFCDM { manager PRemoteDecoderManager; parent: - async GetCapabilities(bool isHwSecured) returns (MFCDMCapabilitiesResult result); + async GetCapabilities(MFCDMCapabilitiesRequest request) returns (MFCDMCapabilitiesResult result); async Init(MFCDMInitParamsIPDL params) returns (MFCDMInitResult result); async CreateSessionAndGenerateRequest(MFCDMCreateSessionParamsIPDL type) returns (MFCDMSessionResult result); diff --git a/dom/media/ipc/PMFMediaEngine.ipdl b/dom/media/ipc/PMFMediaEngine.ipdl index 8edc44bb81..ebed4e101c 100644 --- a/dom/media/ipc/PMFMediaEngine.ipdl +++ b/dom/media/ipc/PMFMediaEngine.ipdl @@ -17,6 +17,7 @@ namespace mozilla { struct MediaEngineInfoIPDL { + MediaInfoIPDL mediaInfo; bool preload; }; @@ -39,7 +40,6 @@ async protocol PMFMediaEngine parent: // Return 0 if media engine can't be created. async InitMediaEngine(MediaEngineInfoIPDL info) returns (uint64_t id); - async NotifyMediaInfo(MediaInfoIPDL info); async Play(); async Pause(); async Seek(double targetTimeInSecond); diff --git a/dom/media/ipc/RemoteMediaDataDecoder.cpp b/dom/media/ipc/RemoteMediaDataDecoder.cpp index 6db3c0d940..32e1ee6b31 100644 --- a/dom/media/ipc/RemoteMediaDataDecoder.cpp +++ b/dom/media/ipc/RemoteMediaDataDecoder.cpp @@ -18,7 +18,12 @@ namespace mozilla { ##__VA_ARGS__) RemoteMediaDataDecoder::RemoteMediaDataDecoder(RemoteDecoderChild* aChild) - : mChild(aChild) { + : mChild(aChild), + mDescription("RemoteMediaDataDecoder"_ns), + mProcessName("unknown"_ns), + mCodecName("unknown"_ns), + mIsHardwareAccelerated(false), + mConversion(ConversionRequired::kNeedNone) { LOG("%p is created", this); } @@ -48,6 +53,7 @@ RefPtr<MediaDataDecoder::InitPromise> RemoteMediaDataDecoder::Init() { ->Then( RemoteDecoderManagerChild::GetManagerThread(), __func__, [self, this](TrackType aTrack) { + MutexAutoLock lock(mMutex); // If shutdown has started in the meantime shutdown promise may // be resloved before this task. In this case mChild will be null // and the init promise has to be canceled. @@ -127,6 +133,7 @@ RefPtr<ShutdownPromise> RemoteMediaDataDecoder::Shutdown() { bool RemoteMediaDataDecoder::IsHardwareAccelerated( nsACString& aFailureReason) const { + MutexAutoLock lock(mMutex); aFailureReason = mHardwareAcceleratedReason; return mIsHardwareAccelerated; } @@ -145,18 +152,24 @@ void RemoteMediaDataDecoder::SetSeekThreshold(const media::TimeUnit& aTime) { MediaDataDecoder::ConversionRequired RemoteMediaDataDecoder::NeedsConversion() const { + MutexAutoLock lock(mMutex); return mConversion; } nsCString RemoteMediaDataDecoder::GetDescriptionName() const { + MutexAutoLock lock(mMutex); return mDescription; } nsCString RemoteMediaDataDecoder::GetProcessName() const { + MutexAutoLock lock(mMutex); return mProcessName; } -nsCString RemoteMediaDataDecoder::GetCodecName() const { return mCodecName; } +nsCString RemoteMediaDataDecoder::GetCodecName() const { + MutexAutoLock lock(mMutex); + return mCodecName; +} #undef LOG diff --git a/dom/media/ipc/RemoteMediaDataDecoder.h b/dom/media/ipc/RemoteMediaDataDecoder.h index 4acc5801f7..5d8612529d 100644 --- a/dom/media/ipc/RemoteMediaDataDecoder.h +++ b/dom/media/ipc/RemoteMediaDataDecoder.h @@ -53,14 +53,16 @@ class RemoteMediaDataDecoder final // destructor when we can guarantee no other threads are accessing it). Only // read from the manager thread. RefPtr<RemoteDecoderChild> mChild; + + mutable Mutex mMutex{"RemoteMediaDataDecoder"}; + // Only ever written/modified during decoder initialisation. - // As such can be accessed from any threads after that. - nsCString mDescription = "RemoteMediaDataDecoder"_ns; - nsCString mProcessName = "unknown"_ns; - nsCString mCodecName = "unknown"_ns; - bool mIsHardwareAccelerated = false; - nsCString mHardwareAcceleratedReason; - ConversionRequired mConversion = ConversionRequired::kNeedNone; + nsCString mDescription MOZ_GUARDED_BY(mMutex); + nsCString mProcessName MOZ_GUARDED_BY(mMutex); + nsCString mCodecName MOZ_GUARDED_BY(mMutex); + bool mIsHardwareAccelerated MOZ_GUARDED_BY(mMutex); + nsCString mHardwareAcceleratedReason MOZ_GUARDED_BY(mMutex); + ConversionRequired mConversion MOZ_GUARDED_BY(mMutex); }; } // namespace mozilla diff --git a/dom/media/mediacontrol/ContentMediaController.cpp b/dom/media/mediacontrol/ContentMediaController.cpp index c0b466ff0f..e1fe574d9b 100644 --- a/dom/media/mediacontrol/ContentMediaController.cpp +++ b/dom/media/mediacontrol/ContentMediaController.cpp @@ -229,7 +229,7 @@ void ContentMediaAgent::EnableAction(uint64_t aBrowsingContextId, } LOG("Notify to enable action '%s' in BC %" PRId64, - ToMediaSessionActionStr(aAction), bc->Id()); + GetEnumString(aAction).get(), bc->Id()); if (XRE_IsContentProcess()) { ContentChild* contentChild = ContentChild::GetSingleton(); Unused << contentChild->SendNotifyMediaSessionSupportedActionChanged( @@ -251,7 +251,7 @@ void ContentMediaAgent::DisableAction(uint64_t aBrowsingContextId, } LOG("Notify to disable action '%s' in BC %" PRId64, - ToMediaSessionActionStr(aAction), bc->Id()); + GetEnumString(aAction).get(), bc->Id()); if (XRE_IsContentProcess()) { ContentChild* contentChild = ContentChild::GetSingleton(); Unused << contentChild->SendNotifyMediaSessionSupportedActionChanged( @@ -325,7 +325,7 @@ void ContentMediaController::HandleMediaKey(MediaControlKey aKey) { if (mReceivers.IsEmpty()) { return; } - LOG("Handle '%s' event, receiver num=%zu", ToMediaControlKeyStr(aKey), + LOG("Handle '%s' event, receiver num=%zu", GetEnumString(aKey).get(), mReceivers.Length()); // We have default handlers for play, pause and stop. // https://w3c.github.io/mediasession/#ref-for-dom-mediasessionaction-play%E2%91%A3 diff --git a/dom/media/mediacontrol/ContentPlaybackController.cpp b/dom/media/mediacontrol/ContentPlaybackController.cpp index fcc8e3ab58..ba06ea1cdb 100644 --- a/dom/media/mediacontrol/ContentPlaybackController.cpp +++ b/dom/media/mediacontrol/ContentPlaybackController.cpp @@ -46,7 +46,7 @@ void ContentPlaybackController::NotifyContentMediaControlKeyReceiver( if (RefPtr<ContentMediaControlKeyReceiver> receiver = ContentMediaControlKeyReceiver::Get(mBC)) { LOG("Handle '%s' in default behavior for BC %" PRIu64, - ToMediaControlKeyStr(aKey), mBC->Id()); + GetEnumString(aKey).get(), mBC->Id()); receiver->HandleMediaKey(aKey); } } @@ -61,7 +61,7 @@ void ContentPlaybackController::NotifyMediaSession( const MediaSessionActionDetails& aDetails) { if (RefPtr<MediaSession> session = GetMediaSession()) { LOG("Handle '%s' in media session behavior for BC %" PRIu64, - ToMediaSessionActionStr(aDetails.mAction), mBC->Id()); + GetEnumString(aDetails.mAction).get(), mBC->Id()); MOZ_ASSERT(session->IsActive(), "Notify inactive media session!"); session->NotifyHandler(aDetails); } diff --git a/dom/media/mediacontrol/MediaControlKeyManager.cpp b/dom/media/mediacontrol/MediaControlKeyManager.cpp index ba6ed3a524..b40d3af91e 100644 --- a/dom/media/mediacontrol/MediaControlKeyManager.cpp +++ b/dom/media/mediacontrol/MediaControlKeyManager.cpp @@ -161,7 +161,7 @@ void MediaControlKeyManager::SetSupportedMediaKeys( const MediaKeysArray& aSupportedKeys) { mSupportedKeys.Clear(); for (const auto& key : aSupportedKeys) { - LOG_INFO("Supported keys=%s", ToMediaControlKeyStr(key)); + LOG_INFO("Supported keys=%s", GetEnumString(key).get()); mSupportedKeys.AppendElement(key); } if (mEventSource && mEventSource->IsOpened()) { diff --git a/dom/media/mediacontrol/MediaControlUtils.h b/dom/media/mediacontrol/MediaControlUtils.h index e4e75e7c97..f013c40aa2 100644 --- a/dom/media/mediacontrol/MediaControlUtils.h +++ b/dom/media/mediacontrol/MediaControlUtils.h @@ -20,66 +20,12 @@ extern mozilla::LazyLogModule gMediaControlLog; namespace mozilla::dom { -inline const char* ToMediaControlKeyStr(MediaControlKey aKey) { - switch (aKey) { - case MediaControlKey::Focus: - return "Focus"; - case MediaControlKey::Pause: - return "Pause"; - case MediaControlKey::Play: - return "Play"; - case MediaControlKey::Playpause: - return "Play & pause"; - case MediaControlKey::Previoustrack: - return "Previous track"; - case MediaControlKey::Nexttrack: - return "Next track"; - case MediaControlKey::Seekbackward: - return "Seek backward"; - case MediaControlKey::Seekforward: - return "Seek forward"; - case MediaControlKey::Skipad: - return "Skip Ad"; - case MediaControlKey::Seekto: - return "Seek to"; - case MediaControlKey::Stop: - return "Stop"; - default: - MOZ_ASSERT_UNREACHABLE("Invalid action."); - return "Unknown"; - } -} - inline const char* ToMediaControlKeyStr(const Maybe<MediaControlKey>& aKey) { if (aKey.isNothing()) { MOZ_ASSERT_UNREACHABLE("Invalid action."); return "Unknown"; } - return ToMediaControlKeyStr(aKey.value()); -} - -inline const char* ToMediaSessionActionStr(MediaSessionAction aAction) { - switch (aAction) { - case MediaSessionAction::Play: - return "play"; - case MediaSessionAction::Pause: - return "pause"; - case MediaSessionAction::Seekbackward: - return "seek backward"; - case MediaSessionAction::Seekforward: - return "seek forward"; - case MediaSessionAction::Previoustrack: - return "previous track"; - case MediaSessionAction::Nexttrack: - return "next track"; - case MediaSessionAction::Skipad: - return "skip ad"; - case MediaSessionAction::Seekto: - return "Seek to"; - default: - MOZ_ASSERT(aAction == MediaSessionAction::Stop); - return "stop"; - } + return GetEnumString(aKey.value()).get(); } inline MediaControlKey ConvertMediaSessionActionToControlKey( diff --git a/dom/media/mediacontrol/MediaStatusManager.cpp b/dom/media/mediacontrol/MediaStatusManager.cpp index 9187e56f25..633ae19a44 100644 --- a/dom/media/mediacontrol/MediaStatusManager.cpp +++ b/dom/media/mediacontrol/MediaStatusManager.cpp @@ -338,10 +338,10 @@ void MediaStatusManager::EnableAction(uint64_t aBrowsingContextId, } if (info->IsActionSupported(aAction)) { LOG("Action '%s' has already been enabled for context %" PRIu64, - ToMediaSessionActionStr(aAction), aBrowsingContextId); + GetEnumString(aAction).get(), aBrowsingContextId); return; } - LOG("Enable action %s for context %" PRIu64, ToMediaSessionActionStr(aAction), + LOG("Enable action %s for context %" PRIu64, GetEnumString(aAction).get(), aBrowsingContextId); info->EnableAction(aAction); NotifySupportedKeysChangedIfNeeded(aBrowsingContextId); @@ -355,11 +355,11 @@ void MediaStatusManager::DisableAction(uint64_t aBrowsingContextId, } if (!info->IsActionSupported(aAction)) { LOG("Action '%s' hasn't been enabled yet for context %" PRIu64, - ToMediaSessionActionStr(aAction), aBrowsingContextId); + GetEnumString(aAction).get(), aBrowsingContextId); return; } - LOG("Disable action %s for context %" PRIu64, - ToMediaSessionActionStr(aAction), aBrowsingContextId); + LOG("Disable action %s for context %" PRIu64, GetEnumString(aAction).get(), + aBrowsingContextId); info->DisableAction(aAction); NotifySupportedKeysChangedIfNeeded(aBrowsingContextId); } diff --git a/dom/media/mediacontrol/tests/browser/browser.toml b/dom/media/mediacontrol/tests/browser/browser.toml index 8b52f2aed4..faeebc0e94 100644 --- a/dom/media/mediacontrol/tests/browser/browser.toml +++ b/dom/media/mediacontrol/tests/browser/browser.toml @@ -16,7 +16,6 @@ support-files = [ "file_non_eligible_media.html", "file_non_looping_media.html", "head.js", - "../../../test/bogus.ogv", "../../../test/gizmo.mp4", "../../../test/gizmo-noaudio.webm", "../../../test/gizmo-short.mp4", diff --git a/dom/media/mediacontrol/tests/browser/file_error_media.html b/dom/media/mediacontrol/tests/browser/file_error_media.html index 7f54340dd1..dfcdeab65f 100644 --- a/dom/media/mediacontrol/tests/browser/file_error_media.html +++ b/dom/media/mediacontrol/tests/browser/file_error_media.html @@ -4,6 +4,6 @@ <title>Error media</title> </head> <body> -<video id="video" src="bogus.ogv"></video> +<video id="video" src="bogus.webm"></video> </body> </html> diff --git a/dom/media/metrics.yaml b/dom/media/metrics.yaml index fe2ed5ff6a..58e525174b 100644 --- a/dom/media/metrics.yaml +++ b/dom/media/metrics.yaml @@ -117,7 +117,24 @@ media.playback: first_frame_loaded_time: description: How long (in milliseconds) does the our media pipeline take to load - the first video frame. + the first video frame from "the creation of MDSM" to "the first frame + loaded". + type: quantity + metadata_loaded_time: + description: + How long (in milliseconds) does the our media pipeline take to load + the metadata, which happens before finishing loading the first frame. + type: quantity + total_waiting_data_time: + description: + How long (in milliseconds) does the our media pipeline has been in a + state of waiting video data due to lacking of data before the first + frame is loaded. + type: quantity + buffering_time: + description: + How long (in milliseconds) does the our media pipeline has been spent + on the buffering state before the first frame is loaded. type: quantity playback_type: description: @@ -139,4 +156,13 @@ media.playback: key_system: description: The key system used for the EME playback if exists type: string + hls_decoder: + description: + This value will only be set on Android. It tells that whether playback + is performed by the HLS decoder, which utilizes the external player to + play video. + type: boolean + is_hardware_decoding: + description: True if the first frame is decoded by a hardware decoder. + type: boolean expires: never diff --git a/dom/media/moz.build b/dom/media/moz.build index c78b794591..ac62e9b67e 100644 --- a/dom/media/moz.build +++ b/dom/media/moz.build @@ -200,6 +200,7 @@ EXPORTS += [ "SeekTarget.h", "SelfRef.h", "SharedBuffer.h", + "TimedPacketizer.h", "TimeUnits.h", "Tracing.h", "VideoFrameContainer.h", diff --git a/dom/media/ogg/OggDecoder.cpp b/dom/media/ogg/OggDecoder.cpp index 5f6d61f694..1bacafcf3e 100644 --- a/dom/media/ogg/OggDecoder.cpp +++ b/dom/media/ogg/OggDecoder.cpp @@ -24,7 +24,10 @@ bool OggDecoder::IsSupportedType(const MediaContainerType& aContainerType) { return false; } - const bool isOggVideo = (aContainerType.Type() != MEDIAMIMETYPE(AUDIO_OGG)); + const bool isOggVideo = (aContainerType.Type() == MEDIAMIMETYPE(VIDEO_OGG)); + if (isOggVideo && !StaticPrefs::media_theora_enabled()) { + return false; + } const MediaCodecs& codecs = aContainerType.ExtendedType().Codecs(); if (codecs.IsEmpty()) { @@ -40,8 +43,9 @@ bool OggDecoder::IsSupportedType(const MediaContainerType& aContainerType) { } // Note: Only accept Theora in a video container type, not in an audio // container type. - if (isOggVideo && codec.EqualsLiteral("theora")) { - continue; + if (aContainerType.Type() != MEDIAMIMETYPE(AUDIO_OGG) && + codec.EqualsLiteral("theora")) { + return StaticPrefs::media_theora_enabled(); } // Some unsupported codec. return false; diff --git a/dom/media/platforms/EncoderConfig.cpp b/dom/media/platforms/EncoderConfig.cpp new file mode 100644 index 0000000000..ed780b947c --- /dev/null +++ b/dom/media/platforms/EncoderConfig.cpp @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "EncoderConfig.h" +#include "MP4Decoder.h" +#include "VPXDecoder.h" + +namespace mozilla { + +CodecType EncoderConfig::CodecTypeForMime(const nsACString& aMimeType) { + if (MP4Decoder::IsH264(aMimeType)) { + return CodecType::H264; + } + if (VPXDecoder::IsVPX(aMimeType, VPXDecoder::VP8)) { + return CodecType::VP8; + } + if (VPXDecoder::IsVPX(aMimeType, VPXDecoder::VP9)) { + return CodecType::VP9; + } + MOZ_ASSERT_UNREACHABLE("Unsupported Mimetype"); + return CodecType::Unknown; +} + +} // namespace mozilla diff --git a/dom/media/platforms/EncoderConfig.h b/dom/media/platforms/EncoderConfig.h new file mode 100644 index 0000000000..e0da1709d6 --- /dev/null +++ b/dom/media/platforms/EncoderConfig.h @@ -0,0 +1,190 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_EncoderConfig_h_ +#define mozilla_EncoderConfig_h_ + +#include "mozilla/dom/ImageBitmapBinding.h" +#include "H264.h" + +namespace mozilla { + +enum class CodecType { + _BeginVideo_, + H264, + VP8, + VP9, + AV1, + _EndVideo_, + _BeginAudio_ = _EndVideo_, + Opus, + Vorbis, + Flac, + AAC, + PCM, + G722, + _EndAudio_, + Unknown, +}; + +enum class Usage { + Realtime, // Low latency prefered + Record +}; +enum class BitrateMode { Constant, Variable }; +// Scalable Video Coding (SVC) settings for WebCodecs: +// https://www.w3.org/TR/webrtc-svc/ +enum class ScalabilityMode { None, L1T2, L1T3 }; + +enum class HardwarePreference { RequireHardware, RequireSoftware, None }; + +// TODO: Automatically generate this (Bug 1865896) +const char* GetCodecTypeString(const CodecType& aCodecType); + +enum class H264BitStreamFormat { AVC, ANNEXB }; + +struct H264Specific final { + const H264_PROFILE mProfile; + const H264_LEVEL mLevel; + const H264BitStreamFormat mFormat; + + H264Specific(H264_PROFILE aProfile, H264_LEVEL aLevel, + H264BitStreamFormat aFormat) + : mProfile(aProfile), mLevel(aLevel), mFormat(aFormat) {} +}; + +enum class OpusBitstreamFormat { Opus, OGG }; + +// The default values come from the Web Codecs specification. +struct OpusSpecific final { + enum class Application { Unspecified, Voip, Audio, RestricedLowDelay }; + Application mApplication = Application::Unspecified; + uint64_t mFrameDuration = 20000; // microseconds + uint8_t mComplexity = 10; // 0-10 + OpusBitstreamFormat mFormat = OpusBitstreamFormat::Opus; + uint64_t mPacketLossPerc = 0; // 0-100 + bool mUseInBandFEC = false; + bool mUseDTX = false; +}; + +enum class VPXComplexity { Normal, High, Higher, Max }; +struct VP8Specific { + VP8Specific() = default; + // Ignore webrtc::VideoCodecVP8::errorConcealmentOn, + // for it's always false in the codebase (except libwebrtc test cases). + VP8Specific(const VPXComplexity aComplexity, const bool aResilience, + const uint8_t aNumTemporalLayers, const bool aDenoising, + const bool aAutoResize, const bool aFrameDropping) + : mComplexity(aComplexity), + mResilience(aResilience), + mNumTemporalLayers(aNumTemporalLayers), + mDenoising(aDenoising), + mAutoResize(aAutoResize), + mFrameDropping(aFrameDropping) {} + const VPXComplexity mComplexity{VPXComplexity::Normal}; + const bool mResilience{true}; + const uint8_t mNumTemporalLayers{1}; + const bool mDenoising{true}; + const bool mAutoResize{false}; + const bool mFrameDropping{false}; +}; + +struct VP9Specific : public VP8Specific { + VP9Specific() = default; + VP9Specific(const VPXComplexity aComplexity, const bool aResilience, + const uint8_t aNumTemporalLayers, const bool aDenoising, + const bool aAutoResize, const bool aFrameDropping, + const bool aAdaptiveQp, const uint8_t aNumSpatialLayers, + const bool aFlexible) + : VP8Specific(aComplexity, aResilience, aNumTemporalLayers, aDenoising, + aAutoResize, aFrameDropping), + mAdaptiveQp(aAdaptiveQp), + mNumSpatialLayers(aNumSpatialLayers), + mFlexible(aFlexible) {} + const bool mAdaptiveQp{true}; + const uint8_t mNumSpatialLayers{1}; + const bool mFlexible{false}; +}; + +// A class that holds the intial configuration of an encoder. For simplicity, +// this is used for both audio and video encoding. Members irrelevant to the +// instance are to be ignored, and are set at their default value. +class EncoderConfig final { + public: + using PixelFormat = dom::ImageBitmapFormat; + using CodecSpecific = + Variant<H264Specific, OpusSpecific, VP8Specific, VP9Specific>; + + EncoderConfig(const EncoderConfig& aConfig) = default; + + // This constructor is used for video encoders + EncoderConfig(const CodecType aCodecType, gfx::IntSize aSize, + const Usage aUsage, const PixelFormat aPixelFormat, + const PixelFormat aSourcePixelFormat, const uint8_t aFramerate, + const size_t aKeyframeInterval, const uint32_t aBitrate, + const BitrateMode aBitrateMode, + const HardwarePreference aHardwarePreference, + const ScalabilityMode aScalabilityMode, + const Maybe<CodecSpecific>& aCodecSpecific) + : mCodec(aCodecType), + mSize(aSize), + mBitrateMode(aBitrateMode), + mBitrate(aBitrate), + mUsage(aUsage), + mHardwarePreference(aHardwarePreference), + mPixelFormat(aPixelFormat), + mSourcePixelFormat(aSourcePixelFormat), + mScalabilityMode(aScalabilityMode), + mFramerate(aFramerate), + mKeyframeInterval(aKeyframeInterval), + mCodecSpecific(aCodecSpecific) { + MOZ_ASSERT(IsVideo()); + } + + // This constructor is used for audio encoders + EncoderConfig(const CodecType aCodecType, uint32_t aNumberOfChannels, + const BitrateMode aBitrateMode, uint32_t aSampleRate, + uint32_t aBitrate, const Maybe<CodecSpecific>& aCodecSpecific) + : mCodec(aCodecType), + mBitrateMode(aBitrateMode), + mBitrate(aBitrate), + mNumberOfChannels(aNumberOfChannels), + mSampleRate(aSampleRate), + mCodecSpecific(aCodecSpecific) { + MOZ_ASSERT(IsAudio()); + } + + static CodecType CodecTypeForMime(const nsACString& aMimeType); + + bool IsVideo() const { + return mCodec > CodecType::_BeginVideo_ && mCodec < CodecType::_EndVideo_; + } + + bool IsAudio() const { + return mCodec > CodecType::_BeginAudio_ && mCodec < CodecType::_EndAudio_; + } + + CodecType mCodec{}; + gfx::IntSize mSize{}; + BitrateMode mBitrateMode{}; + uint32_t mBitrate{}; + Usage mUsage{}; + // Video-only + HardwarePreference mHardwarePreference{}; + PixelFormat mPixelFormat{}; + PixelFormat mSourcePixelFormat{}; + ScalabilityMode mScalabilityMode{}; + uint8_t mFramerate{}; + size_t mKeyframeInterval{}; + // Audio-only + uint32_t mNumberOfChannels{}; + uint32_t mSampleRate{}; + Maybe<CodecSpecific> mCodecSpecific{}; +}; + +} // namespace mozilla + +#endif // mozilla_EncoderConfig_h_ diff --git a/dom/media/platforms/PlatformEncoderModule.cpp b/dom/media/platforms/PlatformEncoderModule.cpp index 3eb4abd511..525729e756 100644 --- a/dom/media/platforms/PlatformEncoderModule.cpp +++ b/dom/media/platforms/PlatformEncoderModule.cpp @@ -32,6 +32,15 @@ const char* GetCodecTypeString(const CodecType& aCodecType) { return "_EndVideo_/_BeginAudio_"; case CodecType::Opus: return "Opus"; + case CodecType::Vorbis: + return "Vorbis"; + case CodecType::Flac: + return "Flac"; + case CodecType::AAC: + return "AAC"; + case CodecType::PCM: + return "PCM"; + break; case CodecType::G722: return "G722"; case CodecType::_EndAudio_: @@ -100,22 +109,28 @@ struct ConfigurationChangeToString { return nsPrintfCString("Framerate: %lfHz", aFramerateChange.get().value()); } nsCString operator()(const BitrateModeChange& aBitrateModeChange) { - return nsPrintfCString( - "Bitrate mode: %s", - aBitrateModeChange.get() == MediaDataEncoder::BitrateMode::Constant - ? "Constant" - : "Variable"); + return nsPrintfCString("Bitrate mode: %s", + aBitrateModeChange.get() == BitrateMode::Constant + ? "Constant" + : "Variable"); } nsCString operator()(const UsageChange& aUsageChange) { return nsPrintfCString( "Usage mode: %s", - aUsageChange.get() == MediaDataEncoder::Usage::Realtime ? "Realtime" - : "Recoding"); + aUsageChange.get() == Usage::Realtime ? "Realtime" : "Recoding"); } nsCString operator()(const ContentHintChange& aContentHintChange) { return nsPrintfCString("Content hint: %s", MaybeToString(aContentHintChange.get()).get()); } + nsCString operator()(const SampleRateChange& aSampleRateChange) { + return nsPrintfCString("Sample rate %" PRIu32 "Hz", + aSampleRateChange.get()); + } + nsCString operator()(const NumberOfChannelsChange& aNumberOfChannelsChange) { + return nsPrintfCString("Channels: %" PRIu32 "Hz", + aNumberOfChannelsChange.get()); + } }; nsString EncoderConfigurationChangeList::ToString() const { @@ -132,7 +147,9 @@ bool CanLikelyEncode(const EncoderConfig& aConfig) { if (aConfig.mCodec == CodecType::H264) { if (!aConfig.mCodecSpecific || !aConfig.mCodecSpecific->is<H264Specific>()) { - LOGD("Error: asking for support codec for h264 without h264 specific config."); + LOGD( + "Error: asking for support codec for h264 without h264 specific " + "config."); return false; } H264Specific specific = aConfig.mCodecSpecific->as<H264Specific>(); diff --git a/dom/media/platforms/PlatformEncoderModule.h b/dom/media/platforms/PlatformEncoderModule.h index 72dad430e6..222a9bb48c 100644 --- a/dom/media/platforms/PlatformEncoderModule.h +++ b/dom/media/platforms/PlatformEncoderModule.h @@ -8,11 +8,8 @@ # define PlatformEncoderModule_h_ # include "MP4Decoder.h" -# include "MediaData.h" -# include "MediaInfo.h" # include "MediaResult.h" # include "VPXDecoder.h" -# include "mozilla/Attributes.h" # include "mozilla/Maybe.h" # include "mozilla/MozPromise.h" # include "mozilla/RefPtr.h" @@ -20,93 +17,14 @@ # include "mozilla/dom/ImageBitmapBinding.h" # include "nsISupportsImpl.h" # include "VideoUtils.h" +# include "EncoderConfig.h" namespace mozilla { class MediaDataEncoder; -class EncoderConfig; +class MediaData; struct EncoderConfigurationChangeList; -enum class CodecType { - _BeginVideo_, - H264, - VP8, - VP9, - AV1, - _EndVideo_, - _BeginAudio_ = _EndVideo_, - Opus, - G722, - _EndAudio_, - Unknown, -}; - -// TODO: Automatically generate this (Bug 1865896) -const char* GetCodecTypeString(const CodecType& aCodecType); - -enum class H264BitStreamFormat { AVC, ANNEXB }; - -struct H264Specific final { - const H264_PROFILE mProfile; - const H264_LEVEL mLevel; - const H264BitStreamFormat mFormat; - - H264Specific(H264_PROFILE aProfile, H264_LEVEL aLevel, - H264BitStreamFormat aFormat) - : mProfile(aProfile), mLevel(aLevel), mFormat(aFormat) {} -}; - -struct OpusSpecific final { - enum class Application { Voip, Audio, RestricedLowDelay }; - - const Application mApplication; - const uint8_t mComplexity; // from 0-10 - - OpusSpecific(const Application aApplication, const uint8_t aComplexity) - : mApplication(aApplication), mComplexity(aComplexity) { - MOZ_ASSERT(mComplexity <= 10); - } -}; - -enum class VPXComplexity { Normal, High, Higher, Max }; -struct VP8Specific { - VP8Specific() = default; - // Ignore webrtc::VideoCodecVP8::errorConcealmentOn, - // for it's always false in the codebase (except libwebrtc test cases). - VP8Specific(const VPXComplexity aComplexity, const bool aResilience, - const uint8_t aNumTemporalLayers, const bool aDenoising, - const bool aAutoResize, const bool aFrameDropping) - : mComplexity(aComplexity), - mResilience(aResilience), - mNumTemporalLayers(aNumTemporalLayers), - mDenoising(aDenoising), - mAutoResize(aAutoResize), - mFrameDropping(aFrameDropping) {} - const VPXComplexity mComplexity{VPXComplexity::Normal}; - const bool mResilience{true}; - const uint8_t mNumTemporalLayers{1}; - const bool mDenoising{true}; - const bool mAutoResize{false}; - const bool mFrameDropping{false}; -}; - -struct VP9Specific : public VP8Specific { - VP9Specific() = default; - VP9Specific(const VPXComplexity aComplexity, const bool aResilience, - const uint8_t aNumTemporalLayers, const bool aDenoising, - const bool aAutoResize, const bool aFrameDropping, - const bool aAdaptiveQp, const uint8_t aNumSpatialLayers, - const bool aFlexible) - : VP8Specific(aComplexity, aResilience, aNumTemporalLayers, aDenoising, - aAutoResize, aFrameDropping), - mAdaptiveQp(aAdaptiveQp), - mNumSpatialLayers(aNumSpatialLayers), - mFlexible(aFlexible) {} - const bool mAdaptiveQp{true}; - const uint8_t mNumSpatialLayers{1}; - const bool mFlexible{false}; -}; - class PlatformEncoderModule { public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PlatformEncoderModule) @@ -144,18 +62,6 @@ class MediaDataEncoder { public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDataEncoder) - enum class Usage { - Realtime, // Low latency prefered - Record - }; - using PixelFormat = dom::ImageBitmapFormat; - enum class BitrateMode { Constant, Variable }; - // Scalable Video Coding (SVC) settings for WebCodecs: - // https://www.w3.org/TR/webrtc-svc/ - enum class ScalabilityMode { None, L1T2, L1T3 }; - - enum class HardwarePreference { RequireHardware, RequireSoftware, None }; - static bool IsVideo(const CodecType aCodec) { return aCodec > CodecType::_BeginVideo_ && aCodec < CodecType::_EndVideo_; } @@ -163,8 +69,7 @@ class MediaDataEncoder { return aCodec > CodecType::_BeginAudio_ && aCodec < CodecType::_EndAudio_; } - using InitPromise = - MozPromise<TrackInfo::TrackType, MediaResult, /* IsExclusive = */ true>; + using InitPromise = MozPromise<bool, MediaResult, /* IsExclusive = */ true>; using EncodedData = nsTArray<RefPtr<MediaRawData>>; using EncodePromise = MozPromise<EncodedData, MediaResult, /* IsExclusive = */ true>; @@ -229,85 +134,6 @@ class MediaDataEncoder { virtual ~MediaDataEncoder() = default; }; -class EncoderConfig final { - public: - using CodecSpecific = - Variant<H264Specific, OpusSpecific, VP8Specific, VP9Specific>; - - EncoderConfig(const EncoderConfig& aConfig) - : mCodec(aConfig.mCodec), - mSize(aConfig.mSize), - mUsage(aConfig.mUsage), - mHardwarePreference(aConfig.mHardwarePreference), - mPixelFormat(aConfig.mPixelFormat), - mSourcePixelFormat(aConfig.mSourcePixelFormat), - mScalabilityMode(aConfig.mScalabilityMode), - mFramerate(aConfig.mFramerate), - mKeyframeInterval(aConfig.mKeyframeInterval), - mBitrate(aConfig.mBitrate), - mBitrateMode(aConfig.mBitrateMode), - mCodecSpecific(aConfig.mCodecSpecific) {} - - template <typename... Ts> - EncoderConfig(const CodecType aCodecType, gfx::IntSize aSize, - const MediaDataEncoder::Usage aUsage, - const MediaDataEncoder::PixelFormat aPixelFormat, - const MediaDataEncoder::PixelFormat aSourcePixelFormat, - const uint8_t aFramerate, const size_t aKeyframeInterval, - const uint32_t aBitrate, - const MediaDataEncoder::BitrateMode aBitrateMode, - const MediaDataEncoder::HardwarePreference aHardwarePreference, - const MediaDataEncoder::ScalabilityMode aScalabilityMode, - const Maybe<CodecSpecific>& aCodecSpecific) - : mCodec(aCodecType), - mSize(aSize), - mUsage(aUsage), - mHardwarePreference(aHardwarePreference), - mPixelFormat(aPixelFormat), - mSourcePixelFormat(aSourcePixelFormat), - mScalabilityMode(aScalabilityMode), - mFramerate(aFramerate), - mKeyframeInterval(aKeyframeInterval), - mBitrate(aBitrate), - mBitrateMode(aBitrateMode), - mCodecSpecific(aCodecSpecific) {} - - static CodecType CodecTypeForMime(const nsACString& aMimeType) { - if (MP4Decoder::IsH264(aMimeType)) { - return CodecType::H264; - } - if (VPXDecoder::IsVPX(aMimeType, VPXDecoder::VP8)) { - return CodecType::VP8; - } - if (VPXDecoder::IsVPX(aMimeType, VPXDecoder::VP9)) { - return CodecType::VP9; - } - MOZ_ASSERT_UNREACHABLE("Unsupported Mimetype"); - return CodecType::Unknown; - } - - bool IsVideo() const { - return mCodec > CodecType::_BeginVideo_ && mCodec < CodecType::_EndVideo_; - } - - bool IsAudio() const { - return mCodec > CodecType::_BeginAudio_ && mCodec < CodecType::_EndAudio_; - } - - CodecType mCodec; - gfx::IntSize mSize; - MediaDataEncoder::Usage mUsage; - MediaDataEncoder::HardwarePreference mHardwarePreference; - MediaDataEncoder::PixelFormat mPixelFormat; - MediaDataEncoder::PixelFormat mSourcePixelFormat; - MediaDataEncoder::ScalabilityMode mScalabilityMode; - uint8_t mFramerate{}; - size_t mKeyframeInterval{}; - uint32_t mBitrate{}; - MediaDataEncoder::BitrateMode mBitrateMode{}; - Maybe<CodecSpecific> mCodecSpecific; -}; - // Wrap a type to make it unique. This allows using ergonomically in the Variant // below. Simply aliasing with `using` isn't enough, because typedefs in C++ // don't produce strong types, so two integer variants result in @@ -341,20 +167,25 @@ using FramerateChange = StrongTypedef<Maybe<double>, struct FramerateChangeType>; // The bitrate mode (variable, constant) of the encoding using BitrateModeChange = - StrongTypedef<MediaDataEncoder::BitrateMode, struct BitrateModeChangeType>; + StrongTypedef<BitrateMode, struct BitrateModeChangeType>; // The usage for the encoded stream, this influence latency, ordering, etc. -using UsageChange = - StrongTypedef<MediaDataEncoder::Usage, struct UsageChangeType>; +using UsageChange = StrongTypedef<Usage, struct UsageChangeType>; // If present, the expected content of the video frames (screen, movie, etc.). // The value the string can have isn't decided just yet. When absent, the // encoder uses generic settings. using ContentHintChange = StrongTypedef<Maybe<nsString>, struct ContentHintTypeType>; +// If present, the new sample-rate of the audio +using SampleRateChange = StrongTypedef<uint32_t, struct SampleRateChangeType>; +// If present, the new sample-rate of the audio +using NumberOfChannelsChange = + StrongTypedef<uint32_t, struct NumberOfChannelsChangeType>; // A change to a parameter of an encoder instance. using EncoderConfigurationItem = Variant<DimensionsChange, DisplayDimensionsChange, BitrateModeChange, - BitrateChange, FramerateChange, UsageChange, ContentHintChange>; + BitrateChange, FramerateChange, UsageChange, ContentHintChange, + SampleRateChange, NumberOfChannelsChange>; // A list of changes to an encoder configuration, that _might_ be able to change // on the fly. Not all encoder modules can adjust their configuration on the diff --git a/dom/media/platforms/agnostic/AgnosticDecoderModule.cpp b/dom/media/platforms/agnostic/AgnosticDecoderModule.cpp index 7bdc30b432..753dee0238 100644 --- a/dom/media/platforms/agnostic/AgnosticDecoderModule.cpp +++ b/dom/media/platforms/agnostic/AgnosticDecoderModule.cpp @@ -36,8 +36,9 @@ static bool IsAvailableInDefault(DecoderType type) { case DecoderType::AV1: return StaticPrefs::media_av1_enabled(); #endif - case DecoderType::Opus: case DecoderType::Theora: + return StaticPrefs::media_theora_enabled(); + case DecoderType::Opus: case DecoderType::Vorbis: case DecoderType::VPX: case DecoderType::Wave: @@ -56,7 +57,8 @@ static bool IsAvailableInRdd(DecoderType type) { case DecoderType::Opus: return StaticPrefs::media_rdd_opus_enabled(); case DecoderType::Theora: - return StaticPrefs::media_rdd_theora_enabled(); + return StaticPrefs::media_rdd_theora_enabled() && + StaticPrefs::media_theora_enabled(); case DecoderType::Vorbis: #if defined(__MINGW32__) // If this is a MinGW build we need to force AgnosticDecoderModule to @@ -129,7 +131,8 @@ media::DecodeSupportSet AgnosticDecoderModule::Supports( (AOMDecoder::IsAV1(mimeType) && IsAvailable(DecoderType::AV1)) || #endif (VPXDecoder::IsVPX(mimeType) && IsAvailable(DecoderType::VPX)) || - (TheoraDecoder::IsTheora(mimeType) && IsAvailable(DecoderType::Theora)); + (TheoraDecoder::IsTheora(mimeType) && IsAvailable(DecoderType::Theora) && + StaticPrefs::media_theora_enabled()); MOZ_LOG(sPDMLog, LogLevel::Debug, ("Agnostic decoder %s requested type '%s'", supports ? "supports" : "rejects", mimeType.BeginReading())); @@ -164,7 +167,8 @@ already_AddRefed<MediaDataDecoder> AgnosticDecoderModule::CreateVideoDecoder( } } #endif - else if (TheoraDecoder::IsTheora(aParams.mConfig.mMimeType)) { + else if (TheoraDecoder::IsTheora(aParams.mConfig.mMimeType) && + StaticPrefs::media_theora_enabled()) { m = new TheoraDecoder(aParams); } diff --git a/dom/media/platforms/agnostic/bytestreams/H264.cpp b/dom/media/platforms/agnostic/bytestreams/H264.cpp index 113be67d0e..ba8d15dc40 100644 --- a/dom/media/platforms/agnostic/bytestreams/H264.cpp +++ b/dom/media/platforms/agnostic/bytestreams/H264.cpp @@ -3,16 +3,17 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "H264.h" -#include <limits> #include "AnnexB.h" #include "BitReader.h" #include "BitWriter.h" #include "BufferReader.h" #include "ByteStreamsUtils.h" #include "ByteWriter.h" +#include "MediaInfo.h" #include "mozilla/PodOperations.h" #include "mozilla/ResultExtensions.h" #include "mozilla/Try.h" +#include <limits> #define READSE(var, min, max) \ { \ diff --git a/dom/media/platforms/agnostic/bytestreams/H264.h b/dom/media/platforms/agnostic/bytestreams/H264.h index c3651d1a0f..6207a26113 100644 --- a/dom/media/platforms/agnostic/bytestreams/H264.h +++ b/dom/media/platforms/agnostic/bytestreams/H264.h @@ -6,11 +6,45 @@ #define MP4_DEMUXER_H264_H_ #include <stdint.h> -#include "DecoderData.h" +#include "ErrorList.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Result.h" +#include "mozilla/Span.h" +#include "mozilla/gfx/Point.h" #include "mozilla/gfx/Types.h" namespace mozilla { class BitReader; +class MediaByteBuffer; +class MediaRawData; + +enum H264_PROFILE { + H264_PROFILE_UNKNOWN = 0, + H264_PROFILE_BASE = 0x42, + H264_PROFILE_MAIN = 0x4D, + H264_PROFILE_EXTENDED = 0x58, + H264_PROFILE_HIGH = 0x64, +}; + +enum H264_LEVEL { + H264_LEVEL_1 = 10, + H264_LEVEL_1_b = 11, + H264_LEVEL_1_1 = 11, + H264_LEVEL_1_2 = 12, + H264_LEVEL_1_3 = 13, + H264_LEVEL_2 = 20, + H264_LEVEL_2_1 = 21, + H264_LEVEL_2_2 = 22, + H264_LEVEL_3 = 30, + H264_LEVEL_3_1 = 31, + H264_LEVEL_3_2 = 32, + H264_LEVEL_4 = 40, + H264_LEVEL_4_1 = 41, + H264_LEVEL_4_2 = 42, + H264_LEVEL_5 = 50, + H264_LEVEL_5_1 = 51, + H264_LEVEL_5_2 = 52 +}; // Spec 7.4.2.1 #define MAX_SPS_COUNT 32 diff --git a/dom/media/platforms/apple/AppleDecoderModule.cpp b/dom/media/platforms/apple/AppleDecoderModule.cpp index c54593a495..b92369601c 100644 --- a/dom/media/platforms/apple/AppleDecoderModule.cpp +++ b/dom/media/platforms/apple/AppleDecoderModule.cpp @@ -13,6 +13,7 @@ #include "MP4Decoder.h" #include "VideoUtils.h" #include "VPXDecoder.h" +#include "AOMDecoder.h" #include "mozilla/Logging.h" #include "mozilla/StaticPrefs_media.h" #include "mozilla/gfx/gfxVars.h" @@ -34,6 +35,7 @@ using media::MediaCodec; bool AppleDecoderModule::sInitialized = false; bool AppleDecoderModule::sCanUseVP9Decoder = false; +bool AppleDecoderModule::sCanUseAV1Decoder = false; /* static */ void AppleDecoderModule::Init() { @@ -45,6 +47,7 @@ void AppleDecoderModule::Init() { if (RegisterSupplementalVP9Decoder()) { sCanUseVP9Decoder = CanCreateHWDecoder(MediaCodec::VP9); } + sCanUseAV1Decoder = CanCreateHWDecoder(MediaCodec::AV1); } nsresult AppleDecoderModule::Startup() { @@ -83,7 +86,8 @@ DecodeSupportSet AppleDecoderModule::SupportsMimeType( const nsACString& aMimeType, DecoderDoctorDiagnostics* aDiagnostics) const { bool checkSupport = aMimeType.EqualsLiteral("audio/mp4a-latm") || MP4Decoder::IsH264(aMimeType) || - VPXDecoder::IsVP9(aMimeType); + VPXDecoder::IsVP9(aMimeType) || + AOMDecoder::IsAV1(aMimeType); DecodeSupportSet supportType{}; if (checkSupport) { @@ -142,6 +146,35 @@ bool AppleDecoderModule::IsVideoSupported( if (MP4Decoder::IsH264(aConfig.mMimeType)) { return true; } + if (AOMDecoder::IsAV1(aConfig.mMimeType)) { + if (!sCanUseAV1Decoder || + aOptions.contains( + CreateDecoderParams::Option::HardwareDecoderNotAllowed)) { + return false; + } + + // HW AV1 decoder only supports 8 or 10 bit color. + if (aConfig.mColorDepth != gfx::ColorDepth::COLOR_8 && + aConfig.mColorDepth != gfx::ColorDepth::COLOR_10) { + return false; + } + + if (aConfig.mColorSpace.isSome()) { + if (*aConfig.mColorSpace == gfx::YUVColorSpace::Identity) { + // HW AV1 decoder doesn't support RGB + return false; + } + } + + if (aConfig.mExtraData && aConfig.mExtraData->Length() < 2) { + return true; // Assume it's okay. + } + // top 3 bits are the profile. + int profile = aConfig.mExtraData->ElementAt(1) >> 5; + // 0 is main profile + return profile == 0; + } + if (!VPXDecoder::IsVP9(aConfig.mMimeType) || !sCanUseVP9Decoder || aOptions.contains( CreateDecoderParams::Option::HardwareDecoderNotAllowed)) { @@ -187,6 +220,20 @@ bool AppleDecoderModule::CanCreateHWDecoder(MediaCodec aCodec) { return false; } switch (aCodec) { + case MediaCodec::AV1: { + info.mMimeType = "video/av1"; + + // Build up a fake CBox + bool hasSeqHdr; + AOMDecoder::AV1SequenceInfo seqInfo; + AOMDecoder::OperatingPoint op; + seqInfo.mOperatingPoints.AppendElement(op); + seqInfo.mImage = {1920, 1080}; + AOMDecoder::WriteAV1CBox(seqInfo, info.mExtraData, hasSeqHdr); + + vtReportsSupport = VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1); + break; + } case MediaCodec::VP9: info.mMimeType = "video/vp9"; VPXDecoder::GetVPCCBox(info.mExtraData, VPXDecoder::VPXStreamInfo()); diff --git a/dom/media/platforms/apple/AppleDecoderModule.h b/dom/media/platforms/apple/AppleDecoderModule.h index f869243a5c..46b0223d75 100644 --- a/dom/media/platforms/apple/AppleDecoderModule.h +++ b/dom/media/platforms/apple/AppleDecoderModule.h @@ -39,6 +39,7 @@ class AppleDecoderModule : public PlatformDecoderModule { static void Init(); static bool sCanUseVP9Decoder; + static bool sCanUseAV1Decoder; static constexpr int kCMVideoCodecType_H264{'avc1'}; static constexpr int kCMVideoCodecType_VP9{'vp09'}; diff --git a/dom/media/platforms/apple/AppleVTDecoder.cpp b/dom/media/platforms/apple/AppleVTDecoder.cpp index ae34c2d142..6a70ed19d5 100644 --- a/dom/media/platforms/apple/AppleVTDecoder.cpp +++ b/dom/media/platforms/apple/AppleVTDecoder.cpp @@ -18,6 +18,7 @@ #include "MacIOSurfaceImage.h" #include "MediaData.h" #include "VPXDecoder.h" +#include "AOMDecoder.h" #include "VideoUtils.h" #include "gfxMacUtils.h" #include "mozilla/ArrayUtils.h" @@ -55,6 +56,7 @@ AppleVTDecoder::AppleVTDecoder(const VideoInfo& aConfig, mColorDepth(aConfig.mColorDepth), mStreamType(MP4Decoder::IsH264(aConfig.mMimeType) ? StreamType::H264 : VPXDecoder::IsVP9(aConfig.mMimeType) ? StreamType::VP9 + : AOMDecoder::IsAV1(aConfig.mMimeType) ? StreamType::AV1 : StreamType::Unknown), mTaskQueue(TaskQueue::Create( GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER), @@ -89,7 +91,10 @@ AppleVTDecoder::AppleVTDecoder(const VideoInfo& aConfig, MOZ_ASSERT(mStreamType != StreamType::Unknown); // TODO: Verify aConfig.mime_type. LOG("Creating AppleVTDecoder for %dx%d %s video", mDisplayWidth, - mDisplayHeight, mStreamType == StreamType::H264 ? "H.264" : "VP9"); + mDisplayHeight, + mStreamType == StreamType::H264 ? "H.264" + : mStreamType == StreamType::VP9 ? "VP9" + : "AV1"); } AppleVTDecoder::~AppleVTDecoder() { MOZ_COUNT_DTOR(AppleVTDecoder); } @@ -177,6 +182,9 @@ void AppleVTDecoder::ProcessDecode(MediaRawData* aSample) { case StreamType::VP9: flag |= MediaInfoFlag::VIDEO_VP9; break; + case StreamType::AV1: + flag |= MediaInfoFlag::VIDEO_AV1; + break; default: break; } @@ -377,6 +385,8 @@ nsCString AppleVTDecoder::GetCodecName() const { return "h264"_ns; case StreamType::VP9: return "vp9"_ns; + case StreamType::AV1: + return "av1"_ns; default: return "unknown"_ns; } @@ -598,13 +608,17 @@ MediaResult AppleVTDecoder::InitializeSession() { OSStatus rv; AutoCFRelease<CFDictionaryRef> extensions = CreateDecoderExtensions(); + CMVideoCodecType streamType; + if (mStreamType == StreamType::H264) { + streamType = kCMVideoCodecType_H264; + } else if (mStreamType == StreamType::VP9) { + streamType = CMVideoCodecType(AppleDecoderModule::kCMVideoCodecType_VP9); + } else { + streamType = kCMVideoCodecType_AV1; + } rv = CMVideoFormatDescriptionCreate( - kCFAllocatorDefault, - mStreamType == StreamType::H264 - ? kCMVideoCodecType_H264 - : CMVideoCodecType(AppleDecoderModule::kCMVideoCodecType_VP9), - AssertedCast<int32_t>(mPictureWidth), + kCFAllocatorDefault, streamType, AssertedCast<int32_t>(mPictureWidth), AssertedCast<int32_t>(mPictureHeight), extensions, &mFormat); if (rv != noErr) { return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, @@ -626,6 +640,7 @@ MediaResult AppleVTDecoder::InitializeSession() { &cb, &mSession); if (rv != noErr) { + LOG("AppleVTDecoder: VTDecompressionSessionCreate failed: %d", rv); return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, RESULT_DETAIL("Couldn't create decompression session!")); } @@ -656,7 +671,10 @@ CFDictionaryRef AppleVTDecoder::CreateDecoderExtensions() { AssertedCast<CFIndex>(mExtraData->Length())); const void* atomsKey[1]; - atomsKey[0] = mStreamType == StreamType::H264 ? CFSTR("avcC") : CFSTR("vpcC"); + atomsKey[0] = mStreamType == StreamType::H264 ? CFSTR("avcC") + : mStreamType == StreamType::VP9 ? CFSTR("vpcC") + : CFSTR("av1C"); + ; const void* atomsValue[] = {data}; static_assert(ArrayLength(atomsKey) == ArrayLength(atomsValue), "Non matching keys/values array size"); diff --git a/dom/media/platforms/apple/AppleVTDecoder.h b/dom/media/platforms/apple/AppleVTDecoder.h index 5b8f02b86f..a32bec112e 100644 --- a/dom/media/platforms/apple/AppleVTDecoder.h +++ b/dom/media/platforms/apple/AppleVTDecoder.h @@ -111,7 +111,7 @@ class AppleVTDecoder final : public MediaDataDecoder, CFDictionaryRef CreateDecoderSpecification(); CFDictionaryRef CreateDecoderExtensions(); - enum class StreamType { Unknown, H264, VP9 }; + enum class StreamType { Unknown, H264, VP9, AV1 }; const StreamType mStreamType; const RefPtr<TaskQueue> mTaskQueue; const uint32_t mMaxRefFrames; diff --git a/dom/media/platforms/apple/AppleVTEncoder.cpp b/dom/media/platforms/apple/AppleVTEncoder.cpp index 5ec9abebe2..c464ddd6f3 100644 --- a/dom/media/platforms/apple/AppleVTEncoder.cpp +++ b/dom/media/platforms/apple/AppleVTEncoder.cpp @@ -80,9 +80,8 @@ static bool SetConstantBitrate(VTCompressionSessionRef& aSession, } static bool SetBitrateAndMode(VTCompressionSessionRef& aSession, - MediaDataEncoder::BitrateMode aBitrateMode, - uint32_t aBitsPerSec) { - if (aBitrateMode == MediaDataEncoder::BitrateMode::Variable) { + BitrateMode aBitrateMode, uint32_t aBitsPerSec) { + if (aBitrateMode == BitrateMode::Variable) { return SetAverageBitrate(aSession, aBitsPerSec); } return SetConstantBitrate(aSession, aBitsPerSec); @@ -177,9 +176,8 @@ RefPtr<MediaDataEncoder::InitPromise> AppleVTEncoder::Init() { if (mConfig.mBitrate) { if (!SetBitrateAndMode(mSession, mConfig.mBitrateMode, mConfig.mBitrate)) { LOGE("failed to set bitrate to %d and mode to %s", mConfig.mBitrate, - mConfig.mBitrateMode == MediaDataEncoder::BitrateMode::Constant - ? "constant" - : "variable"); + mConfig.mBitrateMode == BitrateMode::Constant ? "constant" + : "variable"); return InitPromise::CreateAndReject( MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "fail to configurate bitrate"), @@ -228,26 +226,25 @@ RefPtr<MediaDataEncoder::InitPromise> AppleVTEncoder::Init() { } mError = NS_OK; - return InitPromise::CreateAndResolve(TrackInfo::TrackType::kVideoTrack, - __func__); + return InitPromise::CreateAndResolve(true, __func__); } -static Maybe<OSType> MapPixelFormat(MediaDataEncoder::PixelFormat aFormat) { +static Maybe<OSType> MapPixelFormat(dom::ImageBitmapFormat aFormat) { switch (aFormat) { - case MediaDataEncoder::PixelFormat::RGBA32: - case MediaDataEncoder::PixelFormat::BGRA32: + case dom::ImageBitmapFormat::RGBA32: + case dom::ImageBitmapFormat::BGRA32: return Some(kCVPixelFormatType_32BGRA); - case MediaDataEncoder::PixelFormat::RGB24: + case dom::ImageBitmapFormat::RGB24: return Some(kCVPixelFormatType_24RGB); - case MediaDataEncoder::PixelFormat::BGR24: + case dom::ImageBitmapFormat::BGR24: return Some(kCVPixelFormatType_24BGR); - case MediaDataEncoder::PixelFormat::GRAY8: + case dom::ImageBitmapFormat::GRAY8: return Some(kCVPixelFormatType_OneComponent8); - case MediaDataEncoder::PixelFormat::YUV444P: + case dom::ImageBitmapFormat::YUV444P: return Some(kCVPixelFormatType_444YpCbCr8); - case MediaDataEncoder::PixelFormat::YUV420P: + case dom::ImageBitmapFormat::YUV420P: return Some(kCVPixelFormatType_420YpCbCr8PlanarFullRange); - case MediaDataEncoder::PixelFormat::YUV420SP_NV12: + case dom::ImageBitmapFormat::YUV420SP_NV12: return Some(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange); default: return Nothing(); @@ -459,11 +456,10 @@ void AppleVTEncoder::OutputFrame(CMSampleBufferRef aBuffer) { LOGD("::OutputFrame"); RefPtr<MediaRawData> output(new MediaRawData()); - bool forceAvcc = false; if (mConfig.mCodecSpecific->is<H264Specific>()) { forceAvcc = mConfig.mCodecSpecific->as<H264Specific>().mFormat == - H264BitStreamFormat::AVC; + H264BitStreamFormat::AVC; } bool asAnnexB = mConfig.mUsage == Usage::Realtime && !forceAvcc; bool succeeded = WriteExtraData(output, aBuffer, asAnnexB) && @@ -590,7 +586,9 @@ AppleVTEncoder::ProcessReconfigure( mConfig.mUsage = aChange.get(); return SetRealtime(mSession, aChange.get() == Usage::Realtime); }, - [&](const ContentHintChange& aChange) -> bool { return false; }); + [&](const ContentHintChange& aChange) -> bool { return false; }, + [&](const SampleRateChange& aChange) -> bool { return false; }, + [&](const NumberOfChannelsChange& aChange) -> bool { return false; }); }; using P = MediaDataEncoder::ReconfigurationPromise; if (ok) { @@ -599,18 +597,18 @@ AppleVTEncoder::ProcessReconfigure( return P::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__); } -static size_t NumberOfPlanes(MediaDataEncoder::PixelFormat aPixelFormat) { +static size_t NumberOfPlanes(dom::ImageBitmapFormat aPixelFormat) { switch (aPixelFormat) { - case MediaDataEncoder::PixelFormat::RGBA32: - case MediaDataEncoder::PixelFormat::BGRA32: - case MediaDataEncoder::PixelFormat::RGB24: - case MediaDataEncoder::PixelFormat::BGR24: - case MediaDataEncoder::PixelFormat::GRAY8: + case dom::ImageBitmapFormat::RGBA32: + case dom::ImageBitmapFormat::BGRA32: + case dom::ImageBitmapFormat::RGB24: + case dom::ImageBitmapFormat::BGR24: + case dom::ImageBitmapFormat::GRAY8: return 1; - case MediaDataEncoder::PixelFormat::YUV444P: - case MediaDataEncoder::PixelFormat::YUV420P: + case dom::ImageBitmapFormat::YUV444P: + case dom::ImageBitmapFormat::YUV420P: return 3; - case MediaDataEncoder::PixelFormat::YUV420SP_NV12: + case dom::ImageBitmapFormat::YUV420SP_NV12: return 2; default: LOGE("Unsupported input pixel format"); diff --git a/dom/media/platforms/apple/AppleVTEncoder.h b/dom/media/platforms/apple/AppleVTEncoder.h index eded46c8c8..c7985a454c 100644 --- a/dom/media/platforms/apple/AppleVTEncoder.h +++ b/dom/media/platforms/apple/AppleVTEncoder.h @@ -24,9 +24,8 @@ class AppleVTEncoder final : public MediaDataEncoder { const RefPtr<TaskQueue>& aTaskQueue) : mConfig(aConfig), mTaskQueue(aTaskQueue), - mHardwareNotAllowed( - aConfig.mHardwarePreference == - MediaDataEncoder::HardwarePreference::RequireSoftware), + mHardwareNotAllowed(aConfig.mHardwarePreference == + HardwarePreference::RequireSoftware), mFramesCompleted(false), mError(NS_OK), mSession(nullptr) { diff --git a/dom/media/platforms/ffmpeg/FFmpegAudioEncoder.cpp b/dom/media/platforms/ffmpeg/FFmpegAudioEncoder.cpp new file mode 100644 index 0000000000..28db667732 --- /dev/null +++ b/dom/media/platforms/ffmpeg/FFmpegAudioEncoder.cpp @@ -0,0 +1,458 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FFmpegAudioEncoder.h" + +#include "FFmpegRuntimeLinker.h" +#include "FFmpegLog.h" +#include "FFmpegUtils.h" +#include "MediaData.h" + +#include "AudioSegment.h" + +namespace mozilla { + +FFmpegAudioEncoder<LIBAV_VER>::FFmpegAudioEncoder( + const FFmpegLibWrapper* aLib, AVCodecID aCodecID, + const RefPtr<TaskQueue>& aTaskQueue, const EncoderConfig& aConfig) + : FFmpegDataEncoder(aLib, aCodecID, aTaskQueue, aConfig) {} + +nsCString FFmpegAudioEncoder<LIBAV_VER>::GetDescriptionName() const { +#ifdef USING_MOZFFVPX + return "ffvpx audio encoder"_ns; +#else + const char* lib = +# if defined(MOZ_FFMPEG) + FFmpegRuntimeLinker::LinkStatusLibraryName(); +# else + "no library: ffmpeg disabled during build"; +# endif + return nsPrintfCString("ffmpeg audio encoder (%s)", lib); +#endif +} + +void FFmpegAudioEncoder<LIBAV_VER>::ResamplerDestroy::operator()( + SpeexResamplerState* aResampler) { + speex_resampler_destroy(aResampler); +} + +nsresult FFmpegAudioEncoder<LIBAV_VER>::InitSpecific() { + MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + + FFMPEG_LOG("FFmpegAudioEncoder::InitInternal"); + + // Initialize the common members of the encoder instance + AVCodec* codec = FFmpegDataEncoder<LIBAV_VER>::InitCommon(); + if (!codec) { + FFMPEG_LOG("FFmpegDataEncoder::InitCommon failed"); + return NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR; + } + + // Find a compatible input rate for the codec, update the encoder config, and + // note the rate at which this instance was configured. + mInputSampleRate = AssertedCast<int>(mConfig.mSampleRate); + if (codec->supported_samplerates) { + // Ensure the sample-rate list is sorted, iterate and either find that the + // sample rate is supported, or pick the same rate just above the audio + // input sample-rate (as to not lose information). If the audio is higher + // than the highest supported sample-rate, down-sample to the highest + // sample-rate supported by the codec. This is the case when encoding high + // samplerate audio to opus. + AutoTArray<int, 16> supportedSampleRates; + IterateZeroTerminated(codec->supported_samplerates, + [&supportedSampleRates](int aRate) mutable { + supportedSampleRates.AppendElement(aRate); + }); + supportedSampleRates.Sort(); + + for (const auto& rate : supportedSampleRates) { + if (mInputSampleRate == rate) { + mConfig.mSampleRate = rate; + break; + } + if (mInputSampleRate < rate) { + // This rate is the smallest supported rate above the content's rate. + mConfig.mSampleRate = rate; + break; + } + if (mInputSampleRate > rate) { + mConfig.mSampleRate = rate; + } + } + } + + if (mConfig.mSampleRate != AssertedCast<uint32_t>(mInputSampleRate)) { + // Need to resample to targetRate + int err; + SpeexResamplerState* resampler = speex_resampler_init( + mConfig.mNumberOfChannels, mInputSampleRate, mConfig.mSampleRate, + SPEEX_RESAMPLER_QUALITY_DEFAULT, &err); + if (!err) { + mResampler.reset(resampler); + } else { + FFMPEG_LOG( + "Error creating resampler in FFmpegAudioEncoder %dHz -> %dHz (%dch)", + mInputSampleRate, mConfig.mSampleRate, mConfig.mNumberOfChannels); + } + } + + // And now the audio-specific part + mCodecContext->sample_rate = AssertedCast<int>(mConfig.mSampleRate); + mCodecContext->channels = AssertedCast<int>(mConfig.mNumberOfChannels); + +#if LIBAVCODEC_VERSION_MAJOR >= 60 + // Gecko's ordering intentionnally matches ffmepg's ordering + mLib->av_channel_layout_default(&mCodecContext->ch_layout, + AssertedCast<int>(mCodecContext->channels)); +#endif + + switch (mConfig.mCodec) { + case CodecType::Opus: + // When using libopus, ffmpeg supports interleaved float and s16 input. + mCodecContext->sample_fmt = AV_SAMPLE_FMT_FLT; + break; + case CodecType::Vorbis: + // When using libvorbis, ffmpeg only supports planar f32 input. + mCodecContext->sample_fmt = AV_SAMPLE_FMT_FLTP; + break; + default: + MOZ_ASSERT_UNREACHABLE("Not supported"); + } + + if (mConfig.mCodec == CodecType::Opus) { + // Default is VBR + if (mConfig.mBitrateMode == BitrateMode::Constant) { + mLib->av_opt_set(mCodecContext->priv_data, "vbr", "off", 0); + } + if (mConfig.mCodecSpecific.isSome()) { + MOZ_ASSERT(mConfig.mCodecSpecific->is<OpusSpecific>()); + const OpusSpecific& specific = mConfig.mCodecSpecific->as<OpusSpecific>(); + // This attribute maps directly to complexity + mCodecContext->compression_level = specific.mComplexity; + FFMPEG_LOG("Opus complexity set to %d", specific.mComplexity); + float frameDurationMs = + AssertedCast<float>(specific.mFrameDuration) / 1000.f; + if (mLib->av_opt_set_double(mCodecContext->priv_data, "frame_duration", + frameDurationMs, 0)) { + FFMPEG_LOG("Error setting the frame duration on Opus encoder"); + return NS_ERROR_FAILURE; + } + FFMPEG_LOG("Opus frame duration set to %0.2f", frameDurationMs); + if (specific.mPacketLossPerc) { + if (mLib->av_opt_set_int( + mCodecContext->priv_data, "packet_loss", + AssertedCast<int64_t>(specific.mPacketLossPerc), 0)) { + FFMPEG_LOG("Error setting the packet loss percentage to %" PRIu64 + " on Opus encoder", + specific.mPacketLossPerc); + return NS_ERROR_FAILURE; + } + FFMPEG_LOGV("Packet loss set to %d%% in Opus encoder", + AssertedCast<int>(specific.mPacketLossPerc)); + } + if (specific.mUseInBandFEC) { + if (mLib->av_opt_set(mCodecContext->priv_data, "fec", "on", 0)) { + FFMPEG_LOG("Error %s FEC on Opus encoder", + specific.mUseInBandFEC ? "enabling" : "disabling"); + return NS_ERROR_FAILURE; + } + FFMPEG_LOGV("In-band FEC enabled for Opus encoder."); + } + if (specific.mUseDTX) { + if (mLib->av_opt_set(mCodecContext->priv_data, "dtx", "on", 0)) { + FFMPEG_LOG("Error %s DTX on Opus encoder", + specific.mUseDTX ? "enabling" : "disabling"); + return NS_ERROR_FAILURE; + } + // DTX packets are a TOC byte, and possibly one byte of length, packets + // 3 bytes and larger are to be returned. + mDtxThreshold = 3; + } + // TODO: format + // https://bugzilla.mozilla.org/show_bug.cgi?id=1876066 + } + } + // Override the time base: always the sample-rate the encoder is running at + mCodecContext->time_base = + AVRational{.num = 1, .den = mCodecContext->sample_rate}; + + MediaResult rv = FinishInitCommon(codec); + if (NS_FAILED(rv)) { + FFMPEG_LOG("FFmpeg encode initialization failure."); + return rv.Code(); + } + + return NS_OK; +} + +// avcodec_send_frame and avcodec_receive_packet were introduced in version 58. +#if LIBAVCODEC_VERSION_MAJOR >= 58 + +Result<MediaDataEncoder::EncodedData, nsresult> +FFmpegAudioEncoder<LIBAV_VER>::EncodeOnePacket(Span<float> aSamples, + media::TimeUnit aPts) { + // Allocate AVFrame. + if (!PrepareFrame()) { + FFMPEG_LOG("failed to allocate frame"); + return Err(NS_ERROR_OUT_OF_MEMORY); + } + + uint32_t frameCount = aSamples.Length() / mConfig.mNumberOfChannels; + + // This method assumes that the audio has been packetized appropriately -- + // packets smaller than the packet size are allowed when draining. + MOZ_ASSERT(AssertedCast<int>(frameCount) <= mCodecContext->frame_size); + + mFrame->channels = AssertedCast<int>(mConfig.mNumberOfChannels); + +# if LIBAVCODEC_VERSION_MAJOR >= 60 + int rv = mLib->av_channel_layout_copy(&mFrame->ch_layout, + &mCodecContext->ch_layout); + if (rv < 0) { + FFMPEG_LOG("channel layout copy error: %s", + MakeErrorString(mLib, rv).get()); + return Err(NS_ERROR_DOM_MEDIA_FATAL_ERR); + } +# endif + + mFrame->sample_rate = AssertedCast<int>(mConfig.mSampleRate); + // Not a mistake, nb_samples is per channel in ffmpeg + mFrame->nb_samples = AssertedCast<int>(frameCount); + // Audio is converted below if needed + mFrame->format = mCodecContext->sample_fmt; + // Set presentation timestamp and duration of the AVFrame. +# if LIBAVCODEC_VERSION_MAJOR >= 59 + mFrame->time_base = + AVRational{.num = 1, .den = static_cast<int>(mConfig.mSampleRate)}; +# endif + mFrame->pts = aPts.ToTicksAtRate(mConfig.mSampleRate); + mFrame->pkt_duration = frameCount; +# if LIBAVCODEC_VERSION_MAJOR >= 60 + mFrame->duration = frameCount; +# else + // Save duration in the time_base unit. + mDurationMap.Insert(mFrame->pts, mFrame->pkt_duration); +# endif + + if (int ret = mLib->av_frame_get_buffer(mFrame, 16); ret < 0) { + FFMPEG_LOG("failed to allocate frame data: %s", + MakeErrorString(mLib, ret).get()); + return Err(NS_ERROR_OUT_OF_MEMORY); + } + + // Make sure AVFrame is writable. + if (int ret = mLib->av_frame_make_writable(mFrame); ret < 0) { + FFMPEG_LOG("failed to make frame writable: %s", + MakeErrorString(mLib, ret).get()); + return Err(NS_ERROR_DOM_MEDIA_FATAL_ERR); + } + + // The input is always in f32 interleaved for now + if (mCodecContext->sample_fmt == AV_SAMPLE_FMT_FLT) { + PodCopy(reinterpret_cast<float*>(mFrame->data[0]), aSamples.data(), + aSamples.Length()); + } else { + MOZ_ASSERT(mCodecContext->sample_fmt == AV_SAMPLE_FMT_FLTP); + for (uint32_t i = 0; i < mConfig.mNumberOfChannels; i++) { + DeinterleaveAndConvertBuffer(aSamples.data(), mFrame->nb_samples, + mFrame->channels, mFrame->data); + } + } + + // Now send the AVFrame to ffmpeg for encoding, same code for audio and video. + return FFmpegDataEncoder<LIBAV_VER>::EncodeWithModernAPIs(); +} + +Result<MediaDataEncoder::EncodedData, nsresult> FFmpegAudioEncoder< + LIBAV_VER>::EncodeInputWithModernAPIs(RefPtr<const MediaData> aSample) { + MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + MOZ_ASSERT(mCodecContext); + MOZ_ASSERT(aSample); + + RefPtr<const AudioData> sample(aSample->As<AudioData>()); + + FFMPEG_LOG("Encoding %" PRIu32 " frames of audio at pts: %s", + sample->Frames(), sample->mTime.ToString().get()); + + if ((!mResampler && sample->mRate != mConfig.mSampleRate) || + (mResampler && + sample->mRate != AssertedCast<uint32_t>(mInputSampleRate)) || + sample->mChannels != mConfig.mNumberOfChannels) { + FFMPEG_LOG( + "Rate or sample-rate at the inputof the encoder different from what " + "has been configured initially, erroring out"); + return Result<MediaDataEncoder::EncodedData, nsresult>( + NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR); + } + + // ffmpeg expects exactly sized input audio packets most of the time. + // Packetization is performed if needed, and audio packets of the correct size + // are fed to ffmpeg, with timestamps extrapolated the timestamp found on + // the input MediaData. + + if (!mPacketizer) { + media::TimeUnit basePts = media::TimeUnit::Zero(mConfig.mSampleRate); + basePts += sample->mTime; + mPacketizer.emplace(mCodecContext->frame_size, sample->mChannels, + basePts.ToTicksAtRate(mConfig.mSampleRate), + mConfig.mSampleRate); + } + + if (!mFirstPacketPts.IsValid()) { + mFirstPacketPts = sample->mTime; + } + + Span<float> audio = sample->Data(); + + if (mResampler) { + // Ensure that all input frames are consumed each time by oversizing the + // output buffer. + int bufferLengthGuess = std::ceil(2. * static_cast<float>(audio.size()) * + mConfig.mSampleRate / mInputSampleRate); + mTempBuffer.SetLength(bufferLengthGuess); + uint32_t inputFrames = audio.size() / mConfig.mNumberOfChannels; + uint32_t inputFramesProcessed = inputFrames; + uint32_t outputFrames = bufferLengthGuess / mConfig.mNumberOfChannels; + DebugOnly<int> rv = speex_resampler_process_interleaved_float( + mResampler.get(), audio.data(), &inputFramesProcessed, + mTempBuffer.Elements(), &outputFrames); + audio = Span<float>(mTempBuffer.Elements(), + outputFrames * mConfig.mNumberOfChannels); + MOZ_ASSERT(inputFrames == inputFramesProcessed, + "increate the buffer to consume all input each time"); + MOZ_ASSERT(rv == RESAMPLER_ERR_SUCCESS); + } + + EncodedData output; + MediaResult rv = NS_OK; + + mPacketizer->Input(audio.data(), audio.Length() / mConfig.mNumberOfChannels); + + // Dequeue and encode each packet + while (mPacketizer->PacketsAvailable() && rv.Code() == NS_OK) { + mTempBuffer.SetLength(mCodecContext->frame_size * + mConfig.mNumberOfChannels); + media::TimeUnit pts = mPacketizer->Output(mTempBuffer.Elements()); + auto audio = Span(mTempBuffer.Elements(), mTempBuffer.Length()); + FFMPEG_LOG("Encoding %" PRIu32 " frames, pts: %s", + mPacketizer->PacketSize(), pts.ToString().get()); + auto encodeResult = EncodeOnePacket(audio, pts); + if (encodeResult.isOk()) { + output.AppendElements(std::move(encodeResult.unwrap())); + } else { + return encodeResult; + } + pts += media::TimeUnit(mPacketizer->PacketSize(), mConfig.mSampleRate); + } + return Result<MediaDataEncoder::EncodedData, nsresult>(std::move(output)); +} + +Result<MediaDataEncoder::EncodedData, nsresult> +FFmpegAudioEncoder<LIBAV_VER>::DrainWithModernAPIs() { + // If there's no packetizer, or it's empty, we can proceed immediately. + if (!mPacketizer || mPacketizer->FramesAvailable() == 0) { + return FFmpegDataEncoder<LIBAV_VER>::DrainWithModernAPIs(); + } + EncodedData output; + MediaResult rv = NS_OK; + // Dequeue and encode each packet + mTempBuffer.SetLength(mCodecContext->frame_size * + mPacketizer->ChannelCount()); + uint32_t written; + media::TimeUnit pts = mPacketizer->Drain(mTempBuffer.Elements(), written); + auto audio = + Span(mTempBuffer.Elements(), written * mPacketizer->ChannelCount()); + auto encodeResult = EncodeOnePacket(audio, pts); + if (encodeResult.isOk()) { + auto array = encodeResult.unwrap(); + output.AppendElements(std::move(array)); + } else { + return encodeResult; + } + // Now, drain the encoder + auto drainResult = FFmpegDataEncoder<LIBAV_VER>::DrainWithModernAPIs(); + if (drainResult.isOk()) { + auto array = drainResult.unwrap(); + output.AppendElements(std::move(array)); + } else { + return drainResult; + } + return Result<MediaDataEncoder::EncodedData, nsresult>(std::move(output)); +} +#endif // if LIBAVCODEC_VERSION_MAJOR >= 58 + +RefPtr<MediaRawData> FFmpegAudioEncoder<LIBAV_VER>::ToMediaRawData( + AVPacket* aPacket) { + MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + MOZ_ASSERT(aPacket); + + if (aPacket->size < mDtxThreshold) { + FFMPEG_LOG( + "DTX enabled and packet is %d bytes (threshold %d), not returning.", + aPacket->size, mDtxThreshold); + return nullptr; + } + + RefPtr<MediaRawData> data = ToMediaRawDataCommon(aPacket); + + data->mTime = media::TimeUnit(aPacket->pts, mConfig.mSampleRate); + data->mTimecode = data->mTime; + data->mDuration = + media::TimeUnit(mCodecContext->frame_size, mConfig.mSampleRate); + + // Handle encoder delay + // Tracked in https://github.com/w3c/webcodecs/issues/626 because not quite + // specced yet. + if (mFirstPacketPts > data->mTime) { + data->mOriginalPresentationWindow = + Some(media::TimeInterval{data->mTime, data->GetEndTime()}); + // Duration is likely to be ajusted when the above spec issue is fixed. For + // now, leave it as-is + // data->mDuration -= (mFirstPacketPts - data->mTime); + // if (data->mDuration.IsNegative()) { + // data->mDuration = media::TimeUnit::Zero(); + // } + data->mTime = mFirstPacketPts; + } + + if (mPacketsDelivered++ == 0) { + // Attach extradata, and the config (including any channel / samplerate + // modification to fit the encoder requirements), if needed. + if (auto r = GetExtraData(aPacket); r.isOk()) { + data->mExtraData = r.unwrap(); + } + data->mConfig = MakeUnique<EncoderConfig>(mConfig); + } + + if (data->mExtraData) { + FFMPEG_LOG( + "FFmpegAudioEncoder out: [%s,%s] (%zu bytes, extradata %zu bytes)", + data->mTime.ToString().get(), data->mDuration.ToString().get(), + data->Size(), data->mExtraData->Length()); + } else { + FFMPEG_LOG("FFmpegAudioEncoder out: [%s,%s] (%zu bytes)", + data->mTime.ToString().get(), data->mDuration.ToString().get(), + data->Size()); + } + + return data; +} + +Result<already_AddRefed<MediaByteBuffer>, nsresult> +FFmpegAudioEncoder<LIBAV_VER>::GetExtraData(AVPacket* /* aPacket */) { + if (!mCodecContext->extradata_size) { + return Err(NS_ERROR_NOT_AVAILABLE); + } + // Create extra data -- they are on the context. + auto extraData = MakeRefPtr<MediaByteBuffer>(); + extraData->SetLength(mCodecContext->extradata_size); + MOZ_ASSERT(extraData); + PodCopy(extraData->Elements(), mCodecContext->extradata, + mCodecContext->extradata_size); + return extraData.forget(); +} + +} // namespace mozilla diff --git a/dom/media/platforms/ffmpeg/FFmpegAudioEncoder.h b/dom/media/platforms/ffmpeg/FFmpegAudioEncoder.h new file mode 100644 index 0000000000..51b0bfa44e --- /dev/null +++ b/dom/media/platforms/ffmpeg/FFmpegAudioEncoder.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_MEDIA_PLATFORMS_FFMPEG_FFMPEGAUDIOENCODER_H_ +#define DOM_MEDIA_PLATFORMS_FFMPEG_FFMPEGAUDIOENCODER_H_ + +#include "FFmpegDataEncoder.h" +#include "FFmpegLibWrapper.h" +#include "PlatformEncoderModule.h" +#include "TimedPacketizer.h" + +// This must be the last header included +#include "FFmpegLibs.h" +#include "speex/speex_resampler.h" + +namespace mozilla { + +template <int V> +class FFmpegAudioEncoder : public MediaDataEncoder {}; + +template <> +class FFmpegAudioEncoder<LIBAV_VER> : public FFmpegDataEncoder<LIBAV_VER> { + public: + FFmpegAudioEncoder(const FFmpegLibWrapper* aLib, AVCodecID aCodecID, + const RefPtr<TaskQueue>& aTaskQueue, + const EncoderConfig& aConfig); + + nsCString GetDescriptionName() const override; + + protected: + // Methods only called on mTaskQueue. + virtual nsresult InitSpecific() override; +#if LIBAVCODEC_VERSION_MAJOR >= 58 + Result<EncodedData, nsresult> EncodeOnePacket(Span<float> aSamples, + media::TimeUnit aPts); + Result<EncodedData, nsresult> EncodeInputWithModernAPIs( + RefPtr<const MediaData> aSample) override; + Result<MediaDataEncoder::EncodedData, nsresult> DrainWithModernAPIs() + override; +#endif + virtual RefPtr<MediaRawData> ToMediaRawData(AVPacket* aPacket) override; + Result<already_AddRefed<MediaByteBuffer>, nsresult> GetExtraData( + AVPacket* aPacket) override; + // Most audio codecs (except PCM) require a very specific frame size. + Maybe<TimedPacketizer<float, float>> mPacketizer; + // A temporary buffer kept around for shuffling audio frames, resampling, + // packetization, etc. + nsTArray<float> mTempBuffer; + // The pts of the first packet this encoder has seen, to be able to properly + // mark encoder delay as such. + media::TimeUnit mFirstPacketPts{media::TimeUnit::Invalid()}; + struct ResamplerDestroy { + void operator()(SpeexResamplerState* aResampler); + }; + // Rate at which this instance has been configured, which might be different + // from the rate the underlying encoder is running at. + int mInputSampleRate = 0; + UniquePtr<SpeexResamplerState, ResamplerDestroy> mResampler; + uint64_t mPacketsDelivered = 0; + // Threshold under which a packet isn't returned to the encoder user, + // because it is known to be silent and DTX is enabled. + int mDtxThreshold = 0; +}; + +} // namespace mozilla + +#endif // DOM_MEDIA_PLATFORMS_FFMPEG_FFMPEGAUDIOENCODER_H_ diff --git a/dom/media/platforms/ffmpeg/FFmpegDataDecoder.cpp b/dom/media/platforms/ffmpeg/FFmpegDataDecoder.cpp index 1acfc26a4c..30422987cf 100644 --- a/dom/media/platforms/ffmpeg/FFmpegDataDecoder.cpp +++ b/dom/media/platforms/ffmpeg/FFmpegDataDecoder.cpp @@ -17,30 +17,14 @@ #include "mozilla/TaskQueue.h" #include "prsystem.h" #include "VideoUtils.h" +#include "FFmpegUtils.h" + #include "FFmpegLibs.h" namespace mozilla { StaticMutex FFmpegDataDecoder<LIBAV_VER>::sMutex; -static bool IsVideoCodec(AVCodecID aCodecID) { - switch (aCodecID) { - case AV_CODEC_ID_H264: -#if LIBAVCODEC_VERSION_MAJOR >= 54 - case AV_CODEC_ID_VP8: -#endif -#if LIBAVCODEC_VERSION_MAJOR >= 55 - case AV_CODEC_ID_VP9: -#endif -#if LIBAVCODEC_VERSION_MAJOR >= 59 - case AV_CODEC_ID_AV1: -#endif - return true; - default: - return false; - } -} - FFmpegDataDecoder<LIBAV_VER>::FFmpegDataDecoder(FFmpegLibWrapper* aLib, AVCodecID aCodecID) : mLib(aLib), diff --git a/dom/media/platforms/ffmpeg/FFmpegDataEncoder.cpp b/dom/media/platforms/ffmpeg/FFmpegDataEncoder.cpp new file mode 100644 index 0000000000..6b97a48156 --- /dev/null +++ b/dom/media/platforms/ffmpeg/FFmpegDataEncoder.cpp @@ -0,0 +1,495 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FFmpegDataEncoder.h" +#include "PlatformEncoderModule.h" + +#include <utility> + +#include "FFmpegLog.h" +#include "libavutil/error.h" +#include "mozilla/StaticMutex.h" + +#include "FFmpegUtils.h" + +namespace mozilla { + +// TODO: Remove this function and simply use `avcodec_find_encoder` once +// libopenh264 is supported. +static AVCodec* FindEncoderWithPreference(const FFmpegLibWrapper* aLib, + AVCodecID aCodecId) { + MOZ_ASSERT(aLib); + + AVCodec* codec = nullptr; + + // Prioritize libx264 for now since it's the only h264 codec we tested. + if (aCodecId == AV_CODEC_ID_H264) { + codec = aLib->avcodec_find_encoder_by_name("libx264"); + if (codec) { + FFMPEGV_LOG("Prefer libx264 for h264 codec"); + return codec; + } + FFMPEGV_LOG("Fallback to other h264 library. Fingers crossed"); + } + + return aLib->avcodec_find_encoder(aCodecId); +} + +template <> +AVCodecID GetFFmpegEncoderCodecId<LIBAV_VER>(CodecType aCodec) { +#if LIBAVCODEC_VERSION_MAJOR >= 58 + if (aCodec == CodecType::VP8) { + return AV_CODEC_ID_VP8; + } + + if (aCodec == CodecType::VP9) { + return AV_CODEC_ID_VP9; + } + +# if !defined(USING_MOZFFVPX) + if (aCodec == CodecType::H264) { + return AV_CODEC_ID_H264; + } +# endif + + if (aCodec == CodecType::AV1) { + return AV_CODEC_ID_AV1; + } + + if (aCodec == CodecType::Opus) { + return AV_CODEC_ID_OPUS; + } + + if (aCodec == CodecType::Vorbis) { + return AV_CODEC_ID_VORBIS; + } +#endif + return AV_CODEC_ID_NONE; +} + +StaticMutex FFmpegDataEncoder<LIBAV_VER>::sMutex; + +FFmpegDataEncoder<LIBAV_VER>::FFmpegDataEncoder( + const FFmpegLibWrapper* aLib, AVCodecID aCodecID, + const RefPtr<TaskQueue>& aTaskQueue, const EncoderConfig& aConfig) + : mLib(aLib), + mCodecID(aCodecID), + mTaskQueue(aTaskQueue), + mConfig(aConfig), + mCodecName(EmptyCString()), + mCodecContext(nullptr), + mFrame(nullptr), + mVideoCodec(IsVideoCodec(aCodecID)) { + MOZ_ASSERT(mLib); + MOZ_ASSERT(mTaskQueue); +#if LIBAVCODEC_VERSION_MAJOR < 58 + MOZ_CRASH("FFmpegDataEncoder needs ffmpeg 58 at least."); +#endif +}; + +RefPtr<MediaDataEncoder::InitPromise> FFmpegDataEncoder<LIBAV_VER>::Init() { + FFMPEG_LOG("Init"); + return InvokeAsync(mTaskQueue, this, __func__, + &FFmpegDataEncoder::ProcessInit); +} + +RefPtr<MediaDataEncoder::EncodePromise> FFmpegDataEncoder<LIBAV_VER>::Encode( + const MediaData* aSample) { + MOZ_ASSERT(aSample != nullptr); + + FFMPEG_LOG("Encode"); + return InvokeAsync(mTaskQueue, __func__, + [self = RefPtr<FFmpegDataEncoder<LIBAV_VER>>(this), + sample = RefPtr<const MediaData>(aSample)]() { + return self->ProcessEncode(sample); + }); +} + +RefPtr<MediaDataEncoder::ReconfigurationPromise> +FFmpegDataEncoder<LIBAV_VER>::Reconfigure( + const RefPtr<const EncoderConfigurationChangeList>& aConfigurationChanges) { + return InvokeAsync<const RefPtr<const EncoderConfigurationChangeList>>( + mTaskQueue, this, __func__, + &FFmpegDataEncoder<LIBAV_VER>::ProcessReconfigure, aConfigurationChanges); +} + +RefPtr<MediaDataEncoder::EncodePromise> FFmpegDataEncoder<LIBAV_VER>::Drain() { + FFMPEG_LOG("Drain"); + return InvokeAsync(mTaskQueue, this, __func__, + &FFmpegDataEncoder::ProcessDrain); +} + +RefPtr<ShutdownPromise> FFmpegDataEncoder<LIBAV_VER>::Shutdown() { + FFMPEG_LOG("Shutdown"); + return InvokeAsync(mTaskQueue, this, __func__, + &FFmpegDataEncoder::ProcessShutdown); +} + +RefPtr<GenericPromise> FFmpegDataEncoder<LIBAV_VER>::SetBitrate( + uint32_t aBitrate) { + FFMPEG_LOG("SetBitrate"); + return GenericPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, __func__); +} + +RefPtr<MediaDataEncoder::InitPromise> +FFmpegDataEncoder<LIBAV_VER>::ProcessInit() { + MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + + FFMPEG_LOG("ProcessInit"); + nsresult rv = InitSpecific(); + return NS_FAILED(rv) ? InitPromise::CreateAndReject(rv, __func__) + : InitPromise::CreateAndResolve(true, __func__); +} + +RefPtr<MediaDataEncoder::EncodePromise> +FFmpegDataEncoder<LIBAV_VER>::ProcessEncode(RefPtr<const MediaData> aSample) { + MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + + FFMPEG_LOG("ProcessEncode"); + +#if LIBAVCODEC_VERSION_MAJOR < 58 + // TODO(Bug 1868253): implement encode with avcodec_encode_video2(). + MOZ_CRASH("FFmpegDataEncoder needs ffmpeg 58 at least."); + return EncodePromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, __func__); +#else + + auto rv = EncodeInputWithModernAPIs(std::move(aSample)); + if (rv.isErr()) { + return EncodePromise::CreateAndReject(rv.inspectErr(), __func__); + } + + return EncodePromise::CreateAndResolve(rv.unwrap(), __func__); +#endif +} + +RefPtr<MediaDataEncoder::ReconfigurationPromise> +FFmpegDataEncoder<LIBAV_VER>::ProcessReconfigure( + const RefPtr<const EncoderConfigurationChangeList>& aConfigurationChanges) { + MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + + FFMPEG_LOG("ProcessReconfigure"); + + // Tracked in bug 1869583 -- for now this encoder always reports it cannot be + // reconfigured on the fly + return MediaDataEncoder::ReconfigurationPromise::CreateAndReject( + NS_ERROR_NOT_IMPLEMENTED, __func__); +} + +RefPtr<MediaDataEncoder::EncodePromise> +FFmpegDataEncoder<LIBAV_VER>::ProcessDrain() { + MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + + FFMPEG_LOG("ProcessDrain"); + +#if LIBAVCODEC_VERSION_MAJOR < 58 + MOZ_CRASH("FFmpegDataEncoder needs ffmpeg 58 at least."); + return EncodePromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, __func__); +#else + auto rv = DrainWithModernAPIs(); + if (rv.isErr()) { + return EncodePromise::CreateAndReject(rv.inspectErr(), __func__); + } + return EncodePromise::CreateAndResolve(rv.unwrap(), __func__); +#endif +} + +RefPtr<ShutdownPromise> FFmpegDataEncoder<LIBAV_VER>::ProcessShutdown() { + MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + + FFMPEG_LOG("ProcessShutdown"); + + ShutdownInternal(); + + // Don't shut mTaskQueue down since it's owned by others. + return ShutdownPromise::CreateAndResolve(true, __func__); +} + +AVCodec* FFmpegDataEncoder<LIBAV_VER>::InitCommon() { + MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + + FFMPEG_LOG("FFmpegDataEncoder::InitCommon"); + + AVCodec* codec = FindEncoderWithPreference(mLib, mCodecID); + if (!codec) { + FFMPEG_LOG("failed to find ffmpeg encoder for codec id %d", mCodecID); + return nullptr; + } + FFMPEG_LOG("found codec: %s", codec->name); + mCodecName = codec->name; + + ForceEnablingFFmpegDebugLogs(); + + MOZ_ASSERT(!mCodecContext); + if (!(mCodecContext = mLib->avcodec_alloc_context3(codec))) { + FFMPEG_LOG("failed to allocate ffmpeg context for codec %s", codec->name); + return nullptr; + } + + return codec; +} + +MediaResult FFmpegDataEncoder<LIBAV_VER>::FinishInitCommon(AVCodec* aCodec) { + mCodecContext->bit_rate = static_cast<FFmpegBitRate>(mConfig.mBitrate); +#if LIBAVCODEC_VERSION_MAJOR >= 60 + mCodecContext->flags |= AV_CODEC_FLAG_FRAME_DURATION; +#endif + + AVDictionary* options = nullptr; + if (int ret = OpenCodecContext(aCodec, &options); ret < 0) { + FFMPEG_LOG("failed to open %s avcodec: %s", aCodec->name, + MakeErrorString(mLib, ret).get()); + return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + RESULT_DETAIL("avcodec_open2 error")); + } + mLib->av_dict_free(&options); + + return MediaResult(NS_OK); +} + +void FFmpegDataEncoder<LIBAV_VER>::ShutdownInternal() { + MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + + FFMPEG_LOG("ShutdownInternal"); + + DestroyFrame(); + + if (mCodecContext) { + CloseCodecContext(); + mLib->av_freep(&mCodecContext); + mCodecContext = nullptr; + } +} + +int FFmpegDataEncoder<LIBAV_VER>::OpenCodecContext(const AVCodec* aCodec, + AVDictionary** aOptions) { + MOZ_ASSERT(mCodecContext); + + StaticMutexAutoLock mon(sMutex); + return mLib->avcodec_open2(mCodecContext, aCodec, aOptions); +} + +void FFmpegDataEncoder<LIBAV_VER>::CloseCodecContext() { + MOZ_ASSERT(mCodecContext); + + StaticMutexAutoLock mon(sMutex); + mLib->avcodec_close(mCodecContext); +} + +bool FFmpegDataEncoder<LIBAV_VER>::PrepareFrame() { + MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + + // TODO: Merge the duplicate part with FFmpegDataDecoder's PrepareFrame. +#if LIBAVCODEC_VERSION_MAJOR >= 55 + if (mFrame) { + mLib->av_frame_unref(mFrame); + } else { + mFrame = mLib->av_frame_alloc(); + } +#elif LIBAVCODEC_VERSION_MAJOR == 54 + if (mFrame) { + mLib->avcodec_get_frame_defaults(mFrame); + } else { + mFrame = mLib->avcodec_alloc_frame(); + } +#else + mLib->av_freep(&mFrame); + mFrame = mLib->avcodec_alloc_frame(); +#endif + return !!mFrame; +} + +void FFmpegDataEncoder<LIBAV_VER>::DestroyFrame() { + MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + if (mFrame) { +#if LIBAVCODEC_VERSION_MAJOR >= 55 + mLib->av_frame_unref(mFrame); + mLib->av_frame_free(&mFrame); +#elif LIBAVCODEC_VERSION_MAJOR == 54 + mLib->avcodec_free_frame(&mFrame); +#else + mLib->av_freep(&mFrame); +#endif + mFrame = nullptr; + } +} + +// avcodec_send_frame and avcodec_receive_packet were introduced in version 58. +#if LIBAVCODEC_VERSION_MAJOR >= 58 +Result<MediaDataEncoder::EncodedData, nsresult> +FFmpegDataEncoder<LIBAV_VER>::EncodeWithModernAPIs() { + // Initialize AVPacket. + AVPacket* pkt = mLib->av_packet_alloc(); + + if (!pkt) { + FFMPEG_LOG("failed to allocate packet"); + return Err(NS_ERROR_OUT_OF_MEMORY); + } + + auto freePacket = MakeScopeExit([this, &pkt] { mLib->av_packet_free(&pkt); }); + + // Send frame and receive packets. + if (int ret = mLib->avcodec_send_frame(mCodecContext, mFrame); ret < 0) { + // In theory, avcodec_send_frame could sent -EAGAIN to signal its internal + // buffers is full. In practice this can't happen as we only feed one frame + // at a time, and we immediately call avcodec_receive_packet right after. + // TODO: Create a NS_ERROR_DOM_MEDIA_ENCODE_ERR in ErrorList.py? + FFMPEG_LOG("avcodec_send_frame error: %s", + MakeErrorString(mLib, ret).get()); + return Err(NS_ERROR_DOM_MEDIA_FATAL_ERR); + } + + EncodedData output; + while (true) { + int ret = mLib->avcodec_receive_packet(mCodecContext, pkt); + if (ret == AVERROR(EAGAIN)) { + // The encoder is asking for more inputs. + FFMPEG_LOG("encoder is asking for more input!"); + break; + } + + if (ret < 0) { + // AVERROR_EOF is returned when the encoder has been fully flushed, but it + // shouldn't happen here. + FFMPEG_LOG("avcodec_receive_packet error: %s", + MakeErrorString(mLib, ret).get()); + return Err(NS_ERROR_DOM_MEDIA_FATAL_ERR); + } + + RefPtr<MediaRawData> d = ToMediaRawData(pkt); + mLib->av_packet_unref(pkt); + if (!d) { + // This can happen if e.g. DTX is enabled + FFMPEG_LOG("No encoded packet output"); + continue; + } + output.AppendElement(std::move(d)); + } + + FFMPEG_LOG("Got %zu encoded data", output.Length()); + return std::move(output); +} + +Result<MediaDataEncoder::EncodedData, nsresult> +FFmpegDataEncoder<LIBAV_VER>::DrainWithModernAPIs() { + MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + MOZ_ASSERT(mCodecContext); + + // TODO: Create a Result<EncodedData, nsresult> EncodeWithModernAPIs(AVFrame + // *aFrame) to merge the duplicate code below with EncodeWithModernAPIs above. + + // Initialize AVPacket. + AVPacket* pkt = mLib->av_packet_alloc(); + if (!pkt) { + FFMPEG_LOG("failed to allocate packet"); + return Err(NS_ERROR_DOM_MEDIA_FATAL_ERR); + } + auto freePacket = MakeScopeExit([this, &pkt] { mLib->av_packet_free(&pkt); }); + + // Enter draining mode by sending NULL to the avcodec_send_frame(). Note that + // this can leave the encoder in a permanent EOF state after draining. As a + // result, the encoder is unable to continue encoding. A new + // AVCodecContext/encoder creation is required if users need to encode after + // draining. + // + // TODO: Use `avcodec_flush_buffers` to drain the pending packets if + // AV_CODEC_CAP_ENCODER_FLUSH is set in mCodecContext->codec->capabilities. + if (int ret = mLib->avcodec_send_frame(mCodecContext, nullptr); ret < 0) { + if (ret == AVERROR_EOF) { + // The encoder has been flushed. Drain can be called multiple time. + FFMPEG_LOG("encoder has been flushed!"); + return EncodedData(); + } + + FFMPEG_LOG("avcodec_send_frame error: %s", + MakeErrorString(mLib, ret).get()); + return Err(NS_ERROR_DOM_MEDIA_FATAL_ERR); + } + + EncodedData output; + while (true) { + int ret = mLib->avcodec_receive_packet(mCodecContext, pkt); + if (ret == AVERROR_EOF) { + FFMPEG_LOG("encoder has no more output packet!"); + break; + } + + if (ret < 0) { + // avcodec_receive_packet should not result in a -EAGAIN once it's in + // draining mode. + FFMPEG_LOG("avcodec_receive_packet error: %s", + MakeErrorString(mLib, ret).get()); + return Err(NS_ERROR_DOM_MEDIA_FATAL_ERR); + } + + RefPtr<MediaRawData> d = ToMediaRawData(pkt); + mLib->av_packet_unref(pkt); + if (!d) { + FFMPEG_LOG("failed to create a MediaRawData from the AVPacket"); + return Err(NS_ERROR_DOM_MEDIA_FATAL_ERR); + } + output.AppendElement(std::move(d)); + } + + FFMPEG_LOG("Encoding successful, %zu packets", output.Length()); + + // TODO: Evaluate a better solution (Bug 1869466) + // TODO: Only re-create AVCodecContext when avcodec_flush_buffers is + // unavailable. + ShutdownInternal(); + nsresult r = InitSpecific(); + return NS_FAILED(r) ? Result<MediaDataEncoder::EncodedData, nsresult>( + NS_ERROR_DOM_MEDIA_FATAL_ERR) + : Result<MediaDataEncoder::EncodedData, nsresult>( + std::move(output)); +} +#endif // LIBAVCODEC_VERSION_MAJOR >= 58 + +RefPtr<MediaRawData> FFmpegDataEncoder<LIBAV_VER>::ToMediaRawDataCommon( + AVPacket* aPacket) { + MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + MOZ_ASSERT(aPacket); + + // Copy frame data from AVPacket. + auto data = MakeRefPtr<MediaRawData>(); + UniquePtr<MediaRawDataWriter> writer(data->CreateWriter()); + if (!writer->Append(aPacket->data, static_cast<size_t>(aPacket->size))) { + FFMPEG_LOG("fail to allocate MediaRawData buffer"); + return nullptr; // OOM + } + + data->mKeyframe = (aPacket->flags & AV_PKT_FLAG_KEY) != 0; + // TODO(bug 1869560): The unit of pts, dts, and duration is time_base, which + // is recommended to be the reciprocal of the frame rate, but we set it to + // microsecond for now. + data->mTime = media::TimeUnit::FromMicroseconds(aPacket->pts); +#if LIBAVCODEC_VERSION_MAJOR >= 60 + data->mDuration = media::TimeUnit::FromMicroseconds(aPacket->duration); +#else + int64_t duration; + if (mDurationMap.Find(aPacket->pts, duration)) { + data->mDuration = media::TimeUnit::FromMicroseconds(duration); + } else { + data->mDuration = media::TimeUnit::FromMicroseconds(aPacket->duration); + } +#endif + data->mTimecode = media::TimeUnit::FromMicroseconds(aPacket->dts); + + if (auto r = GetExtraData(aPacket); r.isOk()) { + data->mExtraData = r.unwrap(); + } + + return data; +} +void FFmpegDataEncoder<LIBAV_VER>::ForceEnablingFFmpegDebugLogs() { +#if DEBUG + if (!getenv("MOZ_AV_LOG_LEVEL") && + MOZ_LOG_TEST(sFFmpegVideoLog, LogLevel::Debug)) { + mLib->av_log_set_level(AV_LOG_DEBUG); + } +#endif // DEBUG +} + +} // namespace mozilla diff --git a/dom/media/platforms/ffmpeg/FFmpegDataEncoder.h b/dom/media/platforms/ffmpeg/FFmpegDataEncoder.h new file mode 100644 index 0000000000..de80ed36ca --- /dev/null +++ b/dom/media/platforms/ffmpeg/FFmpegDataEncoder.h @@ -0,0 +1,107 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_MEDIA_PLATFORMS_FFMPEG_FFMPEGDATAENCODER_H_ +#define DOM_MEDIA_PLATFORMS_FFMPEG_FFMPEGDATAENCODER_H_ + +#include "FFmpegLibWrapper.h" +#include "PlatformEncoderModule.h" +#include "SimpleMap.h" +#include "mozilla/ThreadSafety.h" + +// This must be the last header included +#include "FFmpegLibs.h" + +namespace mozilla { + +template <int V> +AVCodecID GetFFmpegEncoderCodecId(CodecType aCodec); + +template <> +AVCodecID GetFFmpegEncoderCodecId<LIBAV_VER>(CodecType aCodec); + +template <int V> +class FFmpegDataEncoder : public MediaDataEncoder {}; + +template <> +class FFmpegDataEncoder<LIBAV_VER> : public MediaDataEncoder { + using DurationMap = SimpleMap<int64_t>; + + public: + FFmpegDataEncoder(const FFmpegLibWrapper* aLib, AVCodecID aCodecID, + const RefPtr<TaskQueue>& aTaskQueue, + const EncoderConfig& aConfig); + + /* MediaDataEncoder Methods */ + // All methods run on the task queue, except for GetDescriptionName. + RefPtr<InitPromise> Init() override; + RefPtr<EncodePromise> Encode(const MediaData* aSample) override; + RefPtr<ReconfigurationPromise> Reconfigure( + const RefPtr<const EncoderConfigurationChangeList>& aConfigurationChanges) + override; + RefPtr<EncodePromise> Drain() override; + RefPtr<ShutdownPromise> Shutdown() override; + RefPtr<GenericPromise> SetBitrate(uint32_t aBitRate) override; + + protected: + // Methods only called on mTaskQueue. + RefPtr<InitPromise> ProcessInit(); + RefPtr<EncodePromise> ProcessEncode(RefPtr<const MediaData> aSample); + RefPtr<ReconfigurationPromise> ProcessReconfigure( + const RefPtr<const EncoderConfigurationChangeList>& + aConfigurationChanges); + RefPtr<EncodePromise> ProcessDrain(); + RefPtr<ShutdownPromise> ProcessShutdown(); + // Initialize the audio or video-specific members of an encoder instance. + virtual nsresult InitSpecific() = 0; + // nullptr in case of failure. This is to be called by the + // audio/video-specific InitInternal methods in the sub-class, and initializes + // the common members. + AVCodec* InitCommon(); + MediaResult FinishInitCommon(AVCodec* aCodec); + void ShutdownInternal(); + int OpenCodecContext(const AVCodec* aCodec, AVDictionary** aOptions) + MOZ_EXCLUDES(sMutex); + void CloseCodecContext() MOZ_EXCLUDES(sMutex); + bool PrepareFrame(); + void DestroyFrame(); +#if LIBAVCODEC_VERSION_MAJOR >= 58 + virtual Result<EncodedData, nsresult> EncodeInputWithModernAPIs( + RefPtr<const MediaData> aSample) = 0; + Result<EncodedData, nsresult> EncodeWithModernAPIs(); + virtual Result<EncodedData, nsresult> DrainWithModernAPIs(); +#endif + // Convert an AVPacket to a MediaRawData. This can return nullptr if a packet + // has been processed by the encoder, but is not to be returned to the caller, + // because DTX is enabled. + virtual RefPtr<MediaRawData> ToMediaRawData(AVPacket* aPacket) = 0; + RefPtr<MediaRawData> ToMediaRawDataCommon(AVPacket* aPacket); + virtual Result<already_AddRefed<MediaByteBuffer>, nsresult> GetExtraData( + AVPacket* aPacket) = 0; + void ForceEnablingFFmpegDebugLogs(); + + // This refers to a static FFmpegLibWrapper, so raw pointer is adequate. + const FFmpegLibWrapper* mLib; + const AVCodecID mCodecID; + const RefPtr<TaskQueue> mTaskQueue; + + // set in constructor, modified when parameters change + EncoderConfig mConfig; + + // mTaskQueue only. + nsCString mCodecName; + AVCodecContext* mCodecContext; + AVFrame* mFrame; + DurationMap mDurationMap; + + // Provide critical-section for open/close mCodecContext. + static StaticMutex sMutex; + const bool mVideoCodec; +}; + +} // namespace mozilla + +#endif /* DOM_MEDIA_PLATFORMS_FFMPEG_FFMPEGDATAENCODER_H_ */ diff --git a/dom/media/platforms/ffmpeg/FFmpegEncoderModule.cpp b/dom/media/platforms/ffmpeg/FFmpegEncoderModule.cpp index 42c54a48ed..b6e734268d 100644 --- a/dom/media/platforms/ffmpeg/FFmpegEncoderModule.cpp +++ b/dom/media/platforms/ffmpeg/FFmpegEncoderModule.cpp @@ -7,6 +7,7 @@ #include "FFmpegEncoderModule.h" #include "FFmpegLog.h" +#include "FFmpegAudioEncoder.h" #include "FFmpegVideoEncoder.h" // This must be the last header included @@ -44,6 +45,23 @@ already_AddRefed<MediaDataEncoder> FFmpegEncoderModule<V>::CreateVideoEncoder( return encoder.forget(); } +template <int V> +already_AddRefed<MediaDataEncoder> FFmpegEncoderModule<V>::CreateAudioEncoder( + const EncoderConfig& aConfig, const RefPtr<TaskQueue>& aTaskQueue) const { + AVCodecID codecId = GetFFmpegEncoderCodecId<V>(aConfig.mCodec); + if (codecId == AV_CODEC_ID_NONE) { + FFMPEGV_LOG("No ffmpeg encoder for %s", GetCodecTypeString(aConfig.mCodec)); + return nullptr; + } + + RefPtr<MediaDataEncoder> encoder = + new FFmpegAudioEncoder<V>(mLib, codecId, aTaskQueue, aConfig); + FFMPEGA_LOG("ffmpeg %s encoder: %s has been created", + GetCodecTypeString(aConfig.mCodec), + encoder->GetDescriptionName().get()); + return encoder.forget(); +} + template class FFmpegEncoderModule<LIBAV_VER>; } // namespace mozilla diff --git a/dom/media/platforms/ffmpeg/FFmpegEncoderModule.h b/dom/media/platforms/ffmpeg/FFmpegEncoderModule.h index 1c9e94b78f..6d0e4b1c30 100644 --- a/dom/media/platforms/ffmpeg/FFmpegEncoderModule.h +++ b/dom/media/platforms/ffmpeg/FFmpegEncoderModule.h @@ -30,6 +30,10 @@ class FFmpegEncoderModule final : public PlatformEncoderModule { const EncoderConfig& aConfig, const RefPtr<TaskQueue>& aTaskQueue) const override; + already_AddRefed<MediaDataEncoder> CreateAudioEncoder( + const EncoderConfig& aConfig, + const RefPtr<TaskQueue>& aTaskQueue) const override; + protected: explicit FFmpegEncoderModule(FFmpegLibWrapper* aLib) : mLib(aLib) { MOZ_ASSERT(mLib); diff --git a/dom/media/platforms/ffmpeg/FFmpegLibWrapper.cpp b/dom/media/platforms/ffmpeg/FFmpegLibWrapper.cpp index bfb3105a57..5fd6102a34 100644 --- a/dom/media/platforms/ffmpeg/FFmpegLibWrapper.cpp +++ b/dom/media/platforms/ffmpeg/FFmpegLibWrapper.cpp @@ -200,6 +200,7 @@ FFmpegLibWrapper::LinkResult FFmpegLibWrapper::Link() { AV_FUNC(av_image_get_buffer_size, AV_FUNC_AVUTIL_ALL) AV_FUNC_OPTION(av_channel_layout_default, AV_FUNC_AVUTIL_60) AV_FUNC_OPTION(av_channel_layout_from_mask, AV_FUNC_AVUTIL_60) + AV_FUNC_OPTION(av_channel_layout_copy, AV_FUNC_AVUTIL_60) AV_FUNC_OPTION(av_buffer_get_opaque, (AV_FUNC_AVUTIL_56 | AV_FUNC_AVUTIL_57 | AV_FUNC_AVUTIL_58 | AV_FUNC_AVUTIL_59 | AV_FUNC_AVUTIL_60)) @@ -218,6 +219,8 @@ FFmpegLibWrapper::LinkResult FFmpegLibWrapper::Link() { AV_FUNC(av_dict_set, AV_FUNC_AVUTIL_ALL) AV_FUNC(av_dict_free, AV_FUNC_AVUTIL_ALL) AV_FUNC(av_opt_set, AV_FUNC_AVUTIL_ALL) + AV_FUNC(av_opt_set_double, AV_FUNC_AVUTIL_ALL) + AV_FUNC(av_opt_set_int, AV_FUNC_AVUTIL_ALL) #ifdef MOZ_WIDGET_GTK AV_FUNC_OPTION_SILENT(avcodec_get_hw_config, diff --git a/dom/media/platforms/ffmpeg/FFmpegLibWrapper.h b/dom/media/platforms/ffmpeg/FFmpegLibWrapper.h index eacbba286a..226b4fc8cb 100644 --- a/dom/media/platforms/ffmpeg/FFmpegLibWrapper.h +++ b/dom/media/platforms/ffmpeg/FFmpegLibWrapper.h @@ -161,11 +161,16 @@ struct MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS FFmpegLibWrapper { int nb_channels); void (*av_channel_layout_from_mask)(AVChannelLayout* ch_layout, uint64_t mask); + int (*av_channel_layout_copy)(AVChannelLayout* dst, AVChannelLayout* src); int (*av_dict_set)(AVDictionary** pm, const char* key, const char* value, int flags); void (*av_dict_free)(AVDictionary** m); int (*av_opt_set)(void* obj, const char* name, const char* val, int search_flags); + int (*av_opt_set_double)(void* obj, const char* name, double val, + int search_flags); + int (*av_opt_set_int)(void* obj, const char* name, int64_t val, + int search_flags); // libavutil v55 and later only AVFrame* (*av_frame_alloc)(); diff --git a/dom/media/platforms/ffmpeg/FFmpegLog.h b/dom/media/platforms/ffmpeg/FFmpegLog.h index 45ea700936..676c5e4ba1 100644 --- a/dom/media/platforms/ffmpeg/FFmpegLog.h +++ b/dom/media/platforms/ffmpeg/FFmpegLog.h @@ -19,6 +19,9 @@ static mozilla::LazyLogModule sFFmpegAudioLog("FFmpegAudio"); # define FFMPEGV_LOG(str, ...) \ MOZ_LOG(sFFmpegVideoLog, mozilla::LogLevel::Debug, \ ("FFVPX: " str, ##__VA_ARGS__)) +# define FFMPEGA_LOG(str, ...) \ + MOZ_LOG(sFFmpegAudioLog, mozilla::LogLevel::Debug, \ + ("FFVPX: " str, ##__VA_ARGS__)) # define FFMPEGP_LOG(str, ...) \ MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, ("FFVPX: " str, ##__VA_ARGS__)) #else @@ -28,11 +31,15 @@ static mozilla::LazyLogModule sFFmpegAudioLog("FFmpegAudio"); # define FFMPEGV_LOG(str, ...) \ MOZ_LOG(sFFmpegVideoLog, mozilla::LogLevel::Debug, \ ("FFMPEG: " str, ##__VA_ARGS__)) +# define FFMPEGA_LOG(str, ...) \ + MOZ_LOG(sFFmpegAudioLog, mozilla::LogLevel::Debug, \ + ("FFMPEG: " str, ##__VA_ARGS__)) # define FFMPEGP_LOG(str, ...) \ MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, ("FFMPEG: " str, ##__VA_ARGS__)) #endif -#define FFMPEG_LOGV(...) \ - MOZ_LOG(sFFmpegVideoLog, mozilla::LogLevel::Verbose, (__VA_ARGS__)) +#define FFMPEG_LOGV(...) \ + MOZ_LOG(mVideoCodec ? sFFmpegVideoLog : sFFmpegAudioLog, \ + mozilla::LogLevel::Verbose, (__VA_ARGS__)) #endif // __FFmpegLog_h__ diff --git a/dom/media/platforms/ffmpeg/FFmpegUtils.cpp b/dom/media/platforms/ffmpeg/FFmpegUtils.cpp new file mode 100644 index 0000000000..e209306133 --- /dev/null +++ b/dom/media/platforms/ffmpeg/FFmpegUtils.cpp @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "FFmpegUtils.h" + +#include "FFmpegLibWrapper.h" +#include "mozilla/Assertions.h" +#include "nsString.h" + +namespace mozilla { + +nsCString MakeErrorString(const FFmpegLibWrapper* aLib, int aErrNum) { + MOZ_ASSERT(aLib); + + char errStr[FFmpegErrorMaxStringSize]; + aLib->av_strerror(aErrNum, errStr, FFmpegErrorMaxStringSize); + return nsCString(errStr); +} + +} // namespace mozilla diff --git a/dom/media/platforms/ffmpeg/FFmpegUtils.h b/dom/media/platforms/ffmpeg/FFmpegUtils.h new file mode 100644 index 0000000000..fe588ed14c --- /dev/null +++ b/dom/media/platforms/ffmpeg/FFmpegUtils.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_MEDIA_PLATFORMS_FFMPEG_FFMPEGUTILS_H_ +#define DOM_MEDIA_PLATFORMS_FFMPEG_FFMPEGUTILS_H_ + +#include <cstddef> +#include "nsStringFwd.h" +#include "FFmpegLibWrapper.h" + +// This must be the last header included +#include "FFmpegLibs.h" + +namespace mozilla { + +#if LIBAVCODEC_VERSION_MAJOR >= 57 +using FFmpegBitRate = int64_t; +constexpr size_t FFmpegErrorMaxStringSize = AV_ERROR_MAX_STRING_SIZE; +#else +using FFmpegBitRate = int; +constexpr size_t FFmpegErrorMaxStringSize = 64; +#endif + +nsCString MakeErrorString(const FFmpegLibWrapper* aLib, int aErrNum); + +template <typename T, typename F> +void IterateZeroTerminated(const T& aList, F&& aLambda) { + for (size_t i = 0; aList[i] != 0; i++) { + aLambda(aList[i]); + } +} + +inline bool IsVideoCodec(AVCodecID aCodecID) { + switch (aCodecID) { + case AV_CODEC_ID_H264: +#if LIBAVCODEC_VERSION_MAJOR >= 54 + case AV_CODEC_ID_VP8: +#endif +#if LIBAVCODEC_VERSION_MAJOR >= 55 + case AV_CODEC_ID_VP9: +#endif +#if LIBAVCODEC_VERSION_MAJOR >= 59 + case AV_CODEC_ID_AV1: +#endif + return true; + default: + return false; + } +} + +} // namespace mozilla + +#endif // DOM_MEDIA_PLATFORMS_FFMPEG_FFMPEGUTILS_H_ diff --git a/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.cpp b/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.cpp index 040b2e72a1..3fe46938fd 100644 --- a/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.cpp +++ b/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.cpp @@ -46,6 +46,7 @@ # define AV_PIX_FMT_YUV444P10LE PIX_FMT_YUV444P10LE # define AV_PIX_FMT_GBRP PIX_FMT_GBRP # define AV_PIX_FMT_NONE PIX_FMT_NONE +# define AV_PIX_FMT_VAAPI_VLD PIX_FMT_VAAPI_VLD #endif #if LIBAVCODEC_VERSION_MAJOR > 58 # define AV_PIX_FMT_VAAPI_VLD AV_PIX_FMT_VAAPI @@ -618,6 +619,9 @@ static gfx::ColorDepth GetColorDepth(const AVPixelFormat& aFormat) { case AV_PIX_FMT_YUV444P12LE: return gfx::ColorDepth::COLOR_12; #endif + case AV_PIX_FMT_VAAPI_VLD: + // Placeholder, it could be deeper colors + return gfx::ColorDepth::COLOR_8; default: MOZ_ASSERT_UNREACHABLE("Not supported format?"); return gfx::ColorDepth::COLOR_8; @@ -662,7 +666,7 @@ static int GetVideoBufferWrapper(struct AVCodecContext* aCodecContext, static void ReleaseVideoBufferWrapper(void* opaque, uint8_t* data) { if (opaque) { - FFMPEG_LOGV("ReleaseVideoBufferWrapper: PlanarYCbCrImage=%p", opaque); + FFMPEGV_LOG("ReleaseVideoBufferWrapper: PlanarYCbCrImage=%p", opaque); RefPtr<ImageBufferWrapper> image = static_cast<ImageBufferWrapper*>(opaque); image->ReleaseBuffer(); } @@ -1199,6 +1203,8 @@ MediaResult FFmpegVideoDecoder<LIBAV_VER>::DoDecode( return Some(DecodeStage::YUV444P); case AV_PIX_FMT_GBRP: return Some(DecodeStage::GBRP); + case AV_PIX_FMT_VAAPI_VLD: + return Some(DecodeStage::VAAPI_SURFACE); default: return Nothing(); } diff --git a/dom/media/platforms/ffmpeg/FFmpegVideoEncoder.cpp b/dom/media/platforms/ffmpeg/FFmpegVideoEncoder.cpp index a3cfdf1b1d..9d1dbcf80f 100644 --- a/dom/media/platforms/ffmpeg/FFmpegVideoEncoder.cpp +++ b/dom/media/platforms/ffmpeg/FFmpegVideoEncoder.cpp @@ -8,32 +8,21 @@ #include "BufferReader.h" #include "FFmpegLog.h" -#include "FFmpegRuntimeLinker.h" +#include "FFmpegUtils.h" #include "H264.h" #include "ImageContainer.h" #include "libavutil/error.h" #include "libavutil/pixfmt.h" -#include "mozilla/CheckedInt.h" -#include "mozilla/PodOperations.h" -#include "mozilla/StaticMutex.h" -#include "mozilla/dom/ImageBitmapBinding.h" #include "mozilla/dom/ImageUtils.h" #include "nsPrintfCString.h" #include "ImageToI420.h" #include "libyuv.h" +#include "FFmpegRuntimeLinker.h" // The ffmpeg namespace is introduced to avoid the PixelFormat's name conflicts // with MediaDataEncoder::PixelFormat in MediaDataEncoder class scope. namespace ffmpeg { -#if LIBAVCODEC_VERSION_MAJOR >= 57 -using FFmpegBitRate = int64_t; -constexpr size_t FFmpegErrorMaxStringSize = AV_ERROR_MAX_STRING_SIZE; -#else -using FFmpegBitRate = int; -constexpr size_t FFmpegErrorMaxStringSize = 64; -#endif - // TODO: WebCodecs' I420A should map to MediaDataEncoder::PixelFormat and then // to AV_PIX_FMT_YUVA420P here. #if LIBAVCODEC_VERSION_MAJOR < 54 @@ -166,9 +155,9 @@ struct VPXSVCSetting { nsTArray<uint32_t> mTargetBitrates; }; -static Maybe<VPXSVCSetting> GetVPXSVCSetting( - const MediaDataEncoder::ScalabilityMode& aMode, uint32_t aBitPerSec) { - if (aMode == MediaDataEncoder::ScalabilityMode::None) { +static Maybe<VPXSVCSetting> GetVPXSVCSetting(const ScalabilityMode& aMode, + uint32_t aBitPerSec) { + if (aMode == ScalabilityMode::None) { return Nothing(); } @@ -183,7 +172,7 @@ static Maybe<VPXSVCSetting> GetVPXSVCSetting( nsTArray<uint8_t> layerIds; nsTArray<uint8_t> rateDecimators; nsTArray<uint32_t> bitrates; - if (aMode == MediaDataEncoder::ScalabilityMode::L1T2) { + if (aMode == ScalabilityMode::L1T2) { // Two temporal layers. 0-1... // // Frame pattern: @@ -208,7 +197,7 @@ static Maybe<VPXSVCSetting> GetVPXSVCSetting( bitrates.AppendElement(kbps * 3 / 5); bitrates.AppendElement(kbps); } else { - MOZ_ASSERT(aMode == MediaDataEncoder::ScalabilityMode::L1T3); + MOZ_ASSERT(aMode == ScalabilityMode::L1T3); // Three temporal layers. 0-2-1-2... // // Frame pattern: @@ -245,59 +234,6 @@ static Maybe<VPXSVCSetting> GetVPXSVCSetting( std::move(rateDecimators), std::move(bitrates)}); } -static nsCString MakeErrorString(const FFmpegLibWrapper* aLib, int aErrNum) { - MOZ_ASSERT(aLib); - - char errStr[ffmpeg::FFmpegErrorMaxStringSize]; - aLib->av_strerror(aErrNum, errStr, ffmpeg::FFmpegErrorMaxStringSize); - return nsCString(errStr); -} - -// TODO: Remove this function and simply use `avcodec_find_encoder` once -// libopenh264 is supported. -static AVCodec* FindEncoderWithPreference(const FFmpegLibWrapper* aLib, - AVCodecID aCodecId) { - MOZ_ASSERT(aLib); - - AVCodec* codec = nullptr; - - // Prioritize libx264 for now since it's the only h264 codec we tested. - if (aCodecId == AV_CODEC_ID_H264) { - codec = aLib->avcodec_find_encoder_by_name("libx264"); - if (codec) { - FFMPEGV_LOG("Prefer libx264 for h264 codec"); - return codec; - } - } - - FFMPEGV_LOG("Fallback to other h264 library. Fingers crossed"); - return aLib->avcodec_find_encoder(aCodecId); -} - -template <> -AVCodecID GetFFmpegEncoderCodecId<LIBAV_VER>(CodecType aCodec) { -#if LIBAVCODEC_VERSION_MAJOR >= 58 - if (aCodec == CodecType::VP8) { - return AV_CODEC_ID_VP8; - } - - if (aCodec == CodecType::VP9) { - return AV_CODEC_ID_VP9; - } - -# if !defined(USING_MOZFFVPX) - if (aCodec == CodecType::H264) { - return AV_CODEC_ID_H264; - } -# endif - - if (aCodec == CodecType::AV1) { - return AV_CODEC_ID_AV1; - } -#endif - return AV_CODEC_ID_NONE; -} - uint8_t FFmpegVideoEncoder<LIBAV_VER>::SVCInfo::UpdateTemporalLayerId() { MOZ_ASSERT(!mTemporalLayerIds.IsEmpty()); @@ -306,70 +242,10 @@ uint8_t FFmpegVideoEncoder<LIBAV_VER>::SVCInfo::UpdateTemporalLayerId() { return static_cast<uint8_t>(mTemporalLayerIds[currentIndex]); } -StaticMutex FFmpegVideoEncoder<LIBAV_VER>::sMutex; - FFmpegVideoEncoder<LIBAV_VER>::FFmpegVideoEncoder( const FFmpegLibWrapper* aLib, AVCodecID aCodecID, const RefPtr<TaskQueue>& aTaskQueue, const EncoderConfig& aConfig) - : mLib(aLib), - mCodecID(aCodecID), - mTaskQueue(aTaskQueue), - mConfig(aConfig), - mCodecName(EmptyCString()), - mCodecContext(nullptr), - mFrame(nullptr), - mSVCInfo(Nothing()) { - MOZ_ASSERT(mLib); - MOZ_ASSERT(mTaskQueue); -#if LIBAVCODEC_VERSION_MAJOR < 58 - MOZ_CRASH("FFmpegVideoEncoder needs ffmpeg 58 at least."); -#endif -}; - -RefPtr<MediaDataEncoder::InitPromise> FFmpegVideoEncoder<LIBAV_VER>::Init() { - FFMPEGV_LOG("Init"); - return InvokeAsync(mTaskQueue, this, __func__, - &FFmpegVideoEncoder::ProcessInit); -} - -RefPtr<MediaDataEncoder::EncodePromise> FFmpegVideoEncoder<LIBAV_VER>::Encode( - const MediaData* aSample) { - MOZ_ASSERT(aSample != nullptr); - - FFMPEGV_LOG("Encode"); - return InvokeAsync(mTaskQueue, __func__, - [self = RefPtr<FFmpegVideoEncoder<LIBAV_VER>>(this), - sample = RefPtr<const MediaData>(aSample)]() { - return self->ProcessEncode(std::move(sample)); - }); -} - -RefPtr<MediaDataEncoder::ReconfigurationPromise> -FFmpegVideoEncoder<LIBAV_VER>::Reconfigure( - const RefPtr<const EncoderConfigurationChangeList>& aConfigurationChanges) { - return InvokeAsync<const RefPtr<const EncoderConfigurationChangeList>>( - mTaskQueue, this, __func__, - &FFmpegVideoEncoder<LIBAV_VER>::ProcessReconfigure, - aConfigurationChanges); -} - -RefPtr<MediaDataEncoder::EncodePromise> FFmpegVideoEncoder<LIBAV_VER>::Drain() { - FFMPEGV_LOG("Drain"); - return InvokeAsync(mTaskQueue, this, __func__, - &FFmpegVideoEncoder::ProcessDrain); -} - -RefPtr<ShutdownPromise> FFmpegVideoEncoder<LIBAV_VER>::Shutdown() { - FFMPEGV_LOG("Shutdown"); - return InvokeAsync(mTaskQueue, this, __func__, - &FFmpegVideoEncoder::ProcessShutdown); -} - -RefPtr<GenericPromise> FFmpegVideoEncoder<LIBAV_VER>::SetBitrate( - uint32_t aBitrate) { - FFMPEGV_LOG("SetBitrate"); - return GenericPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, __func__); -} + : FFmpegDataEncoder(aLib, aCodecID, aTaskQueue, aConfig) {} nsCString FFmpegVideoEncoder<LIBAV_VER>::GetDescriptionName() const { #ifdef USING_MOZFFVPX @@ -385,112 +261,23 @@ nsCString FFmpegVideoEncoder<LIBAV_VER>::GetDescriptionName() const { #endif } -RefPtr<MediaDataEncoder::InitPromise> -FFmpegVideoEncoder<LIBAV_VER>::ProcessInit() { +nsresult FFmpegVideoEncoder<LIBAV_VER>::InitSpecific() { MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); - FFMPEGV_LOG("ProcessInit"); - MediaResult r = InitInternal(); - return NS_FAILED(r) - ? InitPromise::CreateAndReject(r, __func__) - : InitPromise::CreateAndResolve(TrackInfo::kVideoTrack, __func__); -} - -RefPtr<MediaDataEncoder::EncodePromise> -FFmpegVideoEncoder<LIBAV_VER>::ProcessEncode(RefPtr<const MediaData> aSample) { - MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); + FFMPEGV_LOG("FFmpegVideoEncoder::InitSpecific"); - FFMPEGV_LOG("ProcessEncode"); - -#if LIBAVCODEC_VERSION_MAJOR < 58 - // TODO(Bug 1868253): implement encode with avcodec_encode_video2(). - MOZ_CRASH("FFmpegVideoEncoder needs ffmpeg 58 at least."); - return EncodePromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, __func__); -#else - RefPtr<const VideoData> sample(aSample->As<const VideoData>()); - MOZ_ASSERT(sample); - - return EncodeWithModernAPIs(sample); -#endif -} - -RefPtr<MediaDataEncoder::ReconfigurationPromise> -FFmpegVideoEncoder<LIBAV_VER>::ProcessReconfigure( - const RefPtr<const EncoderConfigurationChangeList> aConfigurationChanges) { - MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); - - FFMPEGV_LOG("ProcessReconfigure"); - - // Tracked in bug 1869583 -- for now this encoder always reports it cannot be - // reconfigured on the fly - return MediaDataEncoder::ReconfigurationPromise::CreateAndReject( - NS_ERROR_NOT_IMPLEMENTED, __func__); -} - -RefPtr<MediaDataEncoder::EncodePromise> -FFmpegVideoEncoder<LIBAV_VER>::ProcessDrain() { - MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); - - FFMPEGV_LOG("ProcessDrain"); - -#if LIBAVCODEC_VERSION_MAJOR < 58 - MOZ_CRASH("FFmpegVideoEncoder needs ffmpeg 58 at least."); - return EncodePromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, __func__); -#else - return DrainWithModernAPIs(); -#endif -} - -RefPtr<ShutdownPromise> FFmpegVideoEncoder<LIBAV_VER>::ProcessShutdown() { - MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); - - FFMPEGV_LOG("ProcessShutdown"); - - ShutdownInternal(); - - // Don't shut mTaskQueue down since it's owned by others. - return ShutdownPromise::CreateAndResolve(true, __func__); -} - -MediaResult FFmpegVideoEncoder<LIBAV_VER>::InitInternal() { - MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); - - FFMPEGV_LOG("InitInternal"); - - if (mCodecID == AV_CODEC_ID_H264) { - // H264Specific is required to get the format (avcc vs annexb). - if (!mConfig.mCodecSpecific || - !mConfig.mCodecSpecific->is<H264Specific>()) { - return MediaResult( - NS_ERROR_DOM_MEDIA_FATAL_ERR, - RESULT_DETAIL("Unable to get H264 necessary encoding info")); - } - } - - AVCodec* codec = FindEncoderWithPreference(mLib, mCodecID); + // Initialize the common members of the encoder instance + AVCodec* codec = FFmpegDataEncoder<LIBAV_VER>::InitCommon(); if (!codec) { - FFMPEGV_LOG("failed to find ffmpeg encoder for codec id %d", mCodecID); - return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, - RESULT_DETAIL("Unable to find codec")); + FFMPEGV_LOG("FFmpegDataEncoder::InitCommon failed"); + return NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR; } - FFMPEGV_LOG("find codec: %s", codec->name); - mCodecName = codec->name; - ForceEnablingFFmpegDebugLogs(); - - MOZ_ASSERT(!mCodecContext); - if (!(mCodecContext = mLib->avcodec_alloc_context3(codec))) { - FFMPEGV_LOG("failed to allocate ffmpeg context for codec %s", codec->name); - return MediaResult(NS_ERROR_OUT_OF_MEMORY, - RESULT_DETAIL("Failed to initialize ffmpeg context")); - } - - // Set up AVCodecContext. + // And now the video-specific part mCodecContext->pix_fmt = ffmpeg::FFMPEG_PIX_FMT_YUV420P; - mCodecContext->bit_rate = - static_cast<ffmpeg::FFmpegBitRate>(mConfig.mBitrate); mCodecContext->width = static_cast<int>(mConfig.mSize.width); mCodecContext->height = static_cast<int>(mConfig.mSize.height); + mCodecContext->gop_size = static_cast<int>(mConfig.mKeyframeInterval); // TODO(bug 1869560): The recommended time_base is the reciprocal of the frame // rate, but we set it to microsecond for now. mCodecContext->time_base = @@ -500,12 +287,13 @@ MediaResult FFmpegVideoEncoder<LIBAV_VER>::InitInternal() { mCodecContext->framerate = AVRational{.num = static_cast<int>(mConfig.mFramerate), .den = 1}; #endif + #if LIBAVCODEC_VERSION_MAJOR >= 60 mCodecContext->flags |= AV_CODEC_FLAG_FRAME_DURATION; #endif mCodecContext->gop_size = static_cast<int>(mConfig.mKeyframeInterval); - if (mConfig.mUsage == MediaDataEncoder::Usage::Realtime) { + if (mConfig.mUsage == Usage::Realtime) { mLib->av_opt_set(mCodecContext->priv_data, "deadline", "realtime", 0); // Explicitly ask encoder do not keep in flight at any one time for // lookahead purposes. @@ -578,14 +366,11 @@ MediaResult FFmpegVideoEncoder<LIBAV_VER>::InitInternal() { // encoder. mCodecContext->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL; - AVDictionary* options = nullptr; - if (int ret = OpenCodecContext(codec, &options); ret < 0) { - FFMPEGV_LOG("failed to open %s avcodec: %s", codec->name, - MakeErrorString(mLib, ret).get()); - return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, - RESULT_DETAIL("Unable to open avcodec")); + MediaResult rv = FinishInitCommon(codec); + if (NS_FAILED(rv)) { + FFMPEGV_LOG("FFmpeg video encoder initialization failure."); + return rv; } - mLib->av_dict_free(&options); FFMPEGV_LOG("%s has been initialized with format: %s, bitrate: %" PRIi64 ", width: %d, height: %d, time_base: %d/%d%s", @@ -595,74 +380,7 @@ MediaResult FFmpegVideoEncoder<LIBAV_VER>::InitInternal() { mCodecContext->time_base.num, mCodecContext->time_base.den, h264Log.IsEmpty() ? "" : h264Log.get()); - return MediaResult(NS_OK); -} - -void FFmpegVideoEncoder<LIBAV_VER>::ShutdownInternal() { - MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); - - FFMPEGV_LOG("ShutdownInternal"); - - DestroyFrame(); - - if (mCodecContext) { - CloseCodecContext(); - mLib->av_freep(&mCodecContext); - mCodecContext = nullptr; - } -} - -int FFmpegVideoEncoder<LIBAV_VER>::OpenCodecContext(const AVCodec* aCodec, - AVDictionary** aOptions) { - MOZ_ASSERT(mCodecContext); - - StaticMutexAutoLock mon(sMutex); - return mLib->avcodec_open2(mCodecContext, aCodec, aOptions); -} - -void FFmpegVideoEncoder<LIBAV_VER>::CloseCodecContext() { - MOZ_ASSERT(mCodecContext); - - StaticMutexAutoLock mon(sMutex); - mLib->avcodec_close(mCodecContext); -} - -bool FFmpegVideoEncoder<LIBAV_VER>::PrepareFrame() { - MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); - - // TODO: Merge the duplicate part with FFmpegDataDecoder's PrepareFrame. -#if LIBAVCODEC_VERSION_MAJOR >= 55 - if (mFrame) { - mLib->av_frame_unref(mFrame); - } else { - mFrame = mLib->av_frame_alloc(); - } -#elif LIBAVCODEC_VERSION_MAJOR == 54 - if (mFrame) { - mLib->avcodec_get_frame_defaults(mFrame); - } else { - mFrame = mLib->avcodec_alloc_frame(); - } -#else - mLib->av_freep(&mFrame); - mFrame = mLib->avcodec_alloc_frame(); -#endif - return !!mFrame; -} - -void FFmpegVideoEncoder<LIBAV_VER>::DestroyFrame() { - MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); - if (mFrame) { -#if LIBAVCODEC_VERSION_MAJOR >= 55 - mLib->av_frame_unref(mFrame); - mLib->av_frame_free(&mFrame); -#elif LIBAVCODEC_VERSION_MAJOR == 54 - mLib->avcodec_free_frame(&mFrame); -#else - mLib->av_freep(&mFrame); -#endif - mFrame = nullptr; - } + return NS_OK; } bool FFmpegVideoEncoder<LIBAV_VER>::ScaleInputFrame() { @@ -709,71 +427,62 @@ bool FFmpegVideoEncoder<LIBAV_VER>::ScaleInputFrame() { // avcodec_send_frame and avcodec_receive_packet were introduced in version 58. #if LIBAVCODEC_VERSION_MAJOR >= 58 -RefPtr<MediaDataEncoder::EncodePromise> FFmpegVideoEncoder< - LIBAV_VER>::EncodeWithModernAPIs(RefPtr<const VideoData> aSample) { +Result<MediaDataEncoder::EncodedData, nsresult> FFmpegVideoEncoder< + LIBAV_VER>::EncodeInputWithModernAPIs(RefPtr<const MediaData> aSample) { MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); MOZ_ASSERT(mCodecContext); MOZ_ASSERT(aSample); + RefPtr<const VideoData> sample(aSample->As<VideoData>()); + // Validate input. - if (!aSample->mImage) { + if (!sample->mImage) { FFMPEGV_LOG("No image"); - return EncodePromise::CreateAndReject( - MediaResult(NS_ERROR_ILLEGAL_INPUT, - RESULT_DETAIL("No image in sample")), - __func__); - } else if (aSample->mImage->GetSize().IsEmpty()) { + return Result<MediaDataEncoder::EncodedData, nsresult>( + NS_ERROR_DOM_MEDIA_FATAL_ERR); + } + if (sample->mImage->GetSize().IsEmpty()) { FFMPEGV_LOG("image width or height is invalid"); - return EncodePromise::CreateAndReject( - MediaResult(NS_ERROR_ILLEGAL_INPUT, - RESULT_DETAIL("Invalid image size")), - __func__); + return Result<MediaDataEncoder::EncodedData, nsresult>( + NS_ERROR_DOM_MEDIA_FATAL_ERR); } // Allocate AVFrame. if (!PrepareFrame()) { FFMPEGV_LOG("failed to allocate frame"); - return EncodePromise::CreateAndReject( - MediaResult(NS_ERROR_OUT_OF_MEMORY, - RESULT_DETAIL("Unable to allocate frame")), - __func__); + return Result<MediaDataEncoder::EncodedData, nsresult>( + NS_ERROR_DOM_MEDIA_FATAL_ERR); } // Set AVFrame properties for its internal data allocation. For now, we always // convert into ffmpeg's buffer. mFrame->format = ffmpeg::FFMPEG_PIX_FMT_YUV420P; - mFrame->width = static_cast<int>(aSample->mImage->GetSize().width); - mFrame->height = static_cast<int>(aSample->mImage->GetSize().height); + mFrame->width = static_cast<int>(sample->mImage->GetSize().width); + mFrame->height = static_cast<int>(sample->mImage->GetSize().height); // Allocate AVFrame data. if (int ret = mLib->av_frame_get_buffer(mFrame, 0); ret < 0) { FFMPEGV_LOG("failed to allocate frame data: %s", MakeErrorString(mLib, ret).get()); - return EncodePromise::CreateAndReject( - MediaResult(NS_ERROR_OUT_OF_MEMORY, - RESULT_DETAIL("Unable to allocate frame data")), - __func__); + return Result<MediaDataEncoder::EncodedData, nsresult>( + NS_ERROR_DOM_MEDIA_FATAL_ERR); } // Make sure AVFrame is writable. if (int ret = mLib->av_frame_make_writable(mFrame); ret < 0) { FFMPEGV_LOG("failed to make frame writable: %s", MakeErrorString(mLib, ret).get()); - return EncodePromise::CreateAndReject( - MediaResult(NS_ERROR_NOT_AVAILABLE, - RESULT_DETAIL("Unable to make frame writable")), - __func__); + return Result<MediaDataEncoder::EncodedData, nsresult>( + NS_ERROR_DOM_MEDIA_FATAL_ERR); } nsresult rv = ConvertToI420( - aSample->mImage, mFrame->data[0], mFrame->linesize[0], mFrame->data[1], + sample->mImage, mFrame->data[0], mFrame->linesize[0], mFrame->data[1], mFrame->linesize[1], mFrame->data[2], mFrame->linesize[2]); if (NS_FAILED(rv)) { FFMPEGV_LOG("Conversion error!"); - return EncodePromise::CreateAndReject( - MediaResult(NS_ERROR_ILLEGAL_INPUT, - RESULT_DETAIL("libyuv conversion error")), - __func__); + return Result<MediaDataEncoder::EncodedData, nsresult>( + NS_ERROR_DOM_MEDIA_FATAL_ERR); } // Scale the YUV input frame if needed -- the encoded frame will have the @@ -781,10 +490,8 @@ RefPtr<MediaDataEncoder::EncodePromise> FFmpegVideoEncoder< if (mFrame->width != mConfig.mSize.Width() || mFrame->height != mConfig.mSize.Height()) { if (!ScaleInputFrame()) { - return EncodePromise::CreateAndReject( - MediaResult(NS_ERROR_OUT_OF_MEMORY, - RESULT_DETAIL("libyuv scaling error")), - __func__); + return Result<MediaDataEncoder::EncodedData, nsresult>( + NS_ERROR_DOM_MEDIA_FATAL_ERR); } } @@ -805,193 +512,17 @@ RefPtr<MediaDataEncoder::EncodePromise> FFmpegVideoEncoder< # endif mFrame->pkt_duration = aSample->mDuration.ToMicroseconds(); - // Initialize AVPacket. - AVPacket* pkt = mLib->av_packet_alloc(); - - if (!pkt) { - FFMPEGV_LOG("failed to allocate packet"); - return EncodePromise::CreateAndReject( - MediaResult(NS_ERROR_OUT_OF_MEMORY, - RESULT_DETAIL("Unable to allocate packet")), - __func__); - } - - auto freePacket = MakeScopeExit([this, &pkt] { mLib->av_packet_free(&pkt); }); - - // Send frame and receive packets. - - if (int ret = mLib->avcodec_send_frame(mCodecContext, mFrame); ret < 0) { - // In theory, avcodec_send_frame could sent -EAGAIN to signal its internal - // buffers is full. In practice this can't happen as we only feed one frame - // at a time, and we immediately call avcodec_receive_packet right after. - // TODO: Create a NS_ERROR_DOM_MEDIA_ENCODE_ERR in ErrorList.py? - FFMPEGV_LOG("avcodec_send_frame error: %s", - MakeErrorString(mLib, ret).get()); - return EncodePromise::CreateAndReject( - MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, - RESULT_DETAIL("avcodec_send_frame error")), - __func__); - } - - EncodedData output; - while (true) { - int ret = mLib->avcodec_receive_packet(mCodecContext, pkt); - if (ret == AVERROR(EAGAIN)) { - // The encoder is asking for more inputs. - FFMPEGV_LOG("encoder is asking for more input!"); - break; - } - - if (ret < 0) { - // AVERROR_EOF is returned when the encoder has been fully flushed, but it - // shouldn't happen here. - FFMPEGV_LOG("avcodec_receive_packet error: %s", - MakeErrorString(mLib, ret).get()); - return EncodePromise::CreateAndReject( - MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, - RESULT_DETAIL("avcodec_receive_packet error")), - __func__); - } - - RefPtr<MediaRawData> d = ToMediaRawData(pkt); - mLib->av_packet_unref(pkt); - if (!d) { - FFMPEGV_LOG("failed to create a MediaRawData from the AVPacket"); - return EncodePromise::CreateAndReject( - MediaResult( - NS_ERROR_OUT_OF_MEMORY, - RESULT_DETAIL("Unable to get MediaRawData from AVPacket")), - __func__); - } - output.AppendElement(std::move(d)); - } - - FFMPEGV_LOG("get %zu encoded data", output.Length()); - return EncodePromise::CreateAndResolve(std::move(output), __func__); + // Now send the AVFrame to ffmpeg for encoding, same code for audio and video. + return FFmpegDataEncoder<LIBAV_VER>::EncodeWithModernAPIs(); } - -RefPtr<MediaDataEncoder::EncodePromise> -FFmpegVideoEncoder<LIBAV_VER>::DrainWithModernAPIs() { - MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); - MOZ_ASSERT(mCodecContext); - - // TODO: Create a Result<EncodedData, nsresult> EncodeWithModernAPIs(AVFrame - // *aFrame) to merge the duplicate code below with EncodeWithModernAPIs above. - - // Initialize AVPacket. - AVPacket* pkt = mLib->av_packet_alloc(); - if (!pkt) { - FFMPEGV_LOG("failed to allocate packet"); - return EncodePromise::CreateAndReject( - MediaResult(NS_ERROR_OUT_OF_MEMORY, - RESULT_DETAIL("Unable to allocate packet")), - __func__); - } - auto freePacket = MakeScopeExit([this, &pkt] { mLib->av_packet_free(&pkt); }); - - // Enter draining mode by sending NULL to the avcodec_send_frame(). Note that - // this can leave the encoder in a permanent EOF state after draining. As a - // result, the encoder is unable to continue encoding. A new - // AVCodecContext/encoder creation is required if users need to encode after - // draining. - // - // TODO: Use `avcodec_flush_buffers` to drain the pending packets if - // AV_CODEC_CAP_ENCODER_FLUSH is set in mCodecContext->codec->capabilities. - if (int ret = mLib->avcodec_send_frame(mCodecContext, nullptr); ret < 0) { - if (ret == AVERROR_EOF) { - // The encoder has been flushed. Drain can be called multiple time. - FFMPEGV_LOG("encoder has been flushed!"); - return EncodePromise::CreateAndResolve(EncodedData(), __func__); - } - - FFMPEGV_LOG("avcodec_send_frame error: %s", - MakeErrorString(mLib, ret).get()); - return EncodePromise::CreateAndReject( - MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, - RESULT_DETAIL("avcodec_send_frame error")), - __func__); - } - - EncodedData output; - while (true) { - int ret = mLib->avcodec_receive_packet(mCodecContext, pkt); - if (ret == AVERROR_EOF) { - FFMPEGV_LOG("encoder has no more output packet!"); - break; - } - - if (ret < 0) { - // avcodec_receive_packet should not result in a -EAGAIN once it's in - // draining mode. - FFMPEGV_LOG("avcodec_receive_packet error: %s", - MakeErrorString(mLib, ret).get()); - return EncodePromise::CreateAndReject( - MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, - RESULT_DETAIL("avcodec_receive_packet error")), - __func__); - } - - RefPtr<MediaRawData> d = ToMediaRawData(pkt); - mLib->av_packet_unref(pkt); - if (!d) { - FFMPEGV_LOG("failed to create a MediaRawData from the AVPacket"); - return EncodePromise::CreateAndReject( - MediaResult( - NS_ERROR_OUT_OF_MEMORY, - RESULT_DETAIL("Unable to get MediaRawData from AVPacket")), - __func__); - } - output.AppendElement(std::move(d)); - } - - FFMPEGV_LOG("get %zu encoded data", output.Length()); - - // TODO: Evaluate a better solution (Bug 1869466) - // TODO: Only re-create AVCodecContext when avcodec_flush_buffers is - // unavailable. - ShutdownInternal(); - MediaResult r = InitInternal(); - return NS_FAILED(r) - ? EncodePromise::CreateAndReject(r, __func__) - : EncodePromise::CreateAndResolve(std::move(output), __func__); -} -#endif +#endif // if LIBAVCODEC_VERSION_MAJOR >= 58 RefPtr<MediaRawData> FFmpegVideoEncoder<LIBAV_VER>::ToMediaRawData( AVPacket* aPacket) { MOZ_ASSERT(mTaskQueue->IsOnCurrentThread()); MOZ_ASSERT(aPacket); - // TODO: Do we need to check AV_PKT_FLAG_CORRUPT? - - // Copy frame data from AVPacket. - auto data = MakeRefPtr<MediaRawData>(); - UniquePtr<MediaRawDataWriter> writer(data->CreateWriter()); - if (!writer->Append(aPacket->data, static_cast<size_t>(aPacket->size))) { - FFMPEGV_LOG("fail to allocate MediaRawData buffer"); - return nullptr; // OOM - } - - data->mKeyframe = (aPacket->flags & AV_PKT_FLAG_KEY) != 0; - // TODO(bug 1869560): The unit of pts, dts, and duration is time_base, which - // is recommended to be the reciprocal of the frame rate, but we set it to - // microsecond for now. - data->mTime = media::TimeUnit::FromMicroseconds(aPacket->pts); -#if LIBAVCODEC_VERSION_MAJOR >= 60 - data->mDuration = media::TimeUnit::FromMicroseconds(aPacket->duration); -#else - int64_t duration; - if (mDurationMap.Find(aPacket->pts, duration)) { - data->mDuration = media::TimeUnit::FromMicroseconds(duration); - } else { - data->mDuration = media::TimeUnit::FromMicroseconds(aPacket->duration); - } -#endif - data->mTimecode = media::TimeUnit::FromMicroseconds(aPacket->dts); - - if (auto r = GetExtraData(aPacket); r.isOk()) { - data->mExtraData = r.unwrap(); - } + RefPtr<MediaRawData> data = ToMediaRawDataCommon(aPacket); // TODO: Is it possible to retrieve temporal layer id from underlying codec // instead? diff --git a/dom/media/platforms/ffmpeg/FFmpegVideoEncoder.h b/dom/media/platforms/ffmpeg/FFmpegVideoEncoder.h index 07c433ddd7..0ee5f52aec 100644 --- a/dom/media/platforms/ffmpeg/FFmpegVideoEncoder.h +++ b/dom/media/platforms/ffmpeg/FFmpegVideoEncoder.h @@ -7,10 +7,10 @@ #ifndef DOM_MEDIA_PLATFORMS_FFMPEG_FFMPEGVIDEOENCODER_H_ #define DOM_MEDIA_PLATFORMS_FFMPEG_FFMPEGVIDEOENCODER_H_ +#include "FFmpegDataEncoder.h" #include "FFmpegLibWrapper.h" #include "PlatformEncoderModule.h" #include "SimpleMap.h" -#include "mozilla/ThreadSafety.h" // This must be the last header included #include "FFmpegLibs.h" @@ -18,17 +18,10 @@ namespace mozilla { template <int V> -AVCodecID GetFFmpegEncoderCodecId(CodecType aCodec); - -template <> -AVCodecID GetFFmpegEncoderCodecId<LIBAV_VER>(CodecType aCodec); - -template <int V> class FFmpegVideoEncoder : public MediaDataEncoder {}; -// TODO: Bug 1860925: FFmpegDataEncoder template <> -class FFmpegVideoEncoder<LIBAV_VER> final : public MediaDataEncoder { +class FFmpegVideoEncoder<LIBAV_VER> : public FFmpegDataEncoder<LIBAV_VER> { using DurationMap = SimpleMap<int64_t>; public: @@ -36,44 +29,19 @@ class FFmpegVideoEncoder<LIBAV_VER> final : public MediaDataEncoder { const RefPtr<TaskQueue>& aTaskQueue, const EncoderConfig& aConfig); - /* MediaDataEncoder Methods */ - // All methods run on the task queue, except for GetDescriptionName. - RefPtr<InitPromise> Init() override; - RefPtr<EncodePromise> Encode(const MediaData* aSample) override; - RefPtr<ReconfigurationPromise> Reconfigure( - const RefPtr<const EncoderConfigurationChangeList>& aConfigurationChanges) - override; - RefPtr<EncodePromise> Drain() override; - RefPtr<ShutdownPromise> Shutdown() override; - RefPtr<GenericPromise> SetBitrate(uint32_t aBitRate) override; nsCString GetDescriptionName() const override; - private: - ~FFmpegVideoEncoder() = default; - + protected: // Methods only called on mTaskQueue. - RefPtr<InitPromise> ProcessInit(); - RefPtr<EncodePromise> ProcessEncode(RefPtr<const MediaData> aSample); - RefPtr<ReconfigurationPromise> ProcessReconfigure( - const RefPtr<const EncoderConfigurationChangeList> aConfigurationChanges); - RefPtr<EncodePromise> ProcessDrain(); - RefPtr<ShutdownPromise> ProcessShutdown(); - MediaResult InitInternal(); - void ShutdownInternal(); - // TODO: Share these with FFmpegDataDecoder. - int OpenCodecContext(const AVCodec* aCodec, AVDictionary** aOptions) - MOZ_EXCLUDES(sMutex); - void CloseCodecContext() MOZ_EXCLUDES(sMutex); - bool PrepareFrame(); - void DestroyFrame(); - bool ScaleInputFrame(); + virtual nsresult InitSpecific() override; #if LIBAVCODEC_VERSION_MAJOR >= 58 - RefPtr<EncodePromise> EncodeWithModernAPIs(RefPtr<const VideoData> aSample); - RefPtr<EncodePromise> DrainWithModernAPIs(); + Result<EncodedData, nsresult> EncodeInputWithModernAPIs( + RefPtr<const MediaData> aSample) override; #endif - RefPtr<MediaRawData> ToMediaRawData(AVPacket* aPacket); + bool ScaleInputFrame(); + virtual RefPtr<MediaRawData> ToMediaRawData(AVPacket* aPacket) override; Result<already_AddRefed<MediaByteBuffer>, nsresult> GetExtraData( - AVPacket* aPacket); + AVPacket* aPacket) override; void ForceEnablingFFmpegDebugLogs(); struct SVCSettings { nsTArray<uint8_t> mTemporalLayerIds; @@ -88,21 +56,6 @@ class FFmpegVideoEncoder<LIBAV_VER> final : public MediaDataEncoder { nsTArray<std::pair<nsCString, nsCString>> mSettingKeyValuePairs; }; H264Settings GetH264Settings(const H264Specific& aH264Specific); - - // This refers to a static FFmpegLibWrapper, so raw pointer is adequate. - const FFmpegLibWrapper* mLib; - const AVCodecID mCodecID; - const RefPtr<TaskQueue> mTaskQueue; - - // set in constructor, modified when parameters change - EncoderConfig mConfig; - - // mTaskQueue only. - nsCString mCodecName; - AVCodecContext* mCodecContext; - AVFrame* mFrame; - DurationMap mDurationMap; - struct SVCInfo { explicit SVCInfo(nsTArray<uint8_t>&& aTemporalLayerIds) : mTemporalLayerIds(std::move(aTemporalLayerIds)), mNextIndex(0) {} @@ -111,13 +64,9 @@ class FFmpegVideoEncoder<LIBAV_VER> final : public MediaDataEncoder { // Return the current temporal layer id and update the next. uint8_t UpdateTemporalLayerId(); }; - Maybe<SVCInfo> mSVCInfo; - - // Provide critical-section for open/close mCodecContext. - // TODO: Merge this with FFmpegDataDecoder's one. - static StaticMutex sMutex; + Maybe<SVCInfo> mSVCInfo{}; }; } // namespace mozilla -#endif /* DOM_MEDIA_PLATFORMS_FFMPEG_FFMPEGVIDEOENCODER_H_ */ +#endif // DOM_MEDIA_PLATFORMS_FFMPEG_FFMPEGVIDEOENCODER_H_ diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/moz.build b/dom/media/platforms/ffmpeg/ffmpeg57/moz.build index f26edcdc7f..db48b36f6b 100644 --- a/dom/media/platforms/ffmpeg/ffmpeg57/moz.build +++ b/dom/media/platforms/ffmpeg/ffmpeg57/moz.build @@ -6,7 +6,9 @@ UNIFIED_SOURCES += [ '../FFmpegAudioDecoder.cpp', + '../FFmpegAudioEncoder.cpp', '../FFmpegDataDecoder.cpp', + "../FFmpegDataEncoder.cpp", '../FFmpegDecoderModule.cpp', '../FFmpegEncoderModule.cpp', '../FFmpegVideoDecoder.cpp', diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/moz.build b/dom/media/platforms/ffmpeg/ffmpeg58/moz.build index a22bf98abd..12e48c44f0 100644 --- a/dom/media/platforms/ffmpeg/ffmpeg58/moz.build +++ b/dom/media/platforms/ffmpeg/ffmpeg58/moz.build @@ -6,7 +6,9 @@ UNIFIED_SOURCES += [ '../FFmpegAudioDecoder.cpp', + '../FFmpegAudioEncoder.cpp', '../FFmpegDataDecoder.cpp', + "../FFmpegDataEncoder.cpp", '../FFmpegDecoderModule.cpp', '../FFmpegEncoderModule.cpp', '../FFmpegVideoDecoder.cpp', diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/moz.build b/dom/media/platforms/ffmpeg/ffmpeg59/moz.build index e0c6c10ecd..c4f7b89951 100644 --- a/dom/media/platforms/ffmpeg/ffmpeg59/moz.build +++ b/dom/media/platforms/ffmpeg/ffmpeg59/moz.build @@ -6,7 +6,9 @@ UNIFIED_SOURCES += [ "../FFmpegAudioDecoder.cpp", + '../FFmpegAudioEncoder.cpp', "../FFmpegDataDecoder.cpp", + "../FFmpegDataEncoder.cpp", "../FFmpegDecoderModule.cpp", "../FFmpegEncoderModule.cpp", "../FFmpegVideoDecoder.cpp", diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/moz.build b/dom/media/platforms/ffmpeg/ffmpeg60/moz.build index e0c6c10ecd..c4f7b89951 100644 --- a/dom/media/platforms/ffmpeg/ffmpeg60/moz.build +++ b/dom/media/platforms/ffmpeg/ffmpeg60/moz.build @@ -6,7 +6,9 @@ UNIFIED_SOURCES += [ "../FFmpegAudioDecoder.cpp", + '../FFmpegAudioEncoder.cpp', "../FFmpegDataDecoder.cpp", + "../FFmpegDataEncoder.cpp", "../FFmpegDecoderModule.cpp", "../FFmpegEncoderModule.cpp", "../FFmpegVideoDecoder.cpp", diff --git a/dom/media/platforms/ffmpeg/ffvpx/moz.build b/dom/media/platforms/ffmpeg/ffvpx/moz.build index 97a224b08b..bc72b6d1a7 100644 --- a/dom/media/platforms/ffmpeg/ffvpx/moz.build +++ b/dom/media/platforms/ffmpeg/ffvpx/moz.build @@ -11,9 +11,12 @@ EXPORTS += [ UNIFIED_SOURCES += [ "../FFmpegAudioDecoder.cpp", + "../FFmpegAudioEncoder.cpp", "../FFmpegDataDecoder.cpp", + "../FFmpegDataEncoder.cpp", "../FFmpegDecoderModule.cpp", "../FFmpegEncoderModule.cpp", + "../FFmpegUtils.cpp", "../FFmpegVideoDecoder.cpp", "../FFmpegVideoEncoder.cpp", ] diff --git a/dom/media/platforms/ffmpeg/libav53/moz.build b/dom/media/platforms/ffmpeg/libav53/moz.build index 06b226e1f1..81b8b8dcc6 100644 --- a/dom/media/platforms/ffmpeg/libav53/moz.build +++ b/dom/media/platforms/ffmpeg/libav53/moz.build @@ -6,7 +6,9 @@ UNIFIED_SOURCES += [ '../FFmpegAudioDecoder.cpp', + '../FFmpegAudioEncoder.cpp', '../FFmpegDataDecoder.cpp', + "../FFmpegDataEncoder.cpp", '../FFmpegDecoderModule.cpp', '../FFmpegEncoderModule.cpp', '../FFmpegVideoDecoder.cpp', diff --git a/dom/media/platforms/ffmpeg/libav54/moz.build b/dom/media/platforms/ffmpeg/libav54/moz.build index 06b226e1f1..81b8b8dcc6 100644 --- a/dom/media/platforms/ffmpeg/libav54/moz.build +++ b/dom/media/platforms/ffmpeg/libav54/moz.build @@ -6,7 +6,9 @@ UNIFIED_SOURCES += [ '../FFmpegAudioDecoder.cpp', + '../FFmpegAudioEncoder.cpp', '../FFmpegDataDecoder.cpp', + "../FFmpegDataEncoder.cpp", '../FFmpegDecoderModule.cpp', '../FFmpegEncoderModule.cpp', '../FFmpegVideoDecoder.cpp', diff --git a/dom/media/platforms/ffmpeg/libav55/moz.build b/dom/media/platforms/ffmpeg/libav55/moz.build index af2d4f1831..2c3d89b9b3 100644 --- a/dom/media/platforms/ffmpeg/libav55/moz.build +++ b/dom/media/platforms/ffmpeg/libav55/moz.build @@ -6,7 +6,9 @@ UNIFIED_SOURCES += [ '../FFmpegAudioDecoder.cpp', + '../FFmpegAudioEncoder.cpp', '../FFmpegDataDecoder.cpp', + "../FFmpegDataEncoder.cpp", '../FFmpegDecoderModule.cpp', '../FFmpegEncoderModule.cpp', '../FFmpegVideoDecoder.cpp', diff --git a/dom/media/platforms/ffmpeg/moz.build b/dom/media/platforms/ffmpeg/moz.build index f519b30cec..ac78eee289 100644 --- a/dom/media/platforms/ffmpeg/moz.build +++ b/dom/media/platforms/ffmpeg/moz.build @@ -18,9 +18,7 @@ DIRS += [ "ffmpeg60", ] -UNIFIED_SOURCES += [ - "FFmpegRuntimeLinker.cpp", -] +UNIFIED_SOURCES += ["FFmpegRuntimeLinker.cpp"] if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": include("/ipc/chromium/chromium-config.mozbuild") diff --git a/dom/media/platforms/moz.build b/dom/media/platforms/moz.build index 6f71c5cc12..9a4f19aa4b 100644 --- a/dom/media/platforms/moz.build +++ b/dom/media/platforms/moz.build @@ -11,6 +11,7 @@ EXPORTS += [ "agnostic/TheoraDecoder.h", "agnostic/VPXDecoder.h", "AllocationPolicy.h", + "EncoderConfig.h", "MediaCodecsSupport.h", "MediaTelemetryConstants.h", "PDMFactory.h", @@ -32,6 +33,7 @@ UNIFIED_SOURCES += [ "agnostic/TheoraDecoder.cpp", "agnostic/VPXDecoder.cpp", "AllocationPolicy.cpp", + "EncoderConfig.cpp", "MediaCodecsSupport.cpp", "PDMFactory.cpp", "PEMFactory.cpp", diff --git a/dom/media/platforms/wmf/DXVA2Manager.cpp b/dom/media/platforms/wmf/DXVA2Manager.cpp index 36b424ab8e..9efe9dab55 100644 --- a/dom/media/platforms/wmf/DXVA2Manager.cpp +++ b/dom/media/platforms/wmf/DXVA2Manager.cpp @@ -21,6 +21,8 @@ #include "gfxCrashReporterUtils.h" #include "gfxWindowsPlatform.h" #include "mfapi.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/ClearOnShutdown.h" #include "mozilla/StaticMutex.h" #include "mozilla/StaticPrefs_media.h" #include "mozilla/Telemetry.h" @@ -122,6 +124,38 @@ using layers::ImageContainer; using namespace layers; using namespace gfx; +StaticRefPtr<ID3D11Device> sDevice; +StaticMutex sDeviceMutex; + +// We found an issue where the ID3D11VideoDecoder won't release its underlying +// resources properly if the decoder iscreated from a compositor device by +// ourselves. This problem has been observed with both VP9 and, reportedly, AV1 +// decoders, it does not seem to affect the H264 decoder, but the underlying +// decoder created by MFT seems not having this issue. +// Therefore, when checking whether we can use hardware decoding, we should use +// a non-compositor device to create a decoder in order to prevent resource +// leaking that can significantly degrade the performance. For the actual +// decoding, we will still use the compositor device if it's avaiable in order +// to avoid video copying. +static ID3D11Device* GetDeviceForDecoderCheck() { + StaticMutexAutoLock lock(sDeviceMutex); + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdown)) { + return nullptr; + } + if (!sDevice) { + sDevice = gfx::DeviceManagerDx::Get()->CreateDecoderDevice( + {DeviceManagerDx::DeviceFlag::disableDeviceReuse}); + auto clearOnShutdown = [] { ClearOnShutdown(&sDevice); }; + if (!NS_IsMainThread()) { + Unused << NS_DispatchToMainThread( + NS_NewRunnableFunction(__func__, clearOnShutdown)); + } else { + clearOnShutdown(); + } + } + return sDevice.get(); +} + void GetDXVA2ExtendedFormatFromMFMediaType(IMFMediaType* pType, DXVA2_ExtendedFormat* pFormat) { // Get the interlace mode. @@ -362,10 +396,10 @@ class D3D11DXVA2Manager : public DXVA2Manager { HRESULT CreateOutputSample(RefPtr<IMFSample>& aSample, ID3D11Texture2D* aTexture); + // This is used for check whether hw decoding is possible before using MFT for + // decoding. bool CanCreateDecoder(const D3D11_VIDEO_DECODER_DESC& aDesc) const; - already_AddRefed<ID3D11VideoDecoder> CreateDecoder( - const D3D11_VIDEO_DECODER_DESC& aDesc) const; void RefreshIMFSampleWrappers(); void ReleaseAllIMFSamples(); @@ -618,10 +652,11 @@ D3D11DXVA2Manager::InitInternal(layers::KnowsCompositor* aKnowsCompositor, mDevice = aDevice; if (!mDevice) { - bool useHardwareWebRender = - aKnowsCompositor && aKnowsCompositor->UsingHardwareWebRender(); - mDevice = - gfx::DeviceManagerDx::Get()->CreateDecoderDevice(useHardwareWebRender); + DeviceManagerDx::DeviceFlagSet flags; + if (aKnowsCompositor && aKnowsCompositor->UsingHardwareWebRender()) { + flags += DeviceManagerDx::DeviceFlag::isHardwareWebRenderInUse; + } + mDevice = gfx::DeviceManagerDx::Get()->CreateDecoderDevice(flags); if (!mDevice) { aFailureReason.AssignLiteral("Failed to create D3D11 device for decoder"); return E_FAIL; @@ -1155,20 +1190,26 @@ D3D11DXVA2Manager::ConfigureForSize(IMFMediaType* aInputType, bool D3D11DXVA2Manager::CanCreateDecoder( const D3D11_VIDEO_DECODER_DESC& aDesc) const { - RefPtr<ID3D11VideoDecoder> decoder = CreateDecoder(aDesc); - return decoder.get() != nullptr; -} + RefPtr<ID3D11Device> device = GetDeviceForDecoderCheck(); + if (!device) { + LOG("Can't create decoder due to lacking of ID3D11Device!"); + return false; + } -already_AddRefed<ID3D11VideoDecoder> D3D11DXVA2Manager::CreateDecoder( - const D3D11_VIDEO_DECODER_DESC& aDesc) const { RefPtr<ID3D11VideoDevice> videoDevice; - HRESULT hr = mDevice->QueryInterface( + HRESULT hr = device->QueryInterface( static_cast<ID3D11VideoDevice**>(getter_AddRefs(videoDevice))); - NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + if (FAILED(hr)) { + LOG("Failed to query ID3D11VideoDevice!"); + return false; + } UINT configCount = 0; hr = videoDevice->GetVideoDecoderConfigCount(&aDesc, &configCount); - NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + if (FAILED(hr)) { + LOG("Failed to get decoder config count!"); + return false; + } for (UINT i = 0; i < configCount; i++) { D3D11_VIDEO_DECODER_CONFIG config; @@ -1177,10 +1218,10 @@ already_AddRefed<ID3D11VideoDecoder> D3D11DXVA2Manager::CreateDecoder( RefPtr<ID3D11VideoDecoder> decoder; hr = videoDevice->CreateVideoDecoder(&aDesc, &config, decoder.StartAssignment()); - return decoder.forget(); + return decoder != nullptr; } } - return nullptr; + return false; } /* static */ diff --git a/dom/media/platforms/wmf/MFCDMSession.cpp b/dom/media/platforms/wmf/MFCDMSession.cpp index b797898abb..0ae4614f3b 100644 --- a/dom/media/platforms/wmf/MFCDMSession.cpp +++ b/dom/media/platforms/wmf/MFCDMSession.cpp @@ -11,6 +11,7 @@ #include "MFMediaEngineUtils.h" #include "GMPUtils.h" // ToHexString #include "mozilla/EMEUtils.h" +#include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/MediaKeyMessageEventBinding.h" #include "mozilla/dom/MediaKeyStatusMapBinding.h" #include "nsThreadUtils.h" @@ -244,7 +245,7 @@ void MFCDMSession::OnSessionKeysChange() { nsAutoCString keyIdString(ToHexString(keyId)); LOG("Append keyid-sz=%u, keyid=%s, status=%s", keyStatus.cbKeyId, keyIdString.get(), - ToMediaKeyStatusStr(ToMediaKeyStatus(keyStatus.eMediaKeyStatus))); + dom::GetEnumString(ToMediaKeyStatus(keyStatus.eMediaKeyStatus)).get()); keyInfos.AppendElement(MFCDMKeyInformation{ std::move(keyId), ToMediaKeyStatus(keyStatus.eMediaKeyStatus)}); } diff --git a/dom/media/platforms/wmf/MFMediaEngineStream.cpp b/dom/media/platforms/wmf/MFMediaEngineStream.cpp index 70ffa50142..5875b5a17c 100644 --- a/dom/media/platforms/wmf/MFMediaEngineStream.cpp +++ b/dom/media/platforms/wmf/MFMediaEngineStream.cpp @@ -11,6 +11,7 @@ #include "TimeUnits.h" #include "mozilla/ProfilerLabels.h" #include "mozilla/ProfilerMarkerTypes.h" +#include "mozilla/ScopeExit.h" #include "WMF.h" #include "WMFUtils.h" @@ -126,6 +127,13 @@ HRESULT MFMediaEngineStream::RuntimeClassInitialize( mTaskQueue = aParentSource->GetTaskQueue(); MOZ_ASSERT(mTaskQueue); mStreamId = aStreamId; + + auto errorExit = MakeScopeExit([&] { + SLOG("Failed to initialize media stream (id=%" PRIu64 ")", aStreamId); + mIsShutdown = true; + Unused << mMediaEventQueue->Shutdown(); + }); + RETURN_IF_FAILED(wmf::MFCreateEventQueue(&mMediaEventQueue)); ComPtr<IMFMediaType> mediaType; @@ -134,6 +142,7 @@ HRESULT MFMediaEngineStream::RuntimeClassInitialize( RETURN_IF_FAILED(GenerateStreamDescriptor(mediaType)); SLOG("Initialized %s (id=%" PRIu64 ", descriptorId=%lu)", GetDescriptionName().get(), aStreamId, mStreamDescriptorId); + errorExit.release(); return S_OK; } diff --git a/dom/media/platforms/wmf/WMFDataEncoderUtils.h b/dom/media/platforms/wmf/WMFDataEncoderUtils.h index 7472827b49..19f04e768f 100644 --- a/dom/media/platforms/wmf/WMFDataEncoderUtils.h +++ b/dom/media/platforms/wmf/WMFDataEncoderUtils.h @@ -32,7 +32,6 @@ static const GUID CodecToSubtype(CodecType aCodec) { case CodecType::VP9: return MFVideoFormat_VP90; default: - MOZ_ASSERT(false, "Unsupported codec"); return GUID_NULL; } } diff --git a/dom/media/platforms/wmf/WMFEncoderModule.cpp b/dom/media/platforms/wmf/WMFEncoderModule.cpp index f9f35db653..7b5af9bf50 100644 --- a/dom/media/platforms/wmf/WMFEncoderModule.cpp +++ b/dom/media/platforms/wmf/WMFEncoderModule.cpp @@ -12,6 +12,10 @@ namespace mozilla { extern LazyLogModule sPEMLog; bool WMFEncoderModule::SupportsCodec(CodecType aCodecType) const { + if (aCodecType > CodecType::_BeginAudio_ && + aCodecType < CodecType::_EndAudio_) { + return false; + } return CanCreateWMFEncoder(aCodecType); } @@ -19,6 +23,9 @@ bool WMFEncoderModule::Supports(const EncoderConfig& aConfig) const { if (!CanLikelyEncode(aConfig)) { return false; } + if (aConfig.IsAudio()) { + return false; + } return SupportsCodec(aConfig.mCodec); } diff --git a/dom/media/platforms/wmf/WMFUtils.cpp b/dom/media/platforms/wmf/WMFUtils.cpp index dda9df808e..bf5b8fe67d 100644 --- a/dom/media/platforms/wmf/WMFUtils.cpp +++ b/dom/media/platforms/wmf/WMFUtils.cpp @@ -333,7 +333,9 @@ GUID VideoMimeTypeToMediaFoundationSubtype(const nsACString& aMimeType) { if (MP4Decoder::IsHEVC(aMimeType)) { return MFVideoFormat_HEVC; } - NS_WARNING("Unsupport video mimetype"); + NS_WARNING(nsAutoCString(nsDependentCString("Unsupported video mimetype ") + + aMimeType) + .get()); return GUID_NULL; } diff --git a/dom/media/platforms/wrappers/MediaChangeMonitor.cpp b/dom/media/platforms/wrappers/MediaChangeMonitor.cpp index 46989840bf..bb7b015fab 100644 --- a/dom/media/platforms/wrappers/MediaChangeMonitor.cpp +++ b/dom/media/platforms/wrappers/MediaChangeMonitor.cpp @@ -800,6 +800,7 @@ RefPtr<ShutdownPromise> MediaChangeMonitor::ShutdownDecoder() { AssertOnThread(); mConversionRequired.reset(); if (mDecoder) { + MutexAutoLock lock(mMutex); RefPtr<MediaDataDecoder> decoder = std::move(mDecoder); return decoder->Shutdown(); } @@ -847,6 +848,7 @@ MediaChangeMonitor::CreateDecoder() { ->Then( GetCurrentSerialEventTarget(), __func__, [self = RefPtr{this}, this](RefPtr<MediaDataDecoder>&& aDecoder) { + MutexAutoLock lock(mMutex); mDecoder = std::move(aDecoder); DDLINKCHILD("decoder", mDecoder.get()); return CreateDecoderPromise::CreateAndResolve(true, __func__); @@ -1095,6 +1097,11 @@ void MediaChangeMonitor::FlushThenShutdownDecoder( ->Track(mFlushRequest); } +MediaDataDecoder* MediaChangeMonitor::GetDecoderOnNonOwnerThread() const { + MutexAutoLock lock(mMutex); + return mDecoder; +} + #undef LOG } // namespace mozilla diff --git a/dom/media/platforms/wrappers/MediaChangeMonitor.h b/dom/media/platforms/wrappers/MediaChangeMonitor.h index a3ee5b5aa0..ff4f6921f6 100644 --- a/dom/media/platforms/wrappers/MediaChangeMonitor.h +++ b/dom/media/platforms/wrappers/MediaChangeMonitor.h @@ -41,34 +41,34 @@ class MediaChangeMonitor final RefPtr<ShutdownPromise> Shutdown() override; bool IsHardwareAccelerated(nsACString& aFailureReason) const override; nsCString GetDescriptionName() const override { - if (mDecoder) { - return mDecoder->GetDescriptionName(); + if (RefPtr<MediaDataDecoder> decoder = GetDecoderOnNonOwnerThread()) { + return decoder->GetDescriptionName(); } return "MediaChangeMonitor decoder (pending)"_ns; } nsCString GetProcessName() const override { - if (mDecoder) { - return mDecoder->GetProcessName(); + if (RefPtr<MediaDataDecoder> decoder = GetDecoderOnNonOwnerThread()) { + return decoder->GetProcessName(); } return "MediaChangeMonitor"_ns; } nsCString GetCodecName() const override { - if (mDecoder) { - return mDecoder->GetCodecName(); + if (RefPtr<MediaDataDecoder> decoder = GetDecoderOnNonOwnerThread()) { + return decoder->GetCodecName(); } return "MediaChangeMonitor"_ns; } void SetSeekThreshold(const media::TimeUnit& aTime) override; bool SupportDecoderRecycling() const override { - if (mDecoder) { - return mDecoder->SupportDecoderRecycling(); + if (RefPtr<MediaDataDecoder> decoder = GetDecoderOnNonOwnerThread()) { + return decoder->SupportDecoderRecycling(); } return false; } ConversionRequired NeedsConversion() const override { - if (mDecoder) { - return mDecoder->NeedsConversion(); + if (RefPtr<MediaDataDecoder> decoder = GetDecoderOnNonOwnerThread()) { + return decoder->NeedsConversion(); } // Default so no conversion is performed. return ConversionRequired::kNeedNone; @@ -100,6 +100,9 @@ class MediaChangeMonitor final MOZ_ASSERT(!mThread || mThread->IsOnCurrentThread()); } + // This is used for getting decoder debug info on other threads. Thread-safe. + MediaDataDecoder* GetDecoderOnNonOwnerThread() const; + bool CanRecycleDecoder() const; typedef MozPromise<bool, MediaResult, true /* exclusive */> @@ -140,6 +143,13 @@ class MediaChangeMonitor final const CreateDecoderParamsForAsync mParams; // Keep any seek threshold set for after decoder creation and initialization. Maybe<media::TimeUnit> mPendingSeekThreshold; + + // This lock is used for mDecoder specifically, but it doens't need to be used + // for every places accessing mDecoder which is mostly on the owner thread. + // However, when requesting decoder debug info, it can happen on other + // threads, so we need this mutex to avoid the data race of + // creating/destroying decoder and accessing decoder's debug info. + mutable Mutex MOZ_ANNOTATED mMutex{"MediaChangeMonitor"}; }; } // namespace mozilla diff --git a/dom/media/test/320x240.ogv b/dom/media/test/320x240.ogv Binary files differdeleted file mode 100644 index 093158432a..0000000000 --- a/dom/media/test/320x240.ogv +++ /dev/null diff --git a/dom/media/test/320x240.webm b/dom/media/test/320x240.webm Binary files differnew file mode 100644 index 0000000000..16ecdbf688 --- /dev/null +++ b/dom/media/test/320x240.webm diff --git a/dom/media/test/320x240.ogv^headers^ b/dom/media/test/320x240.webm^headers^ index 4030ea1d3d..4030ea1d3d 100644 --- a/dom/media/test/320x240.ogv^headers^ +++ b/dom/media/test/320x240.webm^headers^ diff --git a/dom/media/test/448636.ogv b/dom/media/test/448636.ogv Binary files differdeleted file mode 100644 index 628df924f8..0000000000 --- a/dom/media/test/448636.ogv +++ /dev/null diff --git a/dom/media/test/bogus.ogv b/dom/media/test/bogus.ogv deleted file mode 100644 index 528ae275d0..0000000000 --- a/dom/media/test/bogus.ogv +++ /dev/null @@ -1,45 +0,0 @@ -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus -bogus bogus bogus diff --git a/dom/media/test/browser/browser_glean_first_frame_loaded_time.js b/dom/media/test/browser/browser_glean_first_frame_loaded_time.js index 1acfa9957e..2ee5274dae 100644 --- a/dom/media/test/browser/browser_glean_first_frame_loaded_time.js +++ b/dom/media/test/browser/browser_glean_first_frame_loaded_time.js @@ -17,7 +17,7 @@ const testCases = [ key_system: undefined, }, async run(tab) { - await loadVideo(tab); + await loadVideo(tab, "mozfirstframeloadedprobe"); }, }, { @@ -28,7 +28,7 @@ const testCases = [ key_system: undefined, }, async run(tab) { - await loadMseVideo(tab); + await loadMseVideo(tab, "mozfirstframeloadedprobe"); }, }, { @@ -39,11 +39,17 @@ const testCases = [ key_system: "org.w3.clearkey", }, async run(tab) { - await loadEmeVideo(tab); + await loadEmeVideo(tab, "mozfirstframeloadedprobe"); }, }, ]; +add_task(async function setTestPref() { + await SpecialPowers.pushPrefEnv({ + set: [["media.testing-only-events", true]], + }); +}); + add_task(async function testGleanMediaPlayackFirstFrameLoaded() { for (let test of testCases) { Services.fog.testResetFOG(); diff --git a/dom/media/test/browser/head.js b/dom/media/test/browser/head.js index 7ef578a804..489d107be5 100644 --- a/dom/media/test/browser/head.js +++ b/dom/media/test/browser/head.js @@ -13,60 +13,94 @@ function openTab() { // Creates and configures a video element for non-MSE playback in `tab`. Does not // start playback for the element. Returns a promise that will resolve once // the element is setup and ready for playback. -function loadVideo(tab) { - return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => { - let video = content.document.createElement("video"); - video.id = "media"; - content.document.body.appendChild(video); +function loadVideo(tab, extraEvent = undefined) { + return SpecialPowers.spawn( + tab.linkedBrowser, + [extraEvent], + async _extraEvent => { + let video = content.document.createElement("video"); + video.id = "media"; + content.document.body.appendChild(video); - video.src = "gizmo.mp4"; - video.load(); + video.src = "gizmo.mp4"; + video.load(); - info(`waiting 'loadeddata' event to ensure playback is ready`); - await new Promise(r => (video.onloadeddata = r)); - }); + info(`waiting 'loadeddata' event to ensure playback is ready`); + let promises = []; + promises.push(new Promise(r => (video.onloadeddata = r))); + if (_extraEvent != undefined) { + info( + `waiting '${_extraEvent}' event to ensure the probe has been recorded` + ); + promises.push( + new Promise(r => + video.addEventListener(_extraEvent, r, { once: true }) + ) + ); + } + await Promise.allSettled(promises); + } + ); } // Creates and configures a video element for MSE playback in `tab`. Does not // start playback for the element. Returns a promise that will resolve once // the element is setup and ready for playback. -function loadMseVideo(tab) { - return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => { - async function once(target, name) { - return new Promise(r => target.addEventListener(name, r, { once: true })); - } +function loadMseVideo(tab, extraEvent = undefined) { + return SpecialPowers.spawn( + tab.linkedBrowser, + [extraEvent], + async _extraEvent => { + async function once(target, name) { + return new Promise(r => + target.addEventListener(name, r, { once: true }) + ); + } - let video = content.document.createElement("video"); - video.id = "media"; - content.document.body.appendChild(video); + let video = content.document.createElement("video"); + video.id = "media"; + content.document.body.appendChild(video); - info(`starting setup MSE`); - const ms = new content.wrappedJSObject.MediaSource(); - video.src = content.wrappedJSObject.URL.createObjectURL(ms); - await once(ms, "sourceopen"); - const sb = ms.addSourceBuffer("video/mp4"); - const videoFile = "bipbop2s.mp4"; - let fetchResponse = await content.fetch(videoFile); - sb.appendBuffer(await fetchResponse.arrayBuffer()); - await once(sb, "updateend"); - ms.endOfStream(); - await once(ms, "sourceended"); + info(`starting setup MSE`); + const ms = new content.wrappedJSObject.MediaSource(); + video.src = content.wrappedJSObject.URL.createObjectURL(ms); + await once(ms, "sourceopen"); + const sb = ms.addSourceBuffer("video/mp4"); + const videoFile = "bipbop2s.mp4"; + let fetchResponse = await content.fetch(videoFile); + sb.appendBuffer(await fetchResponse.arrayBuffer()); + await once(sb, "updateend"); + ms.endOfStream(); + await once(ms, "sourceended"); - info(`waiting 'loadeddata' event to ensure playback is ready`); - await once(video, "loadeddata"); - }); + info(`waiting 'loadeddata' event to ensure playback is ready`); + let promises = []; + promises.push(once(video, "loadeddata")); + if (_extraEvent != undefined) { + info( + `waiting '${_extraEvent}' event to ensure the probe has been recorded` + ); + promises.push( + new Promise(r => + video.addEventListener(_extraEvent, r, { once: true }) + ) + ); + } + await Promise.allSettled(promises); + } + ); } // Creates and configures a video element for EME playback in `tab`. Does not // start playback for the element. Returns a promise that will resolve once // the element is setup and ready for playback. -function loadEmeVideo(tab) { +function loadEmeVideo(tab, extraEvent = undefined) { const emeHelperUri = gTestPath.substr(0, gTestPath.lastIndexOf("/")) + "/eme_standalone.js"; return SpecialPowers.spawn( tab.linkedBrowser, - [emeHelperUri], - async _emeHelperUri => { + [emeHelperUri, extraEvent], + async (_emeHelperUri, _extraEvent) => { async function once(target, name) { return new Promise(r => target.addEventListener(name, r, { once: true }) @@ -113,7 +147,19 @@ function loadEmeVideo(tab) { await once(ms, "sourceended"); info(`waiting 'loadeddata' event to ensure playback is ready`); - await once(video, "loadeddata"); + let promises = []; + promises.push(once(video, "loadeddata")); + if (_extraEvent != undefined) { + info( + `waiting '${_extraEvent}' event to ensure the probe has been recorded` + ); + promises.push( + new Promise(r => + video.addEventListener(_extraEvent, r, { once: true }) + ) + ); + } + await Promise.allSettled(promises); } ); } diff --git a/dom/media/test/bug482461-theora.ogv b/dom/media/test/bug482461-theora.ogv Binary files differdeleted file mode 100644 index 941b8d8efd..0000000000 --- a/dom/media/test/bug482461-theora.ogv +++ /dev/null diff --git a/dom/media/test/bug482461.ogv b/dom/media/test/bug482461.ogv Binary files differdeleted file mode 100644 index 6cf6aed330..0000000000 --- a/dom/media/test/bug482461.ogv +++ /dev/null diff --git a/dom/media/test/bug495129.ogv b/dom/media/test/bug495129.ogv Binary files differdeleted file mode 100644 index 44eb9296f5..0000000000 --- a/dom/media/test/bug495129.ogv +++ /dev/null diff --git a/dom/media/test/bug498380.ogv b/dom/media/test/bug498380.ogv Binary files differdeleted file mode 100644 index 1179ecb70a..0000000000 --- a/dom/media/test/bug498380.ogv +++ /dev/null diff --git a/dom/media/test/bug498855-1.ogv b/dom/media/test/bug498855-1.ogv Binary files differdeleted file mode 100644 index 95a524da4c..0000000000 --- a/dom/media/test/bug498855-1.ogv +++ /dev/null diff --git a/dom/media/test/bug498855-2.ogv b/dom/media/test/bug498855-2.ogv Binary files differdeleted file mode 100644 index 795a308ae1..0000000000 --- a/dom/media/test/bug498855-2.ogv +++ /dev/null diff --git a/dom/media/test/bug498855-3.ogv b/dom/media/test/bug498855-3.ogv Binary files differdeleted file mode 100644 index 714858dfed..0000000000 --- a/dom/media/test/bug498855-3.ogv +++ /dev/null diff --git a/dom/media/test/bug499519.ogv b/dom/media/test/bug499519.ogv Binary files differdeleted file mode 100644 index 62c0922d36..0000000000 --- a/dom/media/test/bug499519.ogv +++ /dev/null diff --git a/dom/media/test/bug500311.ogv b/dom/media/test/bug500311.ogv Binary files differdeleted file mode 100644 index 2cf27ef1ee..0000000000 --- a/dom/media/test/bug500311.ogv +++ /dev/null diff --git a/dom/media/test/bug504613.ogv b/dom/media/test/bug504613.ogv Binary files differdeleted file mode 100644 index 5c7fd015e9..0000000000 --- a/dom/media/test/bug504613.ogv +++ /dev/null diff --git a/dom/media/test/bug504644.ogv b/dom/media/test/bug504644.ogv Binary files differdeleted file mode 100644 index 46fb4a876b..0000000000 --- a/dom/media/test/bug504644.ogv +++ /dev/null diff --git a/dom/media/test/bug504843.ogv b/dom/media/test/bug504843.ogv Binary files differdeleted file mode 100644 index 94b4750865..0000000000 --- a/dom/media/test/bug504843.ogv +++ /dev/null diff --git a/dom/media/test/bug506094.ogv b/dom/media/test/bug506094.ogv Binary files differdeleted file mode 100644 index 142b7b9ad1..0000000000 --- a/dom/media/test/bug506094.ogv +++ /dev/null diff --git a/dom/media/test/bug516323.indexed.ogv b/dom/media/test/bug516323.indexed.ogv Binary files differdeleted file mode 100644 index 7bd76eeccc..0000000000 --- a/dom/media/test/bug516323.indexed.ogv +++ /dev/null diff --git a/dom/media/test/bug516323.ogv b/dom/media/test/bug516323.ogv Binary files differdeleted file mode 100644 index 8f2f38b983..0000000000 --- a/dom/media/test/bug516323.ogv +++ /dev/null diff --git a/dom/media/test/bug523816.ogv b/dom/media/test/bug523816.ogv Binary files differdeleted file mode 100644 index ca9a31b6da..0000000000 --- a/dom/media/test/bug523816.ogv +++ /dev/null diff --git a/dom/media/test/bug556821.ogv b/dom/media/test/bug556821.ogv Binary files differdeleted file mode 100644 index 8d76fee45e..0000000000 --- a/dom/media/test/bug556821.ogv +++ /dev/null diff --git a/dom/media/test/bug557094.ogv b/dom/media/test/bug557094.ogv Binary files differdeleted file mode 100644 index b4fc0799a6..0000000000 --- a/dom/media/test/bug557094.ogv +++ /dev/null diff --git a/dom/media/test/can_play_type_ogg.js b/dom/media/test/can_play_type_ogg.js index 6572ab7c0f..e2c974d601 100644 --- a/dom/media/test/can_play_type_ogg.js +++ b/dom/media/test/can_play_type_ogg.js @@ -5,18 +5,23 @@ function check_ogg(v, enabled, finish) { function basic_test() { return new Promise(function (resolve) { - // Ogg types - check("video/ogg", "maybe"); + if (SpecialPowers.getBoolPref("media.theora.enabled")) { + check("video/ogg", "maybe"); + check("video/ogg; codecs=vorbis", "probably"); + check("video/ogg; codecs=vorbis,theora", "probably"); + check('video/ogg; codecs="vorbis, theora"', "probably"); + check("video/ogg; codecs=theora", "probably"); + } else { + check("video/ogg", ""); + check("video/ogg; codecs=vorbis", ""); + check("video/ogg; codecs=vorbis,theora", ""); + check('video/ogg; codecs="vorbis, theora"', ""); + check("video/ogg; codecs=theora", ""); + } check("audio/ogg", "maybe"); check("application/ogg", "maybe"); - // Supported Ogg codecs check("audio/ogg; codecs=vorbis", "probably"); - check("video/ogg; codecs=vorbis", "probably"); - check("video/ogg; codecs=vorbis,theora", "probably"); - check('video/ogg; codecs="vorbis, theora"', "probably"); - check("video/ogg; codecs=theora", "probably"); - resolve(); }); } diff --git a/dom/media/test/chained-video.ogv b/dom/media/test/chained-video.ogv Binary files differdeleted file mode 100644 index a6288ef6c9..0000000000 --- a/dom/media/test/chained-video.ogv +++ /dev/null diff --git a/dom/media/test/crashtests/576612-1.html b/dom/media/test/crashtests/576612-1.html deleted file mode 100644 index 04f993e780..0000000000 --- a/dom/media/test/crashtests/576612-1.html +++ /dev/null @@ -1,15 +0,0 @@ -<html xmlns="http://www.w3.org/1999/xhtml"> -<head> -<script> -function boom() -{ - - var v = document.getElementById("v"); - v.src = "data:text/plain,_"; - document.documentElement.appendChild(v); - -} -</script> -</head> -<body onload="boom();"><video id="v" src="data:video/ogg;codecs="theora,vorbis",1"></video></body> -</html> diff --git a/dom/media/test/make-headers.sh b/dom/media/test/make-headers.sh index 35d9bd90f8..d2ac0610e2 100644 --- a/dom/media/test/make-headers.sh +++ b/dom/media/test/make-headers.sh @@ -8,9 +8,9 @@ # requests is not interferred with by Necko's cache. See bug 977398 # for details. Necko will fix this in bug 977314. -FILES=(`ls *.ogg *.ogv *.webm *.mp3 *.opus *.mp4 *.m4s *.wav`) +FILES=(`ls *.ogg *.webm *.mp3 *.opus *.mp4 *.m4s *.wav`) -rm -f *.ogg^headers^ *.ogv^headers^ *.webm^headers^ *.mp3^headers^ *.opus^headers^ *.mp4^headers^ *.m4s^headers^ *.wav^headers^ +rm -f *.ogg^headers^ *.webm^headers^ *.mp3^headers^ *.opus^headers^ *.mp4^headers^ *.m4s^headers^ *.wav^headers^ for i in "${FILES[@]}" do diff --git a/dom/media/test/manifest.js b/dom/media/test/manifest.js index c357309021..686e5b07ed 100644 --- a/dom/media/test/manifest.js +++ b/dom/media/test/manifest.js @@ -52,14 +52,6 @@ var gSmallTests = [ { name: "small-shot.flac", type: "audio/flac", duration: 0.197 }, { name: "r11025_s16_c1-short.wav", type: "audio/x-wav", duration: 0.37 }, { - name: "320x240.ogv", - type: "video/ogg", - width: 320, - height: 240, - duration: 0.266, - contentDuration: 0.133, - }, - { name: "seek-short.webm", type: "video/webm", width: 320, @@ -94,7 +86,6 @@ var gFrameCountTests = [ { name: "gizmo.mp4", type: "video/mp4", totalFrameCount: 166 }, { name: "seek-short.webm", type: "video/webm", totalFrameCount: 8 }, { name: "seek.webm", type: "video/webm", totalFrameCount: 120 }, - { name: "320x240.ogv", type: "video/ogg", totalFrameCount: 8 }, { name: "av1.mp4", type: "video/mp4", totalFrameCount: 24 }, ]; @@ -106,13 +97,6 @@ gSmallTests = gSmallTests.concat([ // Used by test_bug654550.html, for videoStats preference var gVideoTests = [ { - name: "320x240.ogv", - type: "video/ogg", - width: 320, - height: 240, - duration: 0.266, - }, - { name: "seek-short.webm", type: "video/webm", width: 320, @@ -145,15 +129,6 @@ var gLongerTests = [ var gProgressTests = [ { name: "r11025_u8_c1.wav", type: "audio/x-wav", duration: 1.0, size: 11069 }, { name: "big-short.wav", type: "audio/x-wav", duration: 1.11, size: 12366 }, - { name: "seek-short.ogv", type: "video/ogg", duration: 1.03, size: 79921 }, - { - name: "320x240.ogv", - type: "video/ogg", - width: 320, - height: 240, - duration: 0.266, - size: 28942, - }, { name: "seek-short.webm", type: "video/webm", duration: 0.23, size: 19267 }, { name: "gizmo-short.mp4", type: "video/mp4", duration: 0.27, size: 29905 }, { name: "bogus.duh", type: "bogus/duh" }, @@ -162,7 +137,6 @@ var gProgressTests = [ // Used by test_played.html var gPlayedTests = [ { name: "big-short.wav", type: "audio/x-wav", duration: 1.11 }, - { name: "seek-short.ogv", type: "video/ogg", duration: 1.03 }, { name: "seek-short.webm", type: "video/webm", duration: 0.23 }, { name: "gizmo-short.mp4", type: "video/mp4", duration: 0.27 }, { name: "owl-short.mp3", type: "audio/mpeg", duration: 0.52 }, @@ -187,14 +161,14 @@ if ( // anything for testing clone-specific bugs. var cloneKey = Math.floor(Math.random() * 100000000); var gCloneTests = [ - // short-video is more like 1s, so if you load this twice you'll get an unexpected duration + // vp9.webm is more like 4s, so if you load this twice you'll get an unexpected duration { name: "dynamic_resource.sjs?key=" + cloneKey + - "&res1=320x240.ogv&res2=short-video.ogv", - type: "video/ogg", - duration: 0.266, + "&res1=seek-short.webm&res2=vp9.webm", + type: "video/webm", + duration: 0.23, }, ]; @@ -223,23 +197,6 @@ var gTrackTests = [ hasVideo: false, }, { - name: "320x240.ogv", - type: "video/ogg", - width: 320, - height: 240, - duration: 0.266, - size: 28942, - hasAudio: false, - hasVideo: true, - }, - { - name: "short-video.ogv", - type: "video/ogg", - duration: 1.081, - hasAudio: true, - hasVideo: true, - }, - { name: "seek-short.webm", type: "video/webm", duration: 0.23, @@ -257,10 +214,6 @@ var gTrackTests = [ { name: "bogus.duh", type: "bogus/duh" }, ]; -var gClosingConnectionsTest = [ - { name: "seek-short.ogv", type: "video/ogg", duration: 1.03 }, -]; - // Used by any media recorder test. Need one test file per decoder backend // currently supported by the media encoder. var gMediaRecorderTests = [ @@ -318,36 +271,6 @@ var gPlayTests = [ // Data length 0xFFFFFFFF and odd chunk lengths. { name: "bug1301226-odd.wav", type: "audio/x-wav", duration: 0.003673 }, - // Ogg stream without eof marker - { name: "bug461281.ogg", type: "application/ogg", duration: 2.208 }, - - // oggz-chop stream - { name: "bug482461.ogv", type: "video/ogg", duration: 4.34 }, - // Theora only oggz-chop stream - { name: "bug482461-theora.ogv", type: "video/ogg", duration: 4.138 }, - // With first frame a "duplicate" (empty) frame. - { - name: "bug500311.ogv", - type: "video/ogg", - duration: 1.96, - contentDuration: 1.958, - }, - // Small audio file - { name: "small-shot.ogg", type: "audio/ogg", duration: 0.276 }, - // More audio in file than video. - { name: "short-video.ogv", type: "video/ogg", duration: 1.081 }, - // First Theora data packet is zero bytes. - { name: "bug504613.ogv", type: "video/ogg", duration: Number.NaN }, - // Multiple audio streams. - { name: "bug516323.ogv", type: "video/ogg", duration: 4.208 }, - // oggz-chop with non-keyframe as first frame - { - name: "bug556821.ogv", - type: "video/ogg", - duration: 2.936, - contentDuration: 2.903, - }, - // Encoded with vorbis beta1, includes unusually sized codebooks { name: "beta-phrasebook.ogg", type: "audio/ogg", duration: 4.01 }, // Small file, only 1 frame with audio only. @@ -355,45 +278,7 @@ var gPlayTests = [ // Small file with vorbis comments with 0 length values and names. { name: "bug520500.ogg", type: "audio/ogg", duration: 0.123 }, - // Various weirdly formed Ogg files - { - name: "bug499519.ogv", - type: "video/ogg", - duration: 0.24, - contentDuration: 0.22, - }, - { name: "bug506094.ogv", type: "video/ogg", duration: 0 }, - { name: "bug498855-1.ogv", type: "video/ogg", duration: 0.24 }, - { name: "bug498855-2.ogv", type: "video/ogg", duration: 0.24 }, - { name: "bug498855-3.ogv", type: "video/ogg", duration: 0.24 }, - { - name: "bug504644.ogv", - type: "video/ogg", - duration: 1.6, - contentDuration: 1.52, - }, - { - name: "chain.ogv", - type: "video/ogg", - duration: Number.NaN, - contentDuration: 0.266, - }, - { - name: "bug523816.ogv", - type: "video/ogg", - duration: 0.766, - contentDuration: 0, - }, - { name: "bug495129.ogv", type: "video/ogg", duration: 2.41 }, - { - name: "bug498380.ogv", - type: "video/ogg", - duration: 0.7663, - contentDuration: 0, - }, { name: "bug495794.ogg", type: "audio/ogg", duration: 0.3 }, - { name: "bug557094.ogv", type: "video/ogg", duration: 0.24 }, - { name: "multiple-bos.ogg", type: "video/ogg", duration: 0.431 }, { name: "audio-overhang.ogg", type: "video/ogg", duration: 2.3 }, { name: "video-overhang.ogg", type: "video/ogg", duration: 3.966 }, @@ -402,9 +287,8 @@ var gPlayTests = [ // Test playback/metadata work after a redirect { - name: "redirect.sjs?domain=mochi.test:8888&file=320x240.ogv", - type: "video/ogg", - duration: 0.266, + name: "redirect.sjs?domain=mochi.test:8888&file=vp9.webm", + type: "video/webm", }, // Test playback of a webm file @@ -559,14 +443,6 @@ var gPlayTests = [ duration: 4.95, contentDuration: 5.03, }, - // Ogg with theora video and flac audio. - { - name: "A4.ogv", - type: "video/ogg", - width: 320, - height: 240, - duration: 3.13, - }, // A file that has no codec delay at the container level, but has a delay at // the codec level. { @@ -651,37 +527,11 @@ var gSeekToNextFrameTests = [ // Test playback of a WebM file with vp9 video { name: "vp9-short.webm", type: "video/webm", duration: 0.2 }, { name: "vp9cake-short.webm", type: "video/webm", duration: 1.0 }, - // oggz-chop stream - { name: "bug482461.ogv", type: "video/ogg", duration: 4.34 }, - // Theora only oggz-chop stream - { name: "bug482461-theora.ogv", type: "video/ogg", duration: 4.138 }, - // With first frame a "duplicate" (empty) frame. - { name: "bug500311.ogv", type: "video/ogg", duration: 1.96 }, - - // More audio in file than video. - { name: "short-video.ogv", type: "video/ogg", duration: 1.081 }, - // First Theora data packet is zero bytes. - { name: "bug504613.ogv", type: "video/ogg", duration: Number.NaN }, - // Multiple audio streams. - { name: "bug516323.ogv", type: "video/ogg", duration: 4.208 }, - // oggz-chop with non-keyframe as first frame - { name: "bug556821.ogv", type: "video/ogg", duration: 2.936 }, - // Various weirdly formed Ogg files - { name: "bug498855-1.ogv", type: "video/ogg", duration: 0.24 }, - { name: "bug498855-2.ogv", type: "video/ogg", duration: 0.24 }, - { name: "bug498855-3.ogv", type: "video/ogg", duration: 0.24 }, - { name: "bug504644.ogv", type: "video/ogg", duration: 1.6 }, - - { name: "bug523816.ogv", type: "video/ogg", duration: 0.766 }, - - { name: "bug498380.ogv", type: "video/ogg", duration: 0.2 }, - { name: "bug557094.ogv", type: "video/ogg", duration: 0.24 }, - { name: "multiple-bos.ogg", type: "video/ogg", duration: 0.431 }, + // Test playback/metadata work after a redirect { - name: "redirect.sjs?domain=mochi.test:8888&file=320x240.ogv", - type: "video/ogg", - duration: 0.266, + name: "redirect.sjs?domain=mochi.test:8888&file=vp9.webm", + type: "video/webm", }, // Test playback of a webm file { name: "seek-short.webm", type: "video/webm", duration: 0.23 }, @@ -698,14 +548,6 @@ var gSeekToNextFrameTests = [ // A file for each type we can support. var gSnifferTests = [ { name: "big.wav", type: "audio/x-wav", duration: 9.278982, size: 102444 }, - { - name: "320x240.ogv", - type: "video/ogg", - width: 320, - height: 240, - duration: 0.233, - size: 28942, - }, { name: "seek.webm", type: "video/webm", duration: 3.966, size: 215529 }, { name: "gizmo.mp4", type: "video/mp4", duration: 5.56, size: 383631 }, // A mp3 file with id3 tags. @@ -743,49 +585,20 @@ var gInvalidPlayTests = [ ]; // Files to check different cases of ogg skeleton information. -// sample-fisbone-skeleton4.ogv -// - Skeleton v4, w/ Content-Type,Role,Name,Language,Title for both theora/vorbis -// sample-fisbone-wrong-header.ogv -// - Skeleton v4, wrong message field sequence for vorbis // multiple-bos-more-header-fields.ogg // - Skeleton v3, w/ Content-Type,Role,Name,Language,Title for both theora/vorbis -// seek-short.ogv -// - No skeleton, but theora // audio-gaps-short.ogg // - No skeleton, but vorbis var gMultitrackInfoOggPlayList = [ - { name: "sample-fisbone-skeleton4.ogv", type: "video/ogg", duration: 1.0 }, - { name: "sample-fisbone-wrong-header.ogv", type: "video/ogg", duration: 1.0 }, { name: "multiple-bos-more-header-fileds.ogg", type: "video/ogg", duration: 0.431, }, - { name: "seek-short.ogv", type: "video/ogg", duration: 1.03 }, { name: "audio-gaps-short.ogg", type: "audio/ogg", duration: 0.5 }, ]; // Pre-parsed results of gMultitrackInfoOggPlayList. var gOggTrackInfoResults = { - "sample-fisbone-skeleton4.ogv": { - audio_id: " audio_1", - audio_kind: "main", - audio_language: " en-US", - audio_label: " Audio track for test", - video_id: " video_1", - video_kind: "main", - video_language: " fr", - video_label: " Video track for test", - }, - "sample-fisbone-wrong-header.ogv": { - audio_id: "1", - audio_kind: "main", - audio_language: "", - audio_label: "", - video_id: " video_1", - video_kind: "main", - video_language: " fr", - video_label: " Video track for test", - }, "multiple-bos-more-header-fileds.ogg": { audio_id: "1", audio_kind: "main", @@ -796,12 +609,6 @@ var gOggTrackInfoResults = { video_language: "", video_label: "", }, - "seek-short.ogv": { - video_id: "2", - video_kind: "main", - video_language: "", - video_label: "", - }, "audio-gaps-short.ogg": { audio_id: "1", audio_kind: "main", @@ -865,14 +672,6 @@ function range_equals(r1, r2) { function makeInfoLeakTests() { return makeAbsolutePathConverter().then(fileUriToSrc => [ { - type: "video/ogg", - src: fileUriToSrc("tests/dom/media/test/320x240.ogv", true), - }, - { - type: "video/ogg", - src: fileUriToSrc("tests/dom/media/test/404.ogv", false), - }, - { type: "audio/x-wav", src: fileUriToSrc("tests/dom/media/test/r11025_s16_c1.wav", true), }, @@ -882,10 +681,6 @@ function makeInfoLeakTests() { }, { type: "audio/ogg", - src: fileUriToSrc("tests/dom/media/test/bug461281.ogg", true), - }, - { - type: "audio/ogg", src: fileUriToSrc("tests/dom/media/test/404.ogg", false), }, { @@ -897,10 +692,6 @@ function makeInfoLeakTests() { src: fileUriToSrc("tests/dom/media/test/404.webm", false), }, { - type: "video/ogg", - src: "http://localhost/404.ogv", - }, - { type: "audio/x-wav", src: "http://localhost/404.wav", }, @@ -925,9 +716,6 @@ function makeInfoLeakTests() { // something crashes we have some idea of which backend is responsible. var gErrorTests = [ { name: "bogus.wav", type: "audio/x-wav" }, - { name: "bogus.ogv", type: "video/ogg" }, - { name: "448636.ogv", type: "video/ogg" }, - { name: "bug504843.ogv", type: "video/ogg" }, { name: "bug501279.ogg", type: "audio/ogg" }, { name: "bug604067.webm", type: "video/webm" }, { name: "bug1535980.webm", type: "video/webm" }, @@ -943,11 +731,8 @@ var gDurationTests = [{ name: "bug604067.webm", duration: 6.076 }]; var gSeekTests = [ { name: "r11025_s16_c1.wav", type: "audio/x-wav", duration: 1.0 }, { name: "audio.wav", type: "audio/x-wav", duration: 0.031247 }, - { name: "seek.ogv", type: "video/ogg", duration: 3.966 }, - { name: "320x240.ogv", type: "video/ogg", duration: 0.266 }, { name: "seek.webm", type: "video/webm", duration: 3.966 }, { name: "sine.webm", type: "audio/webm", duration: 4.001 }, - { name: "bug516323.indexed.ogv", type: "video/ogg", duration: 4.208333 }, { name: "split.webm", type: "video/webm", duration: 1.967 }, { name: "detodos.opus", type: "audio/ogg; codecs=opus", duration: 2.9135 }, { name: "gizmo.mp4", type: "video/mp4", duration: 5.56 }, @@ -958,9 +743,6 @@ var gSeekTests = [ }, { name: "owl.mp3", type: "audio/mpeg", duration: 3.343 }, { name: "bogus.duh", type: "bogus/duh", duration: 123 }, - - // Bug 1242338: hit a numerical problem while seeking to the duration. - { name: "bug482461-theora.ogv", type: "video/ogg", duration: 4.138 }, ]; var gFastSeekTests = [ @@ -971,14 +753,6 @@ var gFastSeekTests = [ }, // Note: Not all keyframes in the file are actually referenced in the Cues in this file. { name: "seek.webm", type: "video/webm", keyframes: [0, 0.8, 1.6, 2.4, 3.2] }, - // Note: the sync points are the points on both the audio and video streams - // before the keyframes. You can't just assume that the keyframes are the sync - // points, as the audio required for that sync point may be before the keyframe. - { - name: "bug516323.indexed.ogv", - type: "video/ogg", - keyframes: [0, 0.46, 3.06], - }, ]; // These files are WebMs without cues. They're seekable within their buffered @@ -1019,7 +793,6 @@ var gAudioTests = [ // various backends. var g404Tests = [ { name: "404.wav", type: "audio/x-wav" }, - { name: "404.ogv", type: "video/ogg" }, { name: "404.oga", type: "audio/ogg" }, { name: "404.webm", type: "video/webm" }, { name: "bogus.duh", type: "bogus/duh" }, @@ -1034,7 +807,6 @@ var gDecodeErrorTests = [ { name: "dirac.ogg", type: "video/ogg" }, // Invalid files { name: "bogus.wav", type: "audio/x-wav" }, - { name: "bogus.ogv", type: "video/ogg" }, { name: "bogus.duh", type: "bogus/duh" }, ]; @@ -1062,12 +834,6 @@ var gChainingTests = [ // original sample rate, so we can safely play Opus chained media that have // different samplerate accross links. { name: "variable-samplerate.opus", type: "audio/ogg; codec=opus", links: 2 }, - // A chained video file. We don't support those, so only one link should be - // reported. - { name: "chained-video.ogv", type: "video/ogg", links: 1 }, - // A file that consist in 4 links of audio, then another link that has video. - // We should stop right after the 4 audio links. - { name: "chained-audio-video.ogg", type: "video/ogg", links: 4 }, // An opus file that has two links, with a different preskip value for each // link. We should be able to play both links. { name: "variable-preskip.opus", type: "audio/ogg; codec=opus", links: 2 }, @@ -1086,36 +852,6 @@ var gAspectRatioTests = [ var gMetadataTests = [ // Ogg Vorbis files { - name: "short-video.ogv", - tags: { - TITLE: "Lepidoptera", - ARTIST: "Epoq", - ALBUM: "Kahvi Collective", - DATE: "2002", - COMMENT: "http://www.kahvi.org", - }, - }, - { - name: "bug516323.ogv", - tags: { - GENRE: "Open Movie", - ENCODER: "Audacity", - TITLE: "Elephants Dream", - ARTIST: "Silvia Pfeiffer", - COMMENTS: "Audio Description", - }, - }, - { - name: "bug516323.indexed.ogv", - tags: { - GENRE: "Open Movie", - ENCODER: "Audacity", - TITLE: "Elephants Dream", - ARTIST: "Silvia Pfeiffer", - COMMENTS: "Audio Description", - }, - }, - { name: "detodos.opus", tags: { title: "De todos. Para todos.", @@ -2208,7 +1944,6 @@ var gDecodeSuspendTests = [ // durations that are looped while we check telemetry for macOS video // low power mode. var gVideoLowPowerTests = [ - { name: "seek.ogv", type: "video/ogg", duration: 3.966 }, { name: "gizmo.mp4", type: "video/mp4", duration: 5.56 }, ]; diff --git a/dom/media/test/mochitest.toml b/dom/media/test/mochitest.toml index 3c8a382766..99bd1c41c8 100644 --- a/dom/media/test/mochitest.toml +++ b/dom/media/test/mochitest.toml @@ -31,12 +31,7 @@ prefs = [ support-files = [ "16bit_wave_extrametadata.wav", "16bit_wave_extrametadata.wav^headers^", - "320x240.ogv", - "320x240.ogv^headers^", - "448636.ogv", - "448636.ogv^headers^", - "A4.ogv", - "A4.ogv^headers^", + "320x240.webm", "VID_0001.ogg", "VID_0001.ogg^headers^", "allowed.sjs", @@ -410,63 +405,19 @@ support-files = [ "bipbop_short_vp8.webm^headers^", "bipbop-lateaudio.mp4", "bipbop-lateaudio.mp4^headers^", - "black100x100-aspect3to2.ogv", - "black100x100-aspect3to2.ogv^headers^", "bogus.duh", - "bogus.ogv", - "bogus.ogv^headers^", "bogus.wav", "bogus.wav^headers^", - "bug461281.ogg", - "bug461281.ogg^headers^", - "bug482461-theora.ogv", - "bug482461-theora.ogv^headers^", - "bug482461.ogv", - "bug482461.ogv^headers^", - "bug495129.ogv", - "bug495129.ogv^headers^", "bug495794.ogg", "bug495794.ogg^headers^", - "bug498380.ogv", - "bug498380.ogv^headers^", - "bug498855-1.ogv", - "bug498855-1.ogv^headers^", - "bug498855-2.ogv", - "bug498855-2.ogv^headers^", - "bug498855-3.ogv", - "bug498855-3.ogv^headers^", - "bug499519.ogv", - "bug499519.ogv^headers^", - "bug500311.ogv", - "bug500311.ogv^headers^", "bug501279.ogg", "bug501279.ogg^headers^", - "bug504613.ogv", - "bug504613.ogv^headers^", - "bug504644.ogv", - "bug504644.ogv^headers^", - "bug504843.ogv", - "bug504843.ogv^headers^", - "bug506094.ogv", - "bug506094.ogv^headers^", - "bug516323.indexed.ogv", - "bug516323.indexed.ogv^headers^", - "bug516323.ogv", - "bug516323.ogv^headers^", "bug520493.ogg", "bug520493.ogg^headers^", "bug520500.ogg", "bug520500.ogg^headers^", - "bug520908.ogv", - "bug520908.ogv^headers^", - "bug523816.ogv", - "bug523816.ogv^headers^", "bug533822.ogg", "bug533822.ogg^headers^", - "bug556821.ogv", - "bug556821.ogv^headers^", - "bug557094.ogv", - "bug557094.ogv^headers^", "bug604067.webm", "bug604067.webm^headers^", "bug1066943.webm", @@ -487,14 +438,10 @@ support-files = [ "cancellable_request.sjs", "chain.ogg", "chain.ogg^headers^", - "chain.ogv", - "chain.ogv^headers^", "chain.opus", "chain.opus^headers^", "chained-audio-video.ogg", "chained-audio-video.ogg^headers^", - "chained-video.ogv", - "chained-video.ogv^headers^", "chromeHelper.js", "cloneElementVisually_helpers.js", "contentType.sjs", @@ -575,8 +522,6 @@ support-files = [ "invalid-preskip.webm^headers^", "manifest.js", "midflight-redirect.sjs", - "multiple-bos.ogg", - "multiple-bos.ogg^headers^", "multiple-bos-more-header-fileds.ogg", "multiple-bos-more-header-fileds.ogg^headers^", "multi_id3v2.mp3", @@ -631,14 +576,6 @@ support-files = [ "sample.3g2", "sample-encrypted-sgpdstbl-sbgptraf.mp4", "sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^", - "sample-fisbone-skeleton4.ogv", - "sample-fisbone-skeleton4.ogv^headers^", - "sample-fisbone-wrong-header.ogv", - "sample-fisbone-wrong-header.ogv^headers^", - "seek.ogv", - "seek.ogv^headers^", - "seek-short.ogv", - "seek-short.ogv^headers^", "seek.webm", "seek.webm^headers^", "seek-short.webm", @@ -651,7 +588,6 @@ support-files = [ "short-audio-fragmented-cenc-without-pssh.mp4", "short-audio-fragmented-cenc-without-pssh.mp4^headers^", "short-cenc.mp4", - "short-video.ogv", "short.mp4", "short.mp4.gz", "short.mp4^headers^", @@ -678,7 +614,6 @@ support-files = [ "sintel-short-clearkey-subsample-encrypted-audio.webm^headers^", "sintel-short-clearkey-subsample-encrypted-video.webm", "sintel-short-clearkey-subsample-encrypted-video.webm^headers^", - "short-video.ogv^headers^", "short-vp9-encrypted-video.mp4", "short-vp9-encrypted-video.mp4^headers^", "small-shot-mp3.mp4", diff --git a/dom/media/test/mochitest_background_video.toml b/dom/media/test/mochitest_background_video.toml index e1bc542264..6eed2c3eb8 100644 --- a/dom/media/test/mochitest_background_video.toml +++ b/dom/media/test/mochitest_background_video.toml @@ -27,12 +27,6 @@ tags = "suspend media-gpu" support-files = [ "16bit_wave_extrametadata.wav", "16bit_wave_extrametadata.wav^headers^", - "320x240.ogv", - "320x240.ogv^headers^", - "448636.ogv", - "448636.ogv^headers^", - "A4.ogv", - "A4.ogv^headers^", "VID_0001.ogg", "VID_0001.ogg^headers^", "allowed.sjs", @@ -406,63 +400,19 @@ support-files = [ "bipbop_short_vp8.webm^headers^", "bipbop-lateaudio.mp4", "bipbop-lateaudio.mp4^headers^", - "black100x100-aspect3to2.ogv", - "black100x100-aspect3to2.ogv^headers^", "bogus.duh", - "bogus.ogv", - "bogus.ogv^headers^", "bogus.wav", "bogus.wav^headers^", - "bug461281.ogg", - "bug461281.ogg^headers^", - "bug482461-theora.ogv", - "bug482461-theora.ogv^headers^", - "bug482461.ogv", - "bug482461.ogv^headers^", - "bug495129.ogv", - "bug495129.ogv^headers^", "bug495794.ogg", "bug495794.ogg^headers^", - "bug498380.ogv", - "bug498380.ogv^headers^", - "bug498855-1.ogv", - "bug498855-1.ogv^headers^", - "bug498855-2.ogv", - "bug498855-2.ogv^headers^", - "bug498855-3.ogv", - "bug498855-3.ogv^headers^", - "bug499519.ogv", - "bug499519.ogv^headers^", - "bug500311.ogv", - "bug500311.ogv^headers^", "bug501279.ogg", "bug501279.ogg^headers^", - "bug504613.ogv", - "bug504613.ogv^headers^", - "bug504644.ogv", - "bug504644.ogv^headers^", - "bug504843.ogv", - "bug504843.ogv^headers^", - "bug506094.ogv", - "bug506094.ogv^headers^", - "bug516323.indexed.ogv", - "bug516323.indexed.ogv^headers^", - "bug516323.ogv", - "bug516323.ogv^headers^", "bug520493.ogg", "bug520493.ogg^headers^", "bug520500.ogg", "bug520500.ogg^headers^", - "bug520908.ogv", - "bug520908.ogv^headers^", - "bug523816.ogv", - "bug523816.ogv^headers^", "bug533822.ogg", "bug533822.ogg^headers^", - "bug556821.ogv", - "bug556821.ogv^headers^", - "bug557094.ogv", - "bug557094.ogv^headers^", "bug604067.webm", "bug604067.webm^headers^", "bug1066943.webm", @@ -481,14 +431,8 @@ support-files = [ "cancellable_request.sjs", "chain.ogg", "chain.ogg^headers^", - "chain.ogv", - "chain.ogv^headers^", "chain.opus", "chain.opus^headers^", - "chained-audio-video.ogg", - "chained-audio-video.ogg^headers^", - "chained-video.ogv", - "chained-video.ogv^headers^", "chromeHelper.js", "cloneElementVisually_helpers.js", "contentType.sjs", @@ -568,8 +512,6 @@ support-files = [ "invalid-preskip.webm^headers^", "manifest.js", "midflight-redirect.sjs", - "multiple-bos.ogg", - "multiple-bos.ogg^headers^", "multiple-bos-more-header-fileds.ogg", "multiple-bos-more-header-fileds.ogg^headers^", "multi_id3v2.mp3", @@ -624,14 +566,6 @@ support-files = [ "sample.3g2", "sample-encrypted-sgpdstbl-sbgptraf.mp4", "sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^", - "sample-fisbone-skeleton4.ogv", - "sample-fisbone-skeleton4.ogv^headers^", - "sample-fisbone-wrong-header.ogv", - "sample-fisbone-wrong-header.ogv^headers^", - "seek.ogv", - "seek.ogv^headers^", - "seek-short.ogv", - "seek-short.ogv^headers^", "seek.webm", "seek.webm^headers^", "seek-short.webm", @@ -653,8 +587,6 @@ support-files = [ "short-aac-encrypted-audio.mp4^headers^", "short-audio-fragmented-cenc-without-pssh.mp4", "short-audio-fragmented-cenc-without-pssh.mp4^headers^", - "short-video.ogv", - "short-video.ogv^headers^", "short-vp9-encrypted-video.mp4", "short-vp9-encrypted-video.mp4^headers^", "small-shot-mp3.mp4", diff --git a/dom/media/test/mochitest_bugs.toml b/dom/media/test/mochitest_bugs.toml index 5c68d0e795..9e5f785408 100644 --- a/dom/media/test/mochitest_bugs.toml +++ b/dom/media/test/mochitest_bugs.toml @@ -27,12 +27,6 @@ tags = "media-gpu" support-files = [ "16bit_wave_extrametadata.wav", "16bit_wave_extrametadata.wav^headers^", - "320x240.ogv", - "320x240.ogv^headers^", - "448636.ogv", - "448636.ogv^headers^", - "A4.ogv", - "A4.ogv^headers^", "VID_0001.ogg", "VID_0001.ogg^headers^", "allowed.sjs", @@ -406,63 +400,19 @@ support-files = [ "bipbop_short_vp8.webm^headers^", "bipbop-lateaudio.mp4", "bipbop-lateaudio.mp4^headers^", - "black100x100-aspect3to2.ogv", - "black100x100-aspect3to2.ogv^headers^", "bogus.duh", - "bogus.ogv", - "bogus.ogv^headers^", "bogus.wav", "bogus.wav^headers^", - "bug461281.ogg", - "bug461281.ogg^headers^", - "bug482461-theora.ogv", - "bug482461-theora.ogv^headers^", - "bug482461.ogv", - "bug482461.ogv^headers^", - "bug495129.ogv", - "bug495129.ogv^headers^", "bug495794.ogg", "bug495794.ogg^headers^", - "bug498380.ogv", - "bug498380.ogv^headers^", - "bug498855-1.ogv", - "bug498855-1.ogv^headers^", - "bug498855-2.ogv", - "bug498855-2.ogv^headers^", - "bug498855-3.ogv", - "bug498855-3.ogv^headers^", - "bug499519.ogv", - "bug499519.ogv^headers^", - "bug500311.ogv", - "bug500311.ogv^headers^", "bug501279.ogg", "bug501279.ogg^headers^", - "bug504613.ogv", - "bug504613.ogv^headers^", - "bug504644.ogv", - "bug504644.ogv^headers^", - "bug504843.ogv", - "bug504843.ogv^headers^", - "bug506094.ogv", - "bug506094.ogv^headers^", - "bug516323.indexed.ogv", - "bug516323.indexed.ogv^headers^", - "bug516323.ogv", - "bug516323.ogv^headers^", "bug520493.ogg", "bug520493.ogg^headers^", "bug520500.ogg", "bug520500.ogg^headers^", - "bug520908.ogv", - "bug520908.ogv^headers^", - "bug523816.ogv", - "bug523816.ogv^headers^", "bug533822.ogg", "bug533822.ogg^headers^", - "bug556821.ogv", - "bug556821.ogv^headers^", - "bug557094.ogv", - "bug557094.ogv^headers^", "bug604067.webm", "bug604067.webm^headers^", "bug1066943.webm", @@ -483,14 +433,8 @@ support-files = [ "cancellable_request.sjs", "chain.ogg", "chain.ogg^headers^", - "chain.ogv", - "chain.ogv^headers^", "chain.opus", "chain.opus^headers^", - "chained-audio-video.ogg", - "chained-audio-video.ogg^headers^", - "chained-video.ogv", - "chained-video.ogv^headers^", "chromeHelper.js", "cloneElementVisually_helpers.js", "contentType.sjs", @@ -570,8 +514,6 @@ support-files = [ "invalid-preskip.webm^headers^", "manifest.js", "midflight-redirect.sjs", - "multiple-bos.ogg", - "multiple-bos.ogg^headers^", "multiple-bos-more-header-fileds.ogg", "multiple-bos-more-header-fileds.ogg^headers^", "multi_id3v2.mp3", @@ -626,14 +568,6 @@ support-files = [ "sample.3g2", "sample-encrypted-sgpdstbl-sbgptraf.mp4", "sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^", - "sample-fisbone-skeleton4.ogv", - "sample-fisbone-skeleton4.ogv^headers^", - "sample-fisbone-wrong-header.ogv", - "sample-fisbone-wrong-header.ogv^headers^", - "seek.ogv", - "seek.ogv^headers^", - "seek-short.ogv", - "seek-short.ogv^headers^", "seek.webm", "seek.webm^headers^", "seek-short.webm", @@ -655,8 +589,6 @@ support-files = [ "short-aac-encrypted-audio.mp4^headers^", "short-audio-fragmented-cenc-without-pssh.mp4", "short-audio-fragmented-cenc-without-pssh.mp4^headers^", - "short-video.ogv", - "short-video.ogv^headers^", "short-vp9-encrypted-video.mp4", "short-vp9-encrypted-video.mp4^headers^", "small-shot-mp3.mp4", diff --git a/dom/media/test/mochitest_compat.toml b/dom/media/test/mochitest_compat.toml index 86f76f1464..fdff340f39 100644 --- a/dom/media/test/mochitest_compat.toml +++ b/dom/media/test/mochitest_compat.toml @@ -35,12 +35,6 @@ prefs = ["media.wmf.hevc.enabled=1"] # for test_hevc_playback support-files = [ "16bit_wave_extrametadata.wav", "16bit_wave_extrametadata.wav^headers^", - "320x240.ogv", - "320x240.ogv^headers^", - "448636.ogv", - "448636.ogv^headers^", - "A4.ogv", - "A4.ogv^headers^", "VID_0001.ogg", "VID_0001.ogg^headers^", "adts.aac", @@ -416,63 +410,19 @@ support-files = [ "bipbop_short_vp8.webm^headers^", "bipbop-lateaudio.mp4", "bipbop-lateaudio.mp4^headers^", - "black100x100-aspect3to2.ogv", - "black100x100-aspect3to2.ogv^headers^", "bogus.duh", - "bogus.ogv", - "bogus.ogv^headers^", "bogus.wav", "bogus.wav^headers^", - "bug461281.ogg", - "bug461281.ogg^headers^", - "bug482461-theora.ogv", - "bug482461-theora.ogv^headers^", - "bug482461.ogv", - "bug482461.ogv^headers^", - "bug495129.ogv", - "bug495129.ogv^headers^", "bug495794.ogg", "bug495794.ogg^headers^", - "bug498380.ogv", - "bug498380.ogv^headers^", - "bug498855-1.ogv", - "bug498855-1.ogv^headers^", - "bug498855-2.ogv", - "bug498855-2.ogv^headers^", - "bug498855-3.ogv", - "bug498855-3.ogv^headers^", - "bug499519.ogv", - "bug499519.ogv^headers^", - "bug500311.ogv", - "bug500311.ogv^headers^", "bug501279.ogg", "bug501279.ogg^headers^", - "bug504613.ogv", - "bug504613.ogv^headers^", - "bug504644.ogv", - "bug504644.ogv^headers^", - "bug504843.ogv", - "bug504843.ogv^headers^", - "bug506094.ogv", - "bug506094.ogv^headers^", - "bug516323.indexed.ogv", - "bug516323.indexed.ogv^headers^", - "bug516323.ogv", - "bug516323.ogv^headers^", "bug520493.ogg", "bug520493.ogg^headers^", "bug520500.ogg", "bug520500.ogg^headers^", - "bug520908.ogv", - "bug520908.ogv^headers^", - "bug523816.ogv", - "bug523816.ogv^headers^", "bug533822.ogg", "bug533822.ogg^headers^", - "bug556821.ogv", - "bug556821.ogv^headers^", - "bug557094.ogv", - "bug557094.ogv^headers^", "bug604067.webm", "bug604067.webm^headers^", "bug1066943.webm", @@ -491,14 +441,8 @@ support-files = [ "cancellable_request.sjs", "chain.ogg", "chain.ogg^headers^", - "chain.ogv", - "chain.ogv^headers^", "chain.opus", "chain.opus^headers^", - "chained-audio-video.ogg", - "chained-audio-video.ogg^headers^", - "chained-video.ogv", - "chained-video.ogv^headers^", "chromeHelper.js", "cloneElementVisually_helpers.js", "contentType.sjs", @@ -592,8 +536,6 @@ support-files = [ "invalid-preskip.webm^headers^", "manifest.js", "midflight-redirect.sjs", - "multiple-bos.ogg", - "multiple-bos.ogg^headers^", "multiple-bos-more-header-fileds.ogg", "multiple-bos-more-header-fileds.ogg^headers^", "multi_id3v2.mp3", @@ -648,14 +590,6 @@ support-files = [ "sample.3g2", "sample-encrypted-sgpdstbl-sbgptraf.mp4", "sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^", - "sample-fisbone-skeleton4.ogv", - "sample-fisbone-skeleton4.ogv^headers^", - "sample-fisbone-wrong-header.ogv", - "sample-fisbone-wrong-header.ogv^headers^", - "seek.ogv", - "seek.ogv^headers^", - "seek-short.ogv", - "seek-short.ogv^headers^", "seek.webm", "seek.webm^headers^", "seek-short.webm", @@ -677,8 +611,6 @@ support-files = [ "short-aac-encrypted-audio.mp4^headers^", "short-audio-fragmented-cenc-without-pssh.mp4", "short-audio-fragmented-cenc-without-pssh.mp4^headers^", - "short-video.ogv", - "short-video.ogv^headers^", "short-vp9-encrypted-video.mp4", "short-vp9-encrypted-video.mp4^headers^", "small-shot-mp3.mp4", @@ -823,6 +755,10 @@ skip-if = ["true"] # bug 475110 - disabled since we don't play Wave files standa ["test_can_play_type_webm.html"] ["test_closing_connections.html"] +# This test attempts to load 20 videos to test something network-related, and +# Android devices that aren't an emulator hit a device-specific decoder limit, +# that make the test fail. +skip-if = ["os == 'android' && !is_emulator"] ["test_constants.html"] diff --git a/dom/media/test/mochitest_eme.toml b/dom/media/test/mochitest_eme.toml index d7f39c3eb8..fb3c12fbc6 100644 --- a/dom/media/test/mochitest_eme.toml +++ b/dom/media/test/mochitest_eme.toml @@ -27,12 +27,6 @@ skip-if = ["os == 'linux' && (asan || debug)"] # Bug 1476870: common fatal error support-files = [ "16bit_wave_extrametadata.wav", "16bit_wave_extrametadata.wav^headers^", - "320x240.ogv", - "320x240.ogv^headers^", - "448636.ogv", - "448636.ogv^headers^", - "A4.ogv", - "A4.ogv^headers^", "VID_0001.ogg", "VID_0001.ogg^headers^", "allowed.sjs", @@ -406,63 +400,19 @@ support-files = [ "bipbop_short_vp8.webm^headers^", "bipbop-lateaudio.mp4", "bipbop-lateaudio.mp4^headers^", - "black100x100-aspect3to2.ogv", - "black100x100-aspect3to2.ogv^headers^", "bogus.duh", - "bogus.ogv", - "bogus.ogv^headers^", "bogus.wav", "bogus.wav^headers^", - "bug461281.ogg", - "bug461281.ogg^headers^", - "bug482461-theora.ogv", - "bug482461-theora.ogv^headers^", - "bug482461.ogv", - "bug482461.ogv^headers^", - "bug495129.ogv", - "bug495129.ogv^headers^", "bug495794.ogg", "bug495794.ogg^headers^", - "bug498380.ogv", - "bug498380.ogv^headers^", - "bug498855-1.ogv", - "bug498855-1.ogv^headers^", - "bug498855-2.ogv", - "bug498855-2.ogv^headers^", - "bug498855-3.ogv", - "bug498855-3.ogv^headers^", - "bug499519.ogv", - "bug499519.ogv^headers^", - "bug500311.ogv", - "bug500311.ogv^headers^", "bug501279.ogg", "bug501279.ogg^headers^", - "bug504613.ogv", - "bug504613.ogv^headers^", - "bug504644.ogv", - "bug504644.ogv^headers^", - "bug504843.ogv", - "bug504843.ogv^headers^", - "bug506094.ogv", - "bug506094.ogv^headers^", - "bug516323.indexed.ogv", - "bug516323.indexed.ogv^headers^", - "bug516323.ogv", - "bug516323.ogv^headers^", "bug520493.ogg", "bug520493.ogg^headers^", "bug520500.ogg", "bug520500.ogg^headers^", - "bug520908.ogv", - "bug520908.ogv^headers^", - "bug523816.ogv", - "bug523816.ogv^headers^", "bug533822.ogg", "bug533822.ogg^headers^", - "bug556821.ogv", - "bug556821.ogv^headers^", - "bug557094.ogv", - "bug557094.ogv^headers^", "bug604067.webm", "bug604067.webm^headers^", "bug1066943.webm", @@ -481,14 +431,8 @@ support-files = [ "cancellable_request.sjs", "chain.ogg", "chain.ogg^headers^", - "chain.ogv", - "chain.ogv^headers^", "chain.opus", "chain.opus^headers^", - "chained-audio-video.ogg", - "chained-audio-video.ogg^headers^", - "chained-video.ogv", - "chained-video.ogv^headers^", "chromeHelper.js", "cloneElementVisually_helpers.js", "contentType.sjs", @@ -568,8 +512,6 @@ support-files = [ "invalid-preskip.webm^headers^", "manifest.js", "midflight-redirect.sjs", - "multiple-bos.ogg", - "multiple-bos.ogg^headers^", "multiple-bos-more-header-fileds.ogg", "multiple-bos-more-header-fileds.ogg^headers^", "multi_id3v2.mp3", @@ -624,14 +566,6 @@ support-files = [ "sample.3g2", "sample-encrypted-sgpdstbl-sbgptraf.mp4", "sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^", - "sample-fisbone-skeleton4.ogv", - "sample-fisbone-skeleton4.ogv^headers^", - "sample-fisbone-wrong-header.ogv", - "sample-fisbone-wrong-header.ogv^headers^", - "seek.ogv", - "seek.ogv^headers^", - "seek-short.ogv", - "seek-short.ogv^headers^", "seek.webm", "seek.webm^headers^", "seek-short.webm", @@ -653,8 +587,6 @@ support-files = [ "short-aac-encrypted-audio.mp4^headers^", "short-audio-fragmented-cenc-without-pssh.mp4", "short-audio-fragmented-cenc-without-pssh.mp4^headers^", - "short-video.ogv", - "short-video.ogv^headers^", "short-vp9-encrypted-video.mp4", "short-vp9-encrypted-video.mp4^headers^", "small-shot-mp3.mp4", diff --git a/dom/media/test/mochitest_eme_compat.toml b/dom/media/test/mochitest_eme_compat.toml index 43a5b510fc..cc093b3d28 100644 --- a/dom/media/test/mochitest_eme_compat.toml +++ b/dom/media/test/mochitest_eme_compat.toml @@ -28,12 +28,6 @@ skip-if = ["os == 'linux' && (asan || debug)"] # Bug 1476870: common fatal error support-files = [ "16bit_wave_extrametadata.wav", "16bit_wave_extrametadata.wav^headers^", - "320x240.ogv", - "320x240.ogv^headers^", - "448636.ogv", - "448636.ogv^headers^", - "A4.ogv", - "A4.ogv^headers^", "VID_0001.ogg", "VID_0001.ogg^headers^", "allowed.sjs", @@ -403,63 +397,19 @@ support-files = [ "bipbop_short_vp8.webm^headers^", "bipbop-lateaudio.mp4", "bipbop-lateaudio.mp4^headers^", - "black100x100-aspect3to2.ogv", - "black100x100-aspect3to2.ogv^headers^", "bogus.duh", - "bogus.ogv", - "bogus.ogv^headers^", "bogus.wav", "bogus.wav^headers^", - "bug461281.ogg", - "bug461281.ogg^headers^", - "bug482461-theora.ogv", - "bug482461-theora.ogv^headers^", - "bug482461.ogv", - "bug482461.ogv^headers^", - "bug495129.ogv", - "bug495129.ogv^headers^", "bug495794.ogg", "bug495794.ogg^headers^", - "bug498380.ogv", - "bug498380.ogv^headers^", - "bug498855-1.ogv", - "bug498855-1.ogv^headers^", - "bug498855-2.ogv", - "bug498855-2.ogv^headers^", - "bug498855-3.ogv", - "bug498855-3.ogv^headers^", - "bug499519.ogv", - "bug499519.ogv^headers^", - "bug500311.ogv", - "bug500311.ogv^headers^", "bug501279.ogg", "bug501279.ogg^headers^", - "bug504613.ogv", - "bug504613.ogv^headers^", - "bug504644.ogv", - "bug504644.ogv^headers^", - "bug504843.ogv", - "bug504843.ogv^headers^", - "bug506094.ogv", - "bug506094.ogv^headers^", - "bug516323.indexed.ogv", - "bug516323.indexed.ogv^headers^", - "bug516323.ogv", - "bug516323.ogv^headers^", "bug520493.ogg", "bug520493.ogg^headers^", "bug520500.ogg", "bug520500.ogg^headers^", - "bug520908.ogv", - "bug520908.ogv^headers^", - "bug523816.ogv", - "bug523816.ogv^headers^", "bug533822.ogg", "bug533822.ogg^headers^", - "bug556821.ogv", - "bug556821.ogv^headers^", - "bug557094.ogv", - "bug557094.ogv^headers^", "bug604067.webm", "bug604067.webm^headers^", "bug1066943.webm", @@ -478,14 +428,8 @@ support-files = [ "cancellable_request.sjs", "chain.ogg", "chain.ogg^headers^", - "chain.ogv", - "chain.ogv^headers^", "chain.opus", "chain.opus^headers^", - "chained-audio-video.ogg", - "chained-audio-video.ogg^headers^", - "chained-video.ogv", - "chained-video.ogv^headers^", "chromeHelper.js", "cloneElementVisually_helpers.js", "contentType.sjs", @@ -565,8 +509,6 @@ support-files = [ "invalid-preskip.webm^headers^", "manifest.js", "midflight-redirect.sjs", - "multiple-bos.ogg", - "multiple-bos.ogg^headers^", "multiple-bos-more-header-fileds.ogg", "multiple-bos-more-header-fileds.ogg^headers^", "multi_id3v2.mp3", @@ -621,14 +563,6 @@ support-files = [ "sample.3g2", "sample-encrypted-sgpdstbl-sbgptraf.mp4", "sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^", - "sample-fisbone-skeleton4.ogv", - "sample-fisbone-skeleton4.ogv^headers^", - "sample-fisbone-wrong-header.ogv", - "sample-fisbone-wrong-header.ogv^headers^", - "seek.ogv", - "seek.ogv^headers^", - "seek-short.ogv", - "seek-short.ogv^headers^", "seek.webm", "seek.webm^headers^", "seek-short.webm", @@ -650,8 +584,6 @@ support-files = [ "short-aac-encrypted-audio.mp4^headers^", "short-audio-fragmented-cenc-without-pssh.mp4", "short-audio-fragmented-cenc-without-pssh.mp4^headers^", - "short-video.ogv", - "short-video.ogv^headers^", "short-vp9-encrypted-video.mp4", "short-vp9-encrypted-video.mp4^headers^", "small-shot-mp3.mp4", diff --git a/dom/media/test/mochitest_media_recorder.toml b/dom/media/test/mochitest_media_recorder.toml index a4893d9cf4..3bebaab839 100644 --- a/dom/media/test/mochitest_media_recorder.toml +++ b/dom/media/test/mochitest_media_recorder.toml @@ -27,12 +27,6 @@ tags = "mtg" support-files = [ "16bit_wave_extrametadata.wav", "16bit_wave_extrametadata.wav^headers^", - "320x240.ogv", - "320x240.ogv^headers^", - "448636.ogv", - "448636.ogv^headers^", - "A4.ogv", - "A4.ogv^headers^", "VID_0001.ogg", "VID_0001.ogg^headers^", "allowed.sjs", @@ -406,63 +400,19 @@ support-files = [ "bipbop_short_vp8.webm^headers^", "bipbop-lateaudio.mp4", "bipbop-lateaudio.mp4^headers^", - "black100x100-aspect3to2.ogv", - "black100x100-aspect3to2.ogv^headers^", "bogus.duh", - "bogus.ogv", - "bogus.ogv^headers^", "bogus.wav", "bogus.wav^headers^", - "bug461281.ogg", - "bug461281.ogg^headers^", - "bug482461-theora.ogv", - "bug482461-theora.ogv^headers^", - "bug482461.ogv", - "bug482461.ogv^headers^", - "bug495129.ogv", - "bug495129.ogv^headers^", "bug495794.ogg", "bug495794.ogg^headers^", - "bug498380.ogv", - "bug498380.ogv^headers^", - "bug498855-1.ogv", - "bug498855-1.ogv^headers^", - "bug498855-2.ogv", - "bug498855-2.ogv^headers^", - "bug498855-3.ogv", - "bug498855-3.ogv^headers^", - "bug499519.ogv", - "bug499519.ogv^headers^", - "bug500311.ogv", - "bug500311.ogv^headers^", "bug501279.ogg", "bug501279.ogg^headers^", - "bug504613.ogv", - "bug504613.ogv^headers^", - "bug504644.ogv", - "bug504644.ogv^headers^", - "bug504843.ogv", - "bug504843.ogv^headers^", - "bug506094.ogv", - "bug506094.ogv^headers^", - "bug516323.indexed.ogv", - "bug516323.indexed.ogv^headers^", - "bug516323.ogv", - "bug516323.ogv^headers^", "bug520493.ogg", "bug520493.ogg^headers^", "bug520500.ogg", "bug520500.ogg^headers^", - "bug520908.ogv", - "bug520908.ogv^headers^", - "bug523816.ogv", - "bug523816.ogv^headers^", "bug533822.ogg", "bug533822.ogg^headers^", - "bug556821.ogv", - "bug556821.ogv^headers^", - "bug557094.ogv", - "bug557094.ogv^headers^", "bug604067.webm", "bug604067.webm^headers^", "bug1066943.webm", @@ -481,14 +431,10 @@ support-files = [ "cancellable_request.sjs", "chain.ogg", "chain.ogg^headers^", - "chain.ogv", - "chain.ogv^headers^", "chain.opus", "chain.opus^headers^", "chained-audio-video.ogg", "chained-audio-video.ogg^headers^", - "chained-video.ogv", - "chained-video.ogv^headers^", "chromeHelper.js", "cloneElementVisually_helpers.js", "contentType.sjs", @@ -568,8 +514,6 @@ support-files = [ "invalid-preskip.webm^headers^", "manifest.js", "midflight-redirect.sjs", - "multiple-bos.ogg", - "multiple-bos.ogg^headers^", "multiple-bos-more-header-fileds.ogg", "multiple-bos-more-header-fileds.ogg^headers^", "multi_id3v2.mp3", @@ -624,14 +568,6 @@ support-files = [ "sample.3g2", "sample-encrypted-sgpdstbl-sbgptraf.mp4", "sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^", - "sample-fisbone-skeleton4.ogv", - "sample-fisbone-skeleton4.ogv^headers^", - "sample-fisbone-wrong-header.ogv", - "sample-fisbone-wrong-header.ogv^headers^", - "seek.ogv", - "seek.ogv^headers^", - "seek-short.ogv", - "seek-short.ogv^headers^", "seek.webm", "seek.webm^headers^", "seek-short.webm", @@ -653,8 +589,6 @@ support-files = [ "short-aac-encrypted-audio.mp4^headers^", "short-audio-fragmented-cenc-without-pssh.mp4", "short-audio-fragmented-cenc-without-pssh.mp4^headers^", - "short-video.ogv", - "short-video.ogv^headers^", "short-vp9-encrypted-video.mp4", "short-vp9-encrypted-video.mp4^headers^", "small-shot-mp3.mp4", @@ -793,7 +727,7 @@ skip-if = ["os == 'android'"] # android(bug 1232305) ["test_mediarecorder_principals.html"] skip-if = [ - "os == 'win' && os_version == '10.0'", # Bug 1453375 + "os == 'win' && os_version == '10.2009'", # Bug 1453375 "os == 'android'", # Bug 1694645 ] diff --git a/dom/media/test/mochitest_seek.toml b/dom/media/test/mochitest_seek.toml index d71aac775a..7d7e703ab3 100644 --- a/dom/media/test/mochitest_seek.toml +++ b/dom/media/test/mochitest_seek.toml @@ -27,12 +27,6 @@ tags = "media-gpu" support-files = [ "16bit_wave_extrametadata.wav", "16bit_wave_extrametadata.wav^headers^", - "320x240.ogv", - "320x240.ogv^headers^", - "448636.ogv", - "448636.ogv^headers^", - "A4.ogv", - "A4.ogv^headers^", "VID_0001.ogg", "VID_0001.ogg^headers^", "allowed.sjs", @@ -406,63 +400,19 @@ support-files = [ "bipbop_short_vp8.webm^headers^", "bipbop-lateaudio.mp4", "bipbop-lateaudio.mp4^headers^", - "black100x100-aspect3to2.ogv", - "black100x100-aspect3to2.ogv^headers^", "bogus.duh", - "bogus.ogv", - "bogus.ogv^headers^", "bogus.wav", "bogus.wav^headers^", - "bug461281.ogg", - "bug461281.ogg^headers^", - "bug482461-theora.ogv", - "bug482461-theora.ogv^headers^", - "bug482461.ogv", - "bug482461.ogv^headers^", - "bug495129.ogv", - "bug495129.ogv^headers^", "bug495794.ogg", "bug495794.ogg^headers^", - "bug498380.ogv", - "bug498380.ogv^headers^", - "bug498855-1.ogv", - "bug498855-1.ogv^headers^", - "bug498855-2.ogv", - "bug498855-2.ogv^headers^", - "bug498855-3.ogv", - "bug498855-3.ogv^headers^", - "bug499519.ogv", - "bug499519.ogv^headers^", - "bug500311.ogv", - "bug500311.ogv^headers^", "bug501279.ogg", "bug501279.ogg^headers^", - "bug504613.ogv", - "bug504613.ogv^headers^", - "bug504644.ogv", - "bug504644.ogv^headers^", - "bug504843.ogv", - "bug504843.ogv^headers^", - "bug506094.ogv", - "bug506094.ogv^headers^", - "bug516323.indexed.ogv", - "bug516323.indexed.ogv^headers^", - "bug516323.ogv", - "bug516323.ogv^headers^", "bug520493.ogg", "bug520493.ogg^headers^", "bug520500.ogg", "bug520500.ogg^headers^", - "bug520908.ogv", - "bug520908.ogv^headers^", - "bug523816.ogv", - "bug523816.ogv^headers^", "bug533822.ogg", "bug533822.ogg^headers^", - "bug556821.ogv", - "bug556821.ogv^headers^", - "bug557094.ogv", - "bug557094.ogv^headers^", "bug604067.webm", "bug604067.webm^headers^", "bug1066943.webm", @@ -481,14 +431,8 @@ support-files = [ "cancellable_request.sjs", "chain.ogg", "chain.ogg^headers^", - "chain.ogv", - "chain.ogv^headers^", "chain.opus", "chain.opus^headers^", - "chained-audio-video.ogg", - "chained-audio-video.ogg^headers^", - "chained-video.ogv", - "chained-video.ogv^headers^", "chromeHelper.js", "cloneElementVisually_helpers.js", "contentType.sjs", @@ -568,8 +512,6 @@ support-files = [ "invalid-preskip.webm^headers^", "manifest.js", "midflight-redirect.sjs", - "multiple-bos.ogg", - "multiple-bos.ogg^headers^", "multiple-bos-more-header-fileds.ogg", "multiple-bos-more-header-fileds.ogg^headers^", "multi_id3v2.mp3", @@ -624,14 +566,6 @@ support-files = [ "sample.3g2", "sample-encrypted-sgpdstbl-sbgptraf.mp4", "sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^", - "sample-fisbone-skeleton4.ogv", - "sample-fisbone-skeleton4.ogv^headers^", - "sample-fisbone-wrong-header.ogv", - "sample-fisbone-wrong-header.ogv^headers^", - "seek.ogv", - "seek.ogv^headers^", - "seek-short.ogv", - "seek-short.ogv^headers^", "seek.webm", "seek.webm^headers^", "seek-short.webm", @@ -653,8 +587,6 @@ support-files = [ "short-aac-encrypted-audio.mp4^headers^", "short-audio-fragmented-cenc-without-pssh.mp4", "short-audio-fragmented-cenc-without-pssh.mp4^headers^", - "short-video.ogv", - "short-video.ogv^headers^", "short-vp9-encrypted-video.mp4", "short-vp9-encrypted-video.mp4^headers^", "small-shot-mp3.mp4", diff --git a/dom/media/test/mochitest_stream.toml b/dom/media/test/mochitest_stream.toml index 0badfc52ab..429e694a96 100644 --- a/dom/media/test/mochitest_stream.toml +++ b/dom/media/test/mochitest_stream.toml @@ -27,12 +27,6 @@ tags = "mtg capturestream" support-files = [ "16bit_wave_extrametadata.wav", "16bit_wave_extrametadata.wav^headers^", - "320x240.ogv", - "320x240.ogv^headers^", - "448636.ogv", - "448636.ogv^headers^", - "A4.ogv", - "A4.ogv^headers^", "VID_0001.ogg", "VID_0001.ogg^headers^", "allowed.sjs", @@ -406,63 +400,19 @@ support-files = [ "bipbop_short_vp8.webm^headers^", "bipbop-lateaudio.mp4", "bipbop-lateaudio.mp4^headers^", - "black100x100-aspect3to2.ogv", - "black100x100-aspect3to2.ogv^headers^", "bogus.duh", - "bogus.ogv", - "bogus.ogv^headers^", "bogus.wav", "bogus.wav^headers^", - "bug461281.ogg", - "bug461281.ogg^headers^", - "bug482461-theora.ogv", - "bug482461-theora.ogv^headers^", - "bug482461.ogv", - "bug482461.ogv^headers^", - "bug495129.ogv", - "bug495129.ogv^headers^", "bug495794.ogg", "bug495794.ogg^headers^", - "bug498380.ogv", - "bug498380.ogv^headers^", - "bug498855-1.ogv", - "bug498855-1.ogv^headers^", - "bug498855-2.ogv", - "bug498855-2.ogv^headers^", - "bug498855-3.ogv", - "bug498855-3.ogv^headers^", - "bug499519.ogv", - "bug499519.ogv^headers^", - "bug500311.ogv", - "bug500311.ogv^headers^", "bug501279.ogg", "bug501279.ogg^headers^", - "bug504613.ogv", - "bug504613.ogv^headers^", - "bug504644.ogv", - "bug504644.ogv^headers^", - "bug504843.ogv", - "bug504843.ogv^headers^", - "bug506094.ogv", - "bug506094.ogv^headers^", - "bug516323.indexed.ogv", - "bug516323.indexed.ogv^headers^", - "bug516323.ogv", - "bug516323.ogv^headers^", "bug520493.ogg", "bug520493.ogg^headers^", "bug520500.ogg", "bug520500.ogg^headers^", - "bug520908.ogv", - "bug520908.ogv^headers^", - "bug523816.ogv", - "bug523816.ogv^headers^", "bug533822.ogg", "bug533822.ogg^headers^", - "bug556821.ogv", - "bug556821.ogv^headers^", - "bug557094.ogv", - "bug557094.ogv^headers^", "bug604067.webm", "bug604067.webm^headers^", "bug1066943.webm", @@ -481,14 +431,8 @@ support-files = [ "cancellable_request.sjs", "chain.ogg", "chain.ogg^headers^", - "chain.ogv", - "chain.ogv^headers^", "chain.opus", "chain.opus^headers^", - "chained-audio-video.ogg", - "chained-audio-video.ogg^headers^", - "chained-video.ogv", - "chained-video.ogv^headers^", "chromeHelper.js", "cloneElementVisually_helpers.js", "contentType.sjs", @@ -568,8 +512,6 @@ support-files = [ "invalid-preskip.webm^headers^", "manifest.js", "midflight-redirect.sjs", - "multiple-bos.ogg", - "multiple-bos.ogg^headers^", "multiple-bos-more-header-fileds.ogg", "multiple-bos-more-header-fileds.ogg^headers^", "multi_id3v2.mp3", @@ -624,14 +566,6 @@ support-files = [ "sample.3g2", "sample-encrypted-sgpdstbl-sbgptraf.mp4", "sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^", - "sample-fisbone-skeleton4.ogv", - "sample-fisbone-skeleton4.ogv^headers^", - "sample-fisbone-wrong-header.ogv", - "sample-fisbone-wrong-header.ogv^headers^", - "seek.ogv", - "seek.ogv^headers^", - "seek-short.ogv", - "seek-short.ogv^headers^", "seek.webm", "seek.webm^headers^", "seek-short.webm", @@ -653,8 +587,6 @@ support-files = [ "short-aac-encrypted-audio.mp4^headers^", "short-audio-fragmented-cenc-without-pssh.mp4", "short-audio-fragmented-cenc-without-pssh.mp4^headers^", - "short-video.ogv", - "short-video.ogv^headers^", "short-vp9-encrypted-video.mp4", "short-vp9-encrypted-video.mp4^headers^", "small-shot-mp3.mp4", @@ -763,11 +695,15 @@ support-files = [ "hls/960x720_seg1.ts", "sync.webm", ] +# This test requires software decoding on Android to be able to check that +# painting a video to the canvas throws. bug 1372457, bug 1526207 +prefs = [ + "media.android-media-codec.preferred=false", +] ["test_streams_capture_origin.html"] ["test_streams_element_capture.html"] -skip-if = ["os == 'android'"] # bug 1372457, bug 1526207 for drawImage ["test_streams_element_capture_mediatrack.html"] @@ -788,9 +724,7 @@ tags = "mtg" ["test_streams_srcObject.html"] skip-if = [ - "os == 'android'", # bug 1300443, android(bug 1232305) "os == 'mac' && debug", # Bug 1756880 - temp due to high frequency shutdown hang ] ["test_streams_tracks.html"] -skip-if = ["os == 'android'"] # android(bug 1232305) diff --git a/dom/media/test/multiple-bos.ogg b/dom/media/test/multiple-bos.ogg Binary files differdeleted file mode 100644 index 193200868e..0000000000 --- a/dom/media/test/multiple-bos.ogg +++ /dev/null diff --git a/dom/media/test/reftest/color_quads/reftest.list b/dom/media/test/reftest/color_quads/reftest.list index 63a538b78a..10b62dc817 100644 --- a/dom/media/test/reftest/color_quads/reftest.list +++ b/dom/media/test/reftest/color_quads/reftest.list @@ -18,7 +18,7 @@ fuzzy(16-51,5234-5622) fuzzy-if(swgl,32-38,1600-91746) fuzzy-if(useDrawSnapshot, fuzzy-if(winWidget&&swgl,0-20,0-5620) fuzzy-if(winWidget&&!swgl,0-1,0-78) fuzzy-if(Android,254-255,273680-273807) fuzzy-if(cocoaWidget,0-35,0-1947) fuzzy-if(cocoaWidget&&swgl,0-67,0-5451) fuzzy-if(appleSilicon,30-48,1760-187409) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.vp9.webm ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.webm fuzzy-if(winWidget,0-1,0-78) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.webm skip-if(winWidget&&isCoverageBuild) fuzzy(0-16,75-1941) fuzzy-if(Android,28-255,273680-359920) fuzzy-if(cocoaWidget,30-32,187326-187407) fuzzy-if(appleSilicon,30-48,1835-187409) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.h264.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.webm -fuzzy-if(winWidget&&swgl,0-20,0-5620) fuzzy-if(winWidget&&!swgl,0-1,0-78) fuzzy-if(Android,254-255,273680-273807) fuzzy-if(cocoaWidget,0-35,0-1947) fuzzy-if(cocoaWidget&&swgl,0-67,0-5451) fuzzy-if(appleSilicon,30-48,1760-187409) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.vp9.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.webm +fuzzy-if(winWidget&&swgl,0-20,0-5620) fuzzy-if(winWidget&&!swgl,0-1,0-78) skip-if(Android) fuzzy-if(cocoaWidget,0-35,0-1947) fuzzy-if(cocoaWidget&&swgl,0-67,0-5451) fuzzy-if(appleSilicon,30-48,1760-187409) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.vp9.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.webm skip-if(Android) fuzzy(16-48,8107-8818) fuzzy-if(winWidget&&swgl,31-38,8240-184080) fuzzy-if(appleSilicon,33-38,8819-11705) fuzzy-if(useDrawSnapshot,20-20,187200-187200) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.webm ../reftest_img.html?src=color_quads/720p.png skip-if(Android) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.webm diff --git a/dom/media/test/reftest/reftest.list b/dom/media/test/reftest/reftest.list index 0f709a35ee..bd4cb2d030 100644 --- a/dom/media/test/reftest/reftest.list +++ b/dom/media/test/reftest/reftest.list @@ -1,15 +1,15 @@ skip-if(Android) fuzzy-if(cocoaWidget,0-80,0-76800) fuzzy-if(appleSilicon,0-80,0-76800) fuzzy-if(winWidget,0-63,0-76799) fuzzy-if(gtkWidget,0-70,0-2032) HTTP(..) == short.mp4.firstframe.html short.mp4.firstframe-ref.html skip-if(Android) fuzzy-if(cocoaWidget,0-87,0-76797) fuzzy-if(appleSilicon,0-87,0-76797) fuzzy-if(winWidget,0-60,0-76797) fuzzy-if(gtkWidget,0-60,0-6070) HTTP(..) == short.mp4.lastframe.html short.mp4.lastframe-ref.html skip-if(Android) skip-if(cocoaWidget) skip-if(winWidget) fuzzy-if(gtkWidget,0-57,0-4282) fuzzy-if(cocoaWidget,55-80,4173-4417) HTTP(..) == bipbop_300_215kbps.mp4.lastframe.html bipbop_300_215kbps.mp4.lastframe-ref.html -skip-if(Android) fuzzy-if(cocoaWidget,0-25,0-175921) fuzzy-if(appleSilicon,34-34,40100-40100) fuzzy-if(winWidget,0-71,0-179198) HTTP(..) == gizmo.mp4.seek.html gizmo.mp4.55thframe-ref.html +skip-if(Android) fuzzy-if(cocoaWidget,0-25,0-175921) fuzzy-if(appleSilicon,34-34,40100-40100) fuzzy-if(winWidget,0-71,0-179198) fuzzy-if(gtkWidget,0-46,0-173482) HTTP(..) == gizmo.mp4.seek.html gizmo.mp4.55thframe-ref.html # Bug 1758718 skip-if(Android) skip-if(cocoaWidget) fuzzy(0-10,0-778236) == image-10bits-rendering-video.html image-10bits-rendering-ref.html skip-if(Android) fuzzy(0-10,0-778536) fuzzy-if(appleSilicon,0-37,0-699614) == image-10bits-rendering-90-video.html image-10bits-rendering-90-ref.html # Bug 1758718 skip-if(Android) fuzzy(0-27,0-573106) skip-if(cocoaWidget) == image-10bits-rendering-720-video.html image-10bits-rendering-720-ref.html skip-if(Android) fuzzy(0-31,0-573249) fuzzy-if(appleSilicon,0-37,0-543189) == image-10bits-rendering-720-90-video.html image-10bits-rendering-720-90-ref.html -skip-if(Android) fuzzy(0-84,0-771156) fails-if(useDrawSnapshot) == uneven_frame_duration_video.html uneven_frame_duration_video-ref.html # Skip on Windows 7 as the resolution of the video is too high for test machines and will fail in the decoder. +skip-if(Android) fuzzy(0-84,0-774213) fails-if(useDrawSnapshot) == uneven_frame_duration_video.html uneven_frame_duration_video-ref.html # Skip on Windows 7 as the resolution of the video is too high for test machines and will fail in the decoder. # Set media.dormant-on-pause-timeout-ms to avoid decoders becoming dormant and busting test, skip on android as test is too noisy and unstable skip-if(Android) pref(media.dormant-on-pause-timeout-ms,-1) fuzzy(0-20,0-500) == frame_order_mp4.html frame_order_mp4-ref.html skip-if(Android) fuzzy(0-30,0-270000) == incorrect_display_in_bytestream_vp8.html incorrect_display_in_bytestream_vp8-ref.html -skip-if(Android) fuzzy(0-22,0-377335) == incorrect_display_in_bytestream_vp9.html incorrect_display_in_bytestream_vp9-ref.html +skip-if(Android) fuzzy(0-22,0-381481) == incorrect_display_in_bytestream_vp9.html incorrect_display_in_bytestream_vp9-ref.html diff --git a/dom/media/test/sample-fisbone-skeleton4.ogv b/dom/media/test/sample-fisbone-skeleton4.ogv Binary files differdeleted file mode 100644 index 8afe0be7a4..0000000000 --- a/dom/media/test/sample-fisbone-skeleton4.ogv +++ /dev/null diff --git a/dom/media/test/sample-fisbone-wrong-header.ogv b/dom/media/test/sample-fisbone-wrong-header.ogv Binary files differdeleted file mode 100644 index 46c3933da5..0000000000 --- a/dom/media/test/sample-fisbone-wrong-header.ogv +++ /dev/null diff --git a/dom/media/test/seek-short.ogv b/dom/media/test/seek-short.ogv Binary files differdeleted file mode 100644 index a5ca6951d0..0000000000 --- a/dom/media/test/seek-short.ogv +++ /dev/null diff --git a/dom/media/test/seek.ogv b/dom/media/test/seek.ogv Binary files differdeleted file mode 100644 index ac7ece3519..0000000000 --- a/dom/media/test/seek.ogv +++ /dev/null diff --git a/dom/media/test/seekLies.sjs b/dom/media/test/seekLies.sjs index 4fc528a0a5..3b2b19921a 100644 --- a/dom/media/test/seekLies.sjs +++ b/dom/media/test/seekLies.sjs @@ -6,7 +6,7 @@ function handleRequest(request, response) { var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance( Ci.nsIBinaryInputStream ); - var paths = "tests/dom/media/test/seek.ogv"; + var paths = "tests/dom/media/test/vp9.webm"; var split = paths.split("/"); for (var i = 0; i < split.length; ++i) { file.append(split[i]); @@ -15,7 +15,7 @@ function handleRequest(request, response) { bis.setInputStream(fis); var bytes = bis.readBytes(bis.available()); response.setHeader("Content-Length", "" + bytes.length, false); - response.setHeader("Content-Type", "video/ogg", false); + response.setHeader("Content-Type", "video/webm", false); response.setHeader("Accept-Ranges", "bytes", false); response.write(bytes, bytes.length); bis.close(); diff --git a/dom/media/test/short-video.ogv b/dom/media/test/short-video.ogv Binary files differdeleted file mode 100644 index 68dee3cf2b..0000000000 --- a/dom/media/test/short-video.ogv +++ /dev/null diff --git a/dom/media/test/test_bug1248229.html b/dom/media/test/test_bug1248229.html index 3165795622..e0ca1c96b5 100644 --- a/dom/media/test/test_bug1248229.html +++ b/dom/media/test/test_bug1248229.html @@ -7,7 +7,7 @@ <script type="text/javascript" src="manifest.js"></script> </head> <body onload="doTest()"> -<video id="v" src="black100x100-aspect3to2.ogv"></video> +<video id="v" src="vp9.webm"></video> <pre id="test"> <script class="testbody" type="text/javascript"> SimpleTest.waitForExplicitFinish(); diff --git a/dom/media/test/test_closing_connections.html b/dom/media/test/test_closing_connections.html index c5eb565447..fc438d8531 100644 --- a/dom/media/test/test_closing_connections.html +++ b/dom/media/test/test_closing_connections.html @@ -32,7 +32,7 @@ window.onload = function() { we've got the first frame. */ -var resource = getPlayableVideo(gClosingConnectionsTest); +var resource = getPlayableVideo(gPlayTests); SimpleTest.waitForExplicitFinish(); function beginTest() { diff --git a/dom/media/test/test_decoder_disable.html b/dom/media/test/test_decoder_disable.html index dd0d2cc51b..d29b62a5fa 100644 --- a/dom/media/test/test_decoder_disable.html +++ b/dom/media/test/test_decoder_disable.html @@ -66,10 +66,10 @@ function videoError(event, id) { </div> <script> function makeVideos() { - document.getElementById('content').innerHTML = '<video id="video1" preload="metadata"><source type="video/ogg" src="320x240.ogv?decoder_disabled=1" onerror="videoError(event, \'video1\');"/><source type="audio/wave" src="r11025_u8_c1.wav?decoder_disabled=1" id=\'s2\' onerror="videoError(event, \'video1\');"/></video><video id="video2" preload="metadata" src="320x240.ogv?decoder_disabled=2" onerror="videoError(event, \'video2\');"></video><video id="video3" preload="metadata" src="r11025_u8_c1.wav?decoder_disabled=2" onerror="videoError(event, \'video3\');"></video>'; + document.getElementById('content').innerHTML = '<video id="video1" preload="metadata"><source type="video/webm" src="vp9.webm?decoder_disabled=1" onerror="videoError(event, \'video1\');"/><source type="audio/wave" src="r11025_u8_c1.wav?decoder_disabled=1" id=\'s2\' onerror="videoError(event, \'video1\');"/></video><video id="video2" preload="metadata" src="vp9.webm?decoder_disabled=2" onerror="videoError(event, \'video2\');"></video><video id="video3" preload="metadata" src="r11025_u8_c1.wav?decoder_disabled=2" onerror="videoError(event, \'video3\');"></video>'; } -SpecialPowers.pushPrefEnv({"set": [["media.ogg.enabled", false], ["media.wave.enabled", false]]}, makeVideos); +SpecialPowers.pushPrefEnv({"set": [["media.webm.enabled", false], ["media.wave.enabled", false]]}, makeVideos); </script> </pre> diff --git a/dom/media/test/test_error_in_video_document.html b/dom/media/test/test_error_in_video_document.html index e376ea95e3..b48e7745d0 100644 --- a/dom/media/test/test_error_in_video_document.html +++ b/dom/media/test/test_error_in_video_document.html @@ -31,7 +31,7 @@ function check() { // Debug info for Bug 608634 ok(true, "iframe src=" + document.body.getElementsByTagName("iframe")[0].src); - is(v.readyState, v.HAVE_NOTHING, "Ready state"); + is(v.readyState, v.HAVE_NOTHING, "Ready state for " + document.body.getElementsByTagName("iframe")[0].src); isnot(v.error, null, "Error object"); is(v.networkState, v.NETWORK_NO_SOURCE, "Network state"); @@ -40,15 +40,16 @@ function check() { } // Find an error test that we'd think we should be able to play (if it -// wasn't already known to fail). -var t = getPlayableVideo(gErrorTests); +// wasn't already known to fail). This needs to fail early: for example, +// incorrect metadata, not correct metadata but incorrect encoded packets. +var t = "bug1535980.webm"; if (!t) { todo(false, "No types supported"); } else { SimpleTest.waitForExplicitFinish(); var f = document.createElement("iframe"); - f.src = t.name; + f.src = t; f.addEventListener("load", check); document.body.appendChild(f); } diff --git a/dom/media/test/test_load_same_resource.html b/dom/media/test/test_load_same_resource.html index f3e6992e8c..d3351226cd 100644 --- a/dom/media/test/test_load_same_resource.html +++ b/dom/media/test/test_load_same_resource.html @@ -78,7 +78,7 @@ async function initTest(test, token) { e.token = token; manager.started(token); - // Since 320x240.ogv is less than 32KB, we need to wait for the + // Since 320x240.webm is less than 32KB, we need to wait for the // 'suspend' event to ensure the partial block is flushed to the cache // otherwise the cloned resource will create a new channel when it // has no data to read at position 0. The new channel will download diff --git a/dom/media/test/test_media_selection.html b/dom/media/test/test_media_selection.html index 33ecabfd58..72bde2dd9e 100644 --- a/dom/media/test/test_media_selection.html +++ b/dom/media/test/test_media_selection.html @@ -111,7 +111,7 @@ for (var i = 0; i < gSmallTests.length; ++i) { checkMetadata(t.name, e, t); }}(test); - var otherType = type.match(/^video\//) ? "audio/x-wav" : "video/ogg"; + var otherType = type.match(/^video\//) ? "audio/x-wav" : "video/webm"; subtests.push(maketest(set_src, src, null, check), maketest(add_source, src, null, check), maketest(add_source, src, type, check), diff --git a/dom/media/test/test_preload_suspend.html b/dom/media/test/test_preload_suspend.html index b715a58dc8..5fefe251ae 100644 --- a/dom/media/test/test_preload_suspend.html +++ b/dom/media/test/test_preload_suspend.html @@ -89,7 +89,7 @@ function startTest(test) { var v = document.createElement("video"); v.name = test.name; var key = Math.random(); - v.src = "seek.ogv?key=" + key + "&id=" + v.name; + v.src = "vp9.webm?key=" + key + "&id=" + v.name; v.preload = test.preload; v.suspendCount = 0; v.expectedSuspendCount = test.expectedSuspendCount; diff --git a/dom/media/test/test_standalone.html b/dom/media/test/test_standalone.html index 620878a394..3b61f66d0a 100644 --- a/dom/media/test/test_standalone.html +++ b/dom/media/test/test_standalone.html @@ -6,56 +6,41 @@ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> <script type="text/javascript" src="manifest.js"></script> </head> -<body onload="doTest()"> - -<pre id="test"> <script class="testbody" type="text/javascript"> -var iframes = []; - -for (let i=0; i<gSmallTests.length; ++i) { - var test = gSmallTests[i]; +// Test whether video can be played correctly in a video document +add_task(async function testStandAloneVideoDocument() { + for (let i=0; i<gSmallTests.length; ++i) { + const test = gSmallTests[i]; - // We can't play WAV files in stand alone documents, so just don't - // run the test on non-video content types. - var tag = getMajorMimeType(test.type); - if (tag != "video" || !document.createElement("video").canPlayType(test.type)) - continue; + // We can't play WAV files in stand alone documents, so just don't + // run the test on non-video content types. + if (getMajorMimeType(test.type) != "video" || + !document.createElement("video").canPlayType(test.type)) { + continue; + } - let f = document.createElement("iframe"); - f.src = test.name; - f._test = test; - f.id = "frame" + i; - iframes.push(f); - document.body.appendChild(f); -} + let f = document.createElement("iframe"); + f.src = test.name; + document.body.appendChild(f); + info(`waiting iframe loading ${test.name}`); + await new Promise(r => f.onload = r); -function filename(uri) { - return uri.substr(uri.lastIndexOf("/")+1); -} - -function doTest() -{ - for (let i=0; i<iframes.length; ++i) { - let f = document.getElementById(iframes[i].id); - var v = f.contentDocument.body.firstChild; + const v = f.contentDocument.body.firstChild; is(v.tagName.toLowerCase(), "video", "Is video element"); - var src = filename(v.currentSrc); - is(src, iframes[i]._test.name, "Name ("+src+") should match ("+iframes[i]._test.name+")"); - is(v.controls, true, "Controls set (" + src + ")"); - is(v.autoplay, true, "Autoplay set (" + src + ")"); + const src = filename(v.currentSrc); + is(src, test.name, `Name (${src}) should match (${test.name})`); + is(v.controls, true, `Controls set (${src})`); + is(v.autoplay, true, `Autoplay set (${src})`); } - SimpleTest.finish(); -} +}); -if (!iframes.length) { - todo(false, "No types supported"); -} else { - SimpleTest.waitForExplicitFinish(); +// Helper function +function filename(uri) { + return uri.substr(uri.lastIndexOf("/")+1); } </script> -</pre> </body> </html> diff --git a/dom/media/test/test_streams_element_capture.html b/dom/media/test/test_streams_element_capture.html index 098136dba7..8695dbf63b 100644 --- a/dom/media/test/test_streams_element_capture.html +++ b/dom/media/test/test_streams_element_capture.html @@ -109,6 +109,11 @@ async function startTest(test, token) { let tests = gPlayTests; // Filter out bug1377278.webm due to bug 1541401. tests = tests.filter(t => !t.name.includes("1377278")); + // bug 1372457, bug 1526207 for drawImage -- restrict to codecs that can be + // decoded in software. + if (navigator.userAgent.includes("Android")) { + tests = tests.filter(t => !t.name.includes("mp4")); + } manager.runTests(tests, async (test, token) => { try { diff --git a/dom/media/test/test_streams_element_capture_twice.html b/dom/media/test/test_streams_element_capture_twice.html index 0e30be1801..f180fd12fd 100644 --- a/dom/media/test/test_streams_element_capture_twice.html +++ b/dom/media/test/test_streams_element_capture_twice.html @@ -66,7 +66,7 @@ async function startTest(src) { (async function() { try { - await startTest("short-video.ogv"); + await startTest("vp9cake.webm"); } catch(e) { ok(false, `Caught error: ${e}${e.stack ? '\n' + e.stack : ''}`); } finally { diff --git a/dom/media/test/test_videoDocumentTitle.html b/dom/media/test/test_videoDocumentTitle.html index dd52dba26c..b03c41a30c 100644 --- a/dom/media/test/test_videoDocumentTitle.html +++ b/dom/media/test/test_videoDocumentTitle.html @@ -21,8 +21,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=463830 /** Test for Bug 463830 **/ var gTests = [ - { file: "320x240.ogv", title: "320x240.ogv" }, - { file: "bug461281.ogg", title: "bug461281.ogg" }, + { file: "vp9.webm", title: "vp9.webm" }, + { file: "vp9cake.webm", title: "vp9cake.webm" }, ]; var gTestNum = 0; diff --git a/dom/media/test/test_video_stats_resistfingerprinting.html b/dom/media/test/test_video_stats_resistfingerprinting.html index 2bc239b367..90c8d2a3d3 100644 --- a/dom/media/test/test_video_stats_resistfingerprinting.html +++ b/dom/media/test/test_video_stats_resistfingerprinting.html @@ -35,7 +35,6 @@ https://trac.torproject.org/projects/tor/ticket/15757 ["privacy.resistFingerprinting.target_video_res", 240] ); var testCases = [ - { name:"320x240.ogv", type:"video/ogg", width:320, height:240, duration:0.266, drop: false }, { name:"seek.webm", type:"video/webm", width:320, height:240, duration:3.966, drop: false }, { name:"gizmo.mp4", type:"video/mp4", width:560, height:320, duration:5.56, drop: true } ]; diff --git a/dom/media/utils/PerformanceRecorder.cpp b/dom/media/utils/PerformanceRecorder.cpp index d6124e8cf6..3dc2c24a5d 100644 --- a/dom/media/utils/PerformanceRecorder.cpp +++ b/dom/media/utils/PerformanceRecorder.cpp @@ -140,6 +140,8 @@ static void AppendImageFormatToName(nsCString& aName, return "gbrp,"; case DecodeStage::ANDROID_SURFACE: return "android.Surface,"; + case DecodeStage::VAAPI_SURFACE: + return "VAAPI.Surface,"; } MOZ_ASSERT_UNREACHABLE("Unhandled DecodeStage::ImageFormat"); return ""; diff --git a/dom/media/utils/PerformanceRecorder.h b/dom/media/utils/PerformanceRecorder.h index 34c6676b01..e423c3fb5d 100644 --- a/dom/media/utils/PerformanceRecorder.h +++ b/dom/media/utils/PerformanceRecorder.h @@ -216,6 +216,7 @@ class DecodeStage { RGB24, GBRP, ANDROID_SURFACE, + VAAPI_SURFACE, }; DecodeStage(nsCString aSource, TrackingId aTrackingId, MediaInfoFlag aFlag) diff --git a/dom/media/utils/TelemetryProbesReporter.cpp b/dom/media/utils/TelemetryProbesReporter.cpp index 8c5614c048..377cee9abc 100644 --- a/dom/media/utils/TelemetryProbesReporter.cpp +++ b/dom/media/utils/TelemetryProbesReporter.cpp @@ -293,23 +293,30 @@ void TelemetryProbesReporter::OnDecodeResumed() { } void TelemetryProbesReporter::OntFirstFrameLoaded( - const TimeDuration& aLoadedFirstFrameTime, bool aIsMSE, - bool aIsExternalEngineStateMachine) { - const MediaInfo& info = mOwner->GetMediaInfo(); - MOZ_ASSERT(info.HasVideo()); + const double aLoadedFirstFrameTime, const double aLoadedMetadataTime, + const double aTotalWaitingDataTime, const double aTotalBufferingTime, + const FirstFrameLoadedFlagSet aFlags, const MediaInfo& aInfo) { + MOZ_ASSERT(aInfo.HasVideo()); nsCString resolution; - DetermineResolutionForTelemetry(info, resolution); + DetermineResolutionForTelemetry(aInfo, resolution); + + const bool isMSE = aFlags.contains(FirstFrameLoadedFlag::IsMSE); + const bool isExternalEngineStateMachine = + aFlags.contains(FirstFrameLoadedFlag::IsExternalEngineStateMachine); glean::media_playback::FirstFrameLoadedExtra extraData; - extraData.firstFrameLoadedTime = Some(aLoadedFirstFrameTime.ToMilliseconds()); - if (!aIsMSE && !aIsExternalEngineStateMachine) { + extraData.firstFrameLoadedTime = Some(aLoadedFirstFrameTime); + extraData.metadataLoadedTime = Some(aLoadedMetadataTime); + extraData.totalWaitingDataTime = Some(aTotalWaitingDataTime); + extraData.bufferingTime = Some(aTotalBufferingTime); + if (!isMSE && !isExternalEngineStateMachine) { extraData.playbackType = Some("Non-MSE playback"_ns); - } else if (aIsMSE && !aIsExternalEngineStateMachine) { + } else if (isMSE && !isExternalEngineStateMachine) { extraData.playbackType = !mOwner->IsEncrypted() ? Some("MSE playback"_ns) : Some("EME playback"_ns); - } else if (!aIsMSE && aIsExternalEngineStateMachine) { + } else if (!isMSE && isExternalEngineStateMachine) { extraData.playbackType = Some("Non-MSE media-engine playback"_ns); - } else if (aIsMSE && aIsExternalEngineStateMachine) { + } else if (isMSE && isExternalEngineStateMachine) { extraData.playbackType = !mOwner->IsEncrypted() ? Some("MSE media-engine playback"_ns) : Some("EME media-engine playback"_ns); @@ -317,18 +324,35 @@ void TelemetryProbesReporter::OntFirstFrameLoaded( extraData.playbackType = Some("ERROR TYPE"_ns); MOZ_ASSERT(false, "Unexpected playback type!"); } - extraData.videoCodec = Some(info.mVideo.mMimeType); + extraData.videoCodec = Some(aInfo.mVideo.mMimeType); extraData.resolution = Some(resolution); if (const auto keySystem = mOwner->GetKeySystem()) { extraData.keySystem = Some(NS_ConvertUTF16toUTF8(*keySystem)); } + if (aFlags.contains(FirstFrameLoadedFlag::IsHardwareDecoding)) { + extraData.isHardwareDecoding = Some(true); + } + +#ifdef MOZ_WIDGET_ANDROID + if (aFlags.contains(FirstFrameLoadedFlag::IsHLS)) { + extraData.hlsDecoder = Some(true); + } +#endif if (MOZ_LOG_TEST(gTelemetryProbesReporterLog, LogLevel::Debug)) { nsPrintfCString logMessage{ - "Media_Playabck First_Frame_Loaded event, time(ms)=%f, " - "playback-type=%s, videoCodec=%s, resolution=%s", - aLoadedFirstFrameTime.ToMilliseconds(), extraData.playbackType->get(), - extraData.videoCodec->get(), extraData.resolution->get()}; + "Media_Playabck First_Frame_Loaded event, time(ms)=[" + "full:%f, loading-meta:%f, waiting-data:%f, buffering:%f], " + "playback-type=%s, " + "videoCodec=%s, resolution=%s, hardware=%d", + aLoadedFirstFrameTime, + aLoadedMetadataTime, + aTotalWaitingDataTime, + aTotalBufferingTime, + extraData.playbackType->get(), + extraData.videoCodec->get(), + extraData.resolution->get(), + aFlags.contains(FirstFrameLoadedFlag::IsHardwareDecoding)}; if (const auto keySystem = mOwner->GetKeySystem()) { logMessage.Append(nsPrintfCString{ ", keySystem=%s", NS_ConvertUTF16toUTF8(*keySystem).get()}); @@ -336,6 +360,7 @@ void TelemetryProbesReporter::OntFirstFrameLoaded( LOG("%s", logMessage.get()); } glean::media_playback::first_frame_loaded.Record(Some(extraData)); + mOwner->DispatchAsyncTestingEvent(u"mozfirstframeloadedprobe"_ns); } void TelemetryProbesReporter::OnShutdown() { @@ -465,6 +490,7 @@ void TelemetryProbesReporter::ReportResultForVideo() { SECONDS_TO_MS(totalVideoPlayTimeS)); } + // TODO: deprecate the old probes. // Report result for video using CDM auto keySystem = mOwner->GetKeySystem(); if (keySystem) { @@ -519,6 +545,10 @@ void TelemetryProbesReporter::ReportResultForVideo() { ReportResultForMFCDMPlaybackIfNeeded(totalVideoPlayTimeS, key); } #endif + if (keySystem) { + ReportPlaytimeForKeySystem(*keySystem, totalVideoPlayTimeS, + info.mVideo.mMimeType, key); + } } #ifdef MOZ_WMF_CDM @@ -564,6 +594,17 @@ void TelemetryProbesReporter::ReportResultForMFCDMPlaybackIfNeeded( } #endif +void TelemetryProbesReporter::ReportPlaytimeForKeySystem( + const nsAString& aKeySystem, const double aTotalPlayTimeS, + const nsCString& aCodec, const nsCString& aResolution) { + glean::mediadrm::EmePlaybackExtra extra = { + .keySystem = Some(NS_ConvertUTF16toUTF8(aKeySystem)), + .playedTime = Some(aTotalPlayTimeS), + .resolution = Some(aResolution), + .videoCodec = Some(aCodec)}; + glean::mediadrm::eme_playback.Record(Some(extra)); +} + void TelemetryProbesReporter::ReportResultForAudio() { // Don't record telemetry for a media that didn't have a valid audio or video // to play, or hasn't played. diff --git a/dom/media/utils/TelemetryProbesReporter.h b/dom/media/utils/TelemetryProbesReporter.h index be81e8022c..43e05dcadd 100644 --- a/dom/media/utils/TelemetryProbesReporter.h +++ b/dom/media/utils/TelemetryProbesReporter.h @@ -8,6 +8,7 @@ #include "MediaInfo.h" #include "mozilla/Maybe.h" #include "mozilla/AwakeTimeStamp.h" +#include "mozilla/EnumSet.h" #include "AudioChannelService.h" #include "nsISupportsImpl.h" @@ -66,8 +67,20 @@ class TelemetryProbesReporter final { void OnMutedChanged(bool aMuted); void OnDecodeSuspended(); void OnDecodeResumed(); - void OntFirstFrameLoaded(const TimeDuration& aLoadedFirstFrameTime, - bool aIsMSE, bool aIsExternalEngineStateMachine); + + enum class FirstFrameLoadedFlag { + IsMSE, + IsExternalEngineStateMachine, + IsHLS, + IsHardwareDecoding, + }; + using FirstFrameLoadedFlagSet = EnumSet<FirstFrameLoadedFlag, uint8_t>; + void OntFirstFrameLoaded(const double aLoadedFirstFrameTime, + const double aLoadedMetadataTime, + const double aTotalWaitingDataTime, + const double aTotalBufferingTime, + const FirstFrameLoadedFlagSet aFlags, + const MediaInfo& aInfo); double GetTotalVideoPlayTimeInSeconds() const; double GetTotalVideoHDRPlayTimeInSeconds() const; @@ -100,7 +113,10 @@ class TelemetryProbesReporter final { void ReportResultForMFCDMPlaybackIfNeeded(double aTotalPlayTimeS, const nsCString& aResolution); #endif - + void ReportPlaytimeForKeySystem(const nsAString& aKeySystem, + const double aTotalPlayTimeS, + const nsCString& aCodec, + const nsCString& aResolution); // Helper class to measure times for playback telemetry stats class TimeDurationAccumulator { public: diff --git a/dom/media/webaudio/MediaStreamAudioSourceNode.cpp b/dom/media/webaudio/MediaStreamAudioSourceNode.cpp index a915e78859..4ade20d16d 100644 --- a/dom/media/webaudio/MediaStreamAudioSourceNode.cpp +++ b/dom/media/webaudio/MediaStreamAudioSourceNode.cpp @@ -23,12 +23,14 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaStreamAudioSourceNode) tmp->Destroy(); NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputStream) NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputTrack) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mListener) NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(AudioNode) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaStreamAudioSourceNode, AudioNode) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputStream) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputTrack) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListener) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaStreamAudioSourceNode) @@ -65,12 +67,13 @@ already_AddRefed<MediaStreamAudioSourceNode> MediaStreamAudioSourceNode::Create( void MediaStreamAudioSourceNode::Init(DOMMediaStream& aMediaStream, ErrorResult& aRv) { + mListener = new TrackListener(this); mInputStream = &aMediaStream; AudioNodeEngine* engine = new MediaStreamAudioSourceNodeEngine(this); mTrack = AudioNodeExternalInputTrack::Create(Context()->Graph(), engine); mInputStream->AddConsumerToKeepAlive(ToSupports(this)); - mInputStream->RegisterTrackListener(this); + mInputStream->RegisterTrackListener(mListener); if (mInputStream->Audible()) { NotifyAudible(); } @@ -79,8 +82,9 @@ void MediaStreamAudioSourceNode::Init(DOMMediaStream& aMediaStream, void MediaStreamAudioSourceNode::Destroy() { if (mInputStream) { - mInputStream->UnregisterTrackListener(this); + mInputStream->UnregisterTrackListener(mListener); mInputStream = nullptr; + mListener = nullptr; } DetachFromTrack(); } @@ -275,4 +279,14 @@ JSObject* MediaStreamAudioSourceNode::WrapObject( return MediaStreamAudioSourceNode_Binding::Wrap(aCx, this, aGivenProto); } +NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaStreamAudioSourceNode::TrackListener, + DOMMediaStream::TrackListener, mNode) +NS_IMPL_ADDREF_INHERITED(MediaStreamAudioSourceNode::TrackListener, + DOMMediaStream::TrackListener) +NS_IMPL_RELEASE_INHERITED(MediaStreamAudioSourceNode::TrackListener, + DOMMediaStream::TrackListener) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + MediaStreamAudioSourceNode::TrackListener) +NS_INTERFACE_MAP_END_INHERITING(DOMMediaStream::TrackListener) + } // namespace mozilla::dom diff --git a/dom/media/webaudio/MediaStreamAudioSourceNode.h b/dom/media/webaudio/MediaStreamAudioSourceNode.h index 1875fc2e83..82ef67d4b7 100644 --- a/dom/media/webaudio/MediaStreamAudioSourceNode.h +++ b/dom/media/webaudio/MediaStreamAudioSourceNode.h @@ -40,7 +40,6 @@ class MediaStreamAudioSourceNodeEngine final : public AudioNodeEngine { class MediaStreamAudioSourceNode : public AudioNode, - public DOMMediaStream::TrackListener, public PrincipalChangeObserver<MediaStreamTrack> { public: static already_AddRefed<MediaStreamAudioSourceNode> Create( @@ -87,9 +86,28 @@ class MediaStreamAudioSourceNode ErrorResult& aRv); // From DOMMediaStream::TrackListener. - void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override; - void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override; - void NotifyAudible() override; + void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack); + void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack); + void NotifyAudible(); + + class TrackListener final : public DOMMediaStream::TrackListener { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TrackListener, + DOMMediaStream::TrackListener) + explicit TrackListener(MediaStreamAudioSourceNode* aNode) : mNode(aNode) {} + void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override { + mNode->NotifyTrackAdded(aTrack); + } + void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override { + mNode->NotifyTrackRemoved(aTrack); + } + void NotifyAudible() override { mNode->NotifyAudible(); } + + private: + virtual ~TrackListener() = default; + RefPtr<MediaStreamAudioSourceNode> mNode; + }; // From PrincipalChangeObserver<MediaStreamTrack>. void PrincipalChanged(MediaStreamTrack* aMediaStreamTrack) override; @@ -120,6 +138,7 @@ class MediaStreamAudioSourceNode // On construction we set this to the first audio track of mInputStream. RefPtr<MediaStreamTrack> mInputTrack; + RefPtr<TrackListener> mListener; }; } // namespace mozilla::dom diff --git a/dom/media/webaudio/test/mochitest_audio.toml b/dom/media/webaudio/test/mochitest_audio.toml index 56e612b102..1f037c0ec6 100644 --- a/dom/media/webaudio/test/mochitest_audio.toml +++ b/dom/media/webaudio/test/mochitest_audio.toml @@ -6,7 +6,6 @@ support-files = [ "audio-mono-expected-2.wav", "audio-mono-expected.wav", "audio-quad.wav", - "audio.ogv", "audiovideo.mp4", "audioBufferSourceNodeDetached_worker.js", "corsServer.sjs", diff --git a/dom/media/webaudio/test/test_mediaDecoding.html b/dom/media/webaudio/test/test_mediaDecoding.html index e76a533e4a..c2105b53b9 100644 --- a/dom/media/webaudio/test/test_mediaDecoding.html +++ b/dom/media/webaudio/test/test_mediaDecoding.html @@ -197,18 +197,6 @@ { url: "invalid.txt", valid: false, sampleRate: 44100 }, // A webm file with no audio { url: "noaudio.webm", valid: false, sampleRate: 48000 }, - // A video ogg file with audio - { - url: "audio.ogv", - valid: true, - expectedUrl: "audio-expected.wav", - numberOfChannels: 2, - sampleRate: 44100, - frames: 47680, - duration: 1.0807, - fuzzTolerance: 106, - fuzzToleranceMobile: 3482 - }, { url: "nil-packet.ogg", expectedUrl: null, diff --git a/dom/media/webcodecs/AudioData.cpp b/dom/media/webcodecs/AudioData.cpp index 0b21798be8..aae58fb32c 100644 --- a/dom/media/webcodecs/AudioData.cpp +++ b/dom/media/webcodecs/AudioData.cpp @@ -151,25 +151,6 @@ JSObject* AudioData::WrapObject(JSContext* aCx, return AudioData_Binding::Wrap(aCx, this, aGivenProto); } -uint32_t BytesPerSamples(const mozilla::dom::AudioSampleFormat& aFormat) { - switch (aFormat) { - case AudioSampleFormat::U8: - case AudioSampleFormat::U8_planar: - return sizeof(uint8_t); - case AudioSampleFormat::S16: - case AudioSampleFormat::S16_planar: - return sizeof(int16_t); - case AudioSampleFormat::S32: - case AudioSampleFormat::F32: - case AudioSampleFormat::S32_planar: - case AudioSampleFormat::F32_planar: - return sizeof(float); - default: - MOZ_ASSERT_UNREACHABLE("wrong enum value"); - } - return 0; -} - Result<Ok, nsCString> IsValidAudioDataInit(const AudioDataInit& aInit) { if (aInit.mSampleRate <= 0.0) { auto msg = nsLiteralCString("sampleRate must be positive"); @@ -205,37 +186,13 @@ Result<Ok, nsCString> IsValidAudioDataInit(const AudioDataInit& aInit) { return Ok(); } -const char* FormatToString(AudioSampleFormat aFormat) { - switch (aFormat) { - case AudioSampleFormat::U8: - return "u8"; - case AudioSampleFormat::S16: - return "s16"; - case AudioSampleFormat::S32: - return "s32"; - case AudioSampleFormat::F32: - return "f32"; - case AudioSampleFormat::U8_planar: - return "u8-planar"; - case AudioSampleFormat::S16_planar: - return "s16-planar"; - case AudioSampleFormat::S32_planar: - return "s32-planar"; - case AudioSampleFormat::F32_planar: - return "f32-planar"; - default: - MOZ_ASSERT_UNREACHABLE("wrong enum value"); - } - return "unsupported"; -} - /* static */ already_AddRefed<AudioData> AudioData::Constructor(const GlobalObject& aGlobal, const AudioDataInit& aInit, ErrorResult& aRv) { nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); LOGD("[%p] AudioData(fmt: %s, rate: %f, ch: %" PRIu32 ", ts: %" PRId64 ")", - global.get(), FormatToString(aInit.mFormat), aInit.mSampleRate, + global.get(), GetEnumString(aInit.mFormat).get(), aInit.mSampleRate, aInit.mNumberOfChannels, aInit.mTimestamp); if (!global) { LOGE("Global unavailable"); @@ -311,6 +268,9 @@ struct CopyToSpec { const uint32_t mFrameOffset; const uint32_t mPlaneIndex; const AudioSampleFormat mFormat; + // False if this is used internally, and this copy call doesn't come from + // script. + DebugOnly<bool> mFromScript = true; }; bool IsInterleaved(const AudioSampleFormat& aFormat) { @@ -463,7 +423,7 @@ void CopySamples(Span<S> aSource, Span<D> aDest, uint32_t aSourceChannelCount, } if (!IsInterleaved(aSourceFormat) && IsInterleaved(aCopyToSpec.mFormat)) { - MOZ_CRASH("This should never be hit -- current spec doesn't support it"); + MOZ_ASSERT(!aCopyToSpec.mFromScript); // Planar to interleaved -- copy of all channels of the source into the // destination buffer. MOZ_ASSERT(aCopyToSpec.mPlaneIndex == 0); @@ -505,7 +465,7 @@ nsCString AudioData::ToString() const { return nsPrintfCString("AudioData[%zu bytes %s %fHz %" PRIu32 "x%" PRIu32 "ch]", mResource->Data().LengthBytes(), - FormatToString(mAudioSampleFormat.value()), + GetEnumString(mAudioSampleFormat.value()).get(), mSampleRate, mNumberOfFrames, mNumberOfChannels); } @@ -515,8 +475,9 @@ nsCString CopyToToString(size_t aDestBufSize, "AudioDataCopyToOptions[data: %zu bytes %s frame count:%" PRIu32 " frame offset: %" PRIu32 " plane: %" PRIu32 "]", aDestBufSize, - aOptions.mFormat.WasPassed() ? FormatToString(aOptions.mFormat.Value()) - : "null", + aOptions.mFormat.WasPassed() + ? GetEnumString(aOptions.mFormat.Value()).get() + : "null", aOptions.mFrameCount.WasPassed() ? aOptions.mFrameCount.Value() : 0, aOptions.mFrameOffset, aOptions.mPlaneIndex); } @@ -650,6 +611,8 @@ void AudioData::Close() { mAudioSampleFormat = Nothing(); } +bool AudioData::IsClosed() const { return !mResource; } + // https://w3c.github.io/webcodecs/#ref-for-deserialization-steps%E2%91%A1 /* static */ JSObject* AudioData::ReadStructuredClone(JSContext* aCx, @@ -724,6 +687,31 @@ void AudioData::CloseIfNeeded() { } } +RefPtr<mozilla::AudioData> AudioData::ToAudioData() const { + // Always convert to f32 interleaved for now, as this Gecko's prefered + // internal audio representation for encoding and decoding. + Span<uint8_t> data = mResource->Data(); + DebugOnly<uint32_t> frames = mNumberOfFrames; + uint32_t bytesPerSample = BytesPerSamples(mAudioSampleFormat.value()); + uint32_t samples = data.Length() / bytesPerSample; + DebugOnly<uint32_t> computedFrames = samples / mNumberOfChannels; + MOZ_ASSERT(frames == computedFrames); + AlignedAudioBuffer buf(samples); + Span<uint8_t> storage(reinterpret_cast<uint8_t*>(buf.Data()), + samples * sizeof(float)); + + CopyToSpec spec(mNumberOfFrames, 0, 0, AudioSampleFormat::F32); +#ifdef DEBUG + spec.mFromScript = false; +#endif + + DoCopy(data, storage, mNumberOfChannels, mAudioSampleFormat.value(), spec); + + return MakeRefPtr<mozilla::AudioData>( + 0, media::TimeUnit::FromMicroseconds(mTimestamp), std::move(buf), + mNumberOfChannels, mSampleRate); +} + #undef LOGD #undef LOGE #undef LOG_INTERNAL diff --git a/dom/media/webcodecs/AudioData.h b/dom/media/webcodecs/AudioData.h index 4ae69a225a..43af638d11 100644 --- a/dom/media/webcodecs/AudioData.h +++ b/dom/media/webcodecs/AudioData.h @@ -90,6 +90,7 @@ class AudioData final : public nsISupports, public nsWrapperCache { already_AddRefed<AudioData> Clone(ErrorResult& aRv); void Close(); + bool IsClosed() const; // [Serializable] implementations: {Read, Write}StructuredClone static JSObject* ReadStructuredClone(JSContext* aCx, nsIGlobalObject* aGlobal, @@ -107,11 +108,13 @@ class AudioData final : public nsISupports, public nsWrapperCache { static already_AddRefed<AudioData> FromTransferred(nsIGlobalObject* aGlobal, TransferredData* aData); + nsCString ToString() const; + + RefPtr<mozilla::AudioData> ToAudioData() const; + private: size_t ComputeCopyElementCount(const AudioDataCopyToOptions& aOptions, ErrorResult& aRv); - - nsCString ToString() const; // AudioData can run on either main thread or worker thread. void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(AudioData); } void CloseIfNeeded(); diff --git a/dom/media/webcodecs/AudioDecoder.cpp b/dom/media/webcodecs/AudioDecoder.cpp index 6b554dcacf..ef2acd4eae 100644 --- a/dom/media/webcodecs/AudioDecoder.cpp +++ b/dom/media/webcodecs/AudioDecoder.cpp @@ -68,11 +68,11 @@ NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) AudioDecoderConfigInternal::AudioDecoderConfigInternal( const nsAString& aCodec, uint32_t aSampleRate, uint32_t aNumberOfChannels, - Maybe<RefPtr<MediaByteBuffer>>&& aDescription) + already_AddRefed<MediaByteBuffer> aDescription) : mCodec(aCodec), mSampleRate(aSampleRate), mNumberOfChannels(aNumberOfChannels), - mDescription(std::move(aDescription)) {} + mDescription(aDescription) {} /*static*/ UniquePtr<AudioDecoderConfigInternal> AudioDecoderConfigInternal::Create( @@ -83,7 +83,7 @@ UniquePtr<AudioDecoderConfigInternal> AudioDecoderConfigInternal::Create( return nullptr; } - Maybe<RefPtr<MediaByteBuffer>> description; + RefPtr<MediaByteBuffer> description; if (aConfig.mDescription.WasPassed()) { auto rv = GetExtraDataFromArrayBuffer(aConfig.mDescription.Value()); if (rv.isErr()) { // Invalid description data. @@ -95,12 +95,28 @@ UniquePtr<AudioDecoderConfigInternal> AudioDecoderConfigInternal::Create( error.get()); return nullptr; } - description.emplace(rv.unwrap()); + description = rv.unwrap(); } return UniquePtr<AudioDecoderConfigInternal>(new AudioDecoderConfigInternal( aConfig.mCodec, aConfig.mSampleRate, aConfig.mNumberOfChannels, - std::move(description))); + description.forget())); +} + +nsCString AudioDecoderConfigInternal::ToString() const { + nsCString rv; + + rv.AppendLiteral("AudioDecoderConfigInternal: "); + rv.AppendPrintf("%s %" PRIu32 "Hz %" PRIu32 " ch", + NS_ConvertUTF16toUTF8(mCodec).get(), mSampleRate, + mNumberOfChannels); + if (mDescription) { + rv.AppendPrintf("(%zu bytes of extradata)", mDescription->Length()); + } else { + rv.AppendLiteral("(no extradata)"); + } + + return rv; } /* @@ -118,24 +134,6 @@ struct AudioMIMECreateParam { // Map between WebCodecs pcm types as strings and codec numbers // All other codecs -nsCString ConvertCodecName(const nsCString& aContainer, - const nsCString& aCodec) { - if (!aContainer.EqualsLiteral("x-wav")) { - return aCodec; - } - if (aCodec.EqualsLiteral("ulaw")) { - return nsCString("7"); - } - if (aCodec.EqualsLiteral("alaw")) { - return nsCString("6"); - } - if (aCodec.Find("f32")) { - return nsCString("3"); - } - // Linear PCM - return nsCString("1"); -} - static nsTArray<nsCString> GuessMIMETypes(const AudioMIMECreateParam& aParam) { nsCString codec = NS_ConvertUTF16toUTF8(aParam.mParsedCodec); nsTArray<nsCString> types; @@ -147,16 +145,6 @@ static nsTArray<nsCString> GuessMIMETypes(const AudioMIMECreateParam& aParam) { return types; } -static bool IsSupportedAudioCodec(const nsAString& aCodec) { - LOG("IsSupportedAudioCodec: %s", NS_ConvertUTF16toUTF8(aCodec).get()); - return aCodec.EqualsLiteral("flac") || aCodec.EqualsLiteral("mp3") || - IsAACCodecString(aCodec) || aCodec.EqualsLiteral("opus") || - aCodec.EqualsLiteral("ulaw") || aCodec.EqualsLiteral("alaw") || - aCodec.EqualsLiteral("pcm-u8") || aCodec.EqualsLiteral("pcm-s16") || - aCodec.EqualsLiteral("pcm-s24") || aCodec.EqualsLiteral("pcm-s32") || - aCodec.EqualsLiteral("pcm-f32"); -} - // https://w3c.github.io/webcodecs/#check-configuration-support template <typename Config> static bool CanDecodeAudio(const Config& aConfig) { @@ -259,13 +247,12 @@ Result<UniquePtr<TrackInfo>, nsresult> AudioDecoderTraits::CreateTrackInfo( return Err(NS_ERROR_INVALID_ARG); } - if (aConfig.mDescription.isSome()) { - RefPtr<MediaByteBuffer> buf; - buf = aConfig.mDescription.value(); - if (buf) { - LOG("The given config has %zu bytes of description data", buf->Length()); - ai->mCodecSpecificConfig = - AudioCodecSpecificVariant{AudioCodecSpecificBinaryBlob{buf}}; + if (aConfig.mDescription) { + if (!aConfig.mDescription->IsEmpty()) { + LOG("The given config has %zu bytes of description data", + aConfig.mDescription->Length()); + ai->mCodecSpecificConfig = AudioCodecSpecificVariant{ + AudioCodecSpecificBinaryBlob{aConfig.mDescription}}; } } @@ -275,7 +262,7 @@ Result<UniquePtr<TrackInfo>, nsresult> AudioDecoderTraits::CreateTrackInfo( LOG("Created AudioInfo %s (%" PRIu32 "ch %" PRIu32 "Hz - with extra-data: %s)", NS_ConvertUTF16toUTF8(aConfig.mCodec).get(), ai->mChannels, ai->mChannels, - aConfig.mDescription.isSome() ? "yes" : "no"); + aConfig.mDescription && !aConfig.mDescription->IsEmpty() ? "yes" : "no"); return track; } diff --git a/dom/media/webcodecs/AudioEncoder.cpp b/dom/media/webcodecs/AudioEncoder.cpp new file mode 100644 index 0000000000..7204a13200 --- /dev/null +++ b/dom/media/webcodecs/AudioEncoder.cpp @@ -0,0 +1,488 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/AudioEncoder.h" +#include "EncoderTraits.h" +#include "mozilla/dom/AudioEncoderBinding.h" + +#include "EncoderConfig.h" +#include "EncoderTypes.h" +#include "MediaData.h" +#include "mozilla/Assertions.h" +#include "mozilla/Logging.h" +#include "mozilla/Maybe.h" +#include "mozilla/dom/AudioDataBinding.h" +#include "mozilla/dom/EncodedAudioChunk.h" +#include "mozilla/dom/EncodedAudioChunkBinding.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/WebCodecsUtils.h" +#include "EncoderConfig.h" + +extern mozilla::LazyLogModule gWebCodecsLog; + +namespace mozilla::dom { + +#ifdef LOG_INTERNAL +# undef LOG_INTERNAL +#endif // LOG_INTERNAL +#define LOG_INTERNAL(level, msg, ...) \ + MOZ_LOG(gWebCodecsLog, LogLevel::level, (msg, ##__VA_ARGS__)) + +#ifdef LOG +# undef LOG +#endif // LOG +#define LOG(msg, ...) LOG_INTERNAL(Debug, msg, ##__VA_ARGS__) + +#ifdef LOGW +# undef LOGW +#endif // LOGW +#define LOGW(msg, ...) LOG_INTERNAL(Warning, msg, ##__VA_ARGS__) + +#ifdef LOGE +# undef LOGE +#endif // LOGE +#define LOGE(msg, ...) LOG_INTERNAL(Error, msg, ##__VA_ARGS__) + +#ifdef LOGV +# undef LOGV +#endif // LOGV +#define LOGV(msg, ...) LOG_INTERNAL(Verbose, msg, ##__VA_ARGS__) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioEncoder, DOMEventTargetHelper, + mErrorCallback, mOutputCallback) +NS_IMPL_ADDREF_INHERITED(AudioEncoder, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(AudioEncoder, DOMEventTargetHelper) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioEncoder) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +/* + * Below are helper classes + */ +AudioEncoderConfigInternal::AudioEncoderConfigInternal( + const nsAString& aCodec, Maybe<uint32_t> aSampleRate, + Maybe<uint32_t> aNumberOfChannels, Maybe<uint32_t> aBitrate, + BitrateMode aBitrateMode) + : mCodec(aCodec), + mSampleRate(aSampleRate), + mNumberOfChannels(aNumberOfChannels), + mBitrate(aBitrate), + mBitrateMode(aBitrateMode) {} + +AudioEncoderConfigInternal::AudioEncoderConfigInternal( + const AudioEncoderConfig& aConfig) + : AudioEncoderConfigInternal( + aConfig.mCodec, OptionalToMaybe(aConfig.mSampleRate), + OptionalToMaybe(aConfig.mNumberOfChannels), + OptionalToMaybe(aConfig.mBitrate), aConfig.mBitrateMode) { + DebugOnly<nsCString> errorMessage; + if (aConfig.mCodec.EqualsLiteral("opus") && aConfig.mOpus.WasPassed()) { + // All values are in range at this point, the config is known valid. + OpusSpecific specific; + if (aConfig.mOpus.Value().mComplexity.WasPassed()) { + specific.mComplexity = aConfig.mOpus.Value().mComplexity.Value(); + } else { + // https://w3c.github.io/webcodecs/opus_codec_registration.html#dom-opusencoderconfig-complexity + // If no value is specificied, the default value is platform-specific: + // User Agents SHOULD set a default of 5 for mobile platforms, and a + // default of 9 for all other platforms. + if (IsOnAndroid()) { + specific.mComplexity = 5; + } else { + specific.mComplexity = 9; + } + } + specific.mApplication = OpusSpecific::Application::Unspecified; + specific.mFrameDuration = aConfig.mOpus.Value().mFrameDuration; + specific.mPacketLossPerc = aConfig.mOpus.Value().mPacketlossperc; + specific.mUseDTX = aConfig.mOpus.Value().mUsedtx; + specific.mUseInBandFEC = aConfig.mOpus.Value().mUseinbandfec; + mSpecific.emplace(specific); + } + MOZ_ASSERT(AudioEncoderTraits::Validate(aConfig, errorMessage)); +} + +AudioEncoderConfigInternal::AudioEncoderConfigInternal( + const AudioEncoderConfigInternal& aConfig) + : AudioEncoderConfigInternal(aConfig.mCodec, aConfig.mSampleRate, + aConfig.mNumberOfChannels, aConfig.mBitrate, + aConfig.mBitrateMode) {} + +void AudioEncoderConfigInternal::SetSpecific( + const EncoderConfig::CodecSpecific& aSpecific) { + mSpecific.emplace(aSpecific); +} + +/* + * The followings are helpers for AudioEncoder methods + */ + +static void CloneConfiguration(RootedDictionary<AudioEncoderConfig>& aDest, + JSContext* aCx, + const AudioEncoderConfig& aConfig) { + aDest.mCodec = aConfig.mCodec; + + if (aConfig.mNumberOfChannels.WasPassed()) { + aDest.mNumberOfChannels.Construct(aConfig.mNumberOfChannels.Value()); + } + if (aConfig.mSampleRate.WasPassed()) { + aDest.mSampleRate.Construct(aConfig.mSampleRate.Value()); + } + if (aConfig.mBitrate.WasPassed()) { + aDest.mBitrate.Construct(aConfig.mBitrate.Value()); + } + if (aConfig.mOpus.WasPassed()) { + aDest.mOpus.Construct(aConfig.mOpus.Value()); + // Handle the default value manually since it's different on mobile + if (!aConfig.mOpus.Value().mComplexity.WasPassed()) { + if (IsOnAndroid()) { + aDest.mOpus.Value().mComplexity.Construct(5); + } else { + aDest.mOpus.Value().mComplexity.Construct(9); + } + } + } + aDest.mBitrateMode = aConfig.mBitrateMode; +} + +static bool IsAudioEncodeSupported(const nsAString& aCodec) { + LOG("IsEncodeSupported: %s", NS_ConvertUTF16toUTF8(aCodec).get()); + + return aCodec.EqualsLiteral("opus") || aCodec.EqualsLiteral("vorbis"); +} + +static bool CanEncode(const RefPtr<AudioEncoderConfigInternal>& aConfig, + nsCString& aErrorMessage) { + auto parsedCodecString = + ParseCodecString(aConfig->mCodec).valueOr(EmptyString()); + // TODO: Enable WebCodecs on Android (Bug 1840508) + if (IsOnAndroid()) { + return false; + } + if (!IsAudioEncodeSupported(parsedCodecString)) { + return false; + } + + if (aConfig->mNumberOfChannels.value() > 256) { + aErrorMessage.AppendPrintf( + "Invalid number of channels, supported range is between 1 and 256"); + return false; + } + + // Somewhat arbitrarily chosen, but reflects real-life and what the rest of + // Gecko does. + if (aConfig->mSampleRate.value() < 3000 || + aConfig->mSampleRate.value() > 384000) { + aErrorMessage.AppendPrintf( + "Invalid sample-rate of %d, supported range is 3000Hz to 384000Hz", + aConfig->mSampleRate.value()); + return false; + } + + return EncoderSupport::Supports(aConfig); +} + +nsCString AudioEncoderConfigInternal::ToString() const { + nsCString rv; + + rv.AppendPrintf("AudioEncoderConfigInternal: %s", + NS_ConvertUTF16toUTF8(mCodec).get()); + if (mSampleRate) { + rv.AppendPrintf(" %" PRIu32 "Hz", mSampleRate.value()); + } + if (mNumberOfChannels) { + rv.AppendPrintf(" %" PRIu32 "ch", mNumberOfChannels.value()); + } + if (mBitrate) { + rv.AppendPrintf(" %" PRIu32 "bps", mBitrate.value()); + } + rv.AppendPrintf(" (%s)", mBitrateMode == mozilla::dom::BitrateMode::Constant + ? "CRB" + : "VBR"); + + return rv; +} + +EncoderConfig AudioEncoderConfigInternal::ToEncoderConfig() const { + const mozilla::BitrateMode bitrateMode = + mBitrateMode == mozilla::dom::BitrateMode::Constant + ? mozilla::BitrateMode::Constant + : mozilla::BitrateMode::Variable; + + CodecType type = CodecType::Opus; + Maybe<EncoderConfig::CodecSpecific> specific; + if (mCodec.EqualsLiteral("opus")) { + type = CodecType::Opus; + MOZ_ASSERT(mSpecific.isNothing() || mSpecific->is<OpusSpecific>()); + specific = mSpecific; + } else if (mCodec.EqualsLiteral("vorbis")) { + type = CodecType::Vorbis; + } else if (mCodec.EqualsLiteral("flac")) { + type = CodecType::Flac; + } else if (StringBeginsWith(mCodec, u"pcm-"_ns)) { + type = CodecType::PCM; + } else if (mCodec.EqualsLiteral("ulaw")) { + type = CodecType::PCM; + } else if (mCodec.EqualsLiteral("alaw")) { + type = CodecType::PCM; + } else if (StringBeginsWith(mCodec, u"mp4a."_ns)) { + type = CodecType::AAC; + } + + // This should have been checked ahead of time -- we can't encode without + // knowing the sample-rate or the channel count at the very least. + MOZ_ASSERT(mSampleRate.value()); + MOZ_ASSERT(mNumberOfChannels.value()); + + return EncoderConfig(type, mNumberOfChannels.value(), bitrateMode, + AssertedCast<uint32_t>(mSampleRate.value()), + mBitrate.valueOr(0), specific); +} + +bool AudioEncoderConfigInternal::Equals( + const AudioEncoderConfigInternal& aOther) const { + return false; +} + +bool AudioEncoderConfigInternal::CanReconfigure( + const AudioEncoderConfigInternal& aOther) const { + return false; +} + +already_AddRefed<WebCodecsConfigurationChangeList> +AudioEncoderConfigInternal::Diff( + const AudioEncoderConfigInternal& aOther) const { + return MakeRefPtr<WebCodecsConfigurationChangeList>().forget(); +} + +/* static */ +bool AudioEncoderTraits::IsSupported( + const AudioEncoderConfigInternal& aConfig) { + nsCString errorMessage; + bool canEncode = + CanEncode(MakeRefPtr<AudioEncoderConfigInternal>(aConfig), errorMessage); + if (!canEncode) { + LOGE("Can't encode configuration %s: %s", aConfig.ToString().get(), + errorMessage.get()); + } + return canEncode; +} + +// https://w3c.github.io/webcodecs/#valid-audioencoderconfig +/* static */ +bool AudioEncoderTraits::Validate(const AudioEncoderConfig& aConfig, + nsCString& aErrorMessage) { + Maybe<nsString> codec = ParseCodecString(aConfig.mCodec); + if (!codec || codec->IsEmpty()) { + LOGE("Validating AudioEncoderConfig: invalid codec string"); + return false; + } + + if (!aConfig.mNumberOfChannels.WasPassed()) { + aErrorMessage.AppendPrintf("Channel count required"); + return false; + } + if (aConfig.mNumberOfChannels.Value() == 0) { + aErrorMessage.AppendPrintf( + "Invalid number of channels, supported range is between 1 and 256"); + return false; + } + if (!aConfig.mSampleRate.WasPassed()) { + aErrorMessage.AppendPrintf("Sample-rate required"); + return false; + } + if (aConfig.mSampleRate.Value() == 0) { + aErrorMessage.AppendPrintf("Invalid sample-rate of 0"); + return false; + } + + if (aConfig.mBitrate.WasPassed() && + aConfig.mBitrate.Value() > std::numeric_limits<int>::max()) { + aErrorMessage.AppendPrintf("Invalid config: bitrate value too large"); + return false; + } + + if (codec->EqualsLiteral("opus")) { + // This comes from + // https://w3c.github.io/webcodecs/opus_codec_registration.html#opus-encoder-config + if (aConfig.mBitrate.WasPassed() && (aConfig.mBitrate.Value() < 6000 || + aConfig.mBitrate.Value() > 510000)) { + aErrorMessage.AppendPrintf( + "Invalid config: bitrate value outside of [6k, 510k] for opus"); + return false; + } + if (aConfig.mOpus.WasPassed()) { + // Verify value ranges + const std::array validFrameDurationUs = {2500, 5000, 10000, + 20000, 40000, 60000}; + if (std::find(validFrameDurationUs.begin(), validFrameDurationUs.end(), + aConfig.mOpus.Value().mFrameDuration) == + validFrameDurationUs.end()) { + aErrorMessage.AppendPrintf("Invalid config: invalid frame duration"); + return false; + } + if (aConfig.mOpus.Value().mComplexity.WasPassed() && + aConfig.mOpus.Value().mComplexity.Value() > 10) { + aErrorMessage.AppendPrintf( + "Invalid config: Opus complexity greater than 10"); + return false; + } + if (aConfig.mOpus.Value().mPacketlossperc > 100) { + aErrorMessage.AppendPrintf( + "Invalid config: Opus packet loss percentage greater than 100"); + return false; + } + } + } + + return true; +} + +/* static */ +RefPtr<AudioEncoderConfigInternal> AudioEncoderTraits::CreateConfigInternal( + const AudioEncoderConfig& aConfig) { + nsCString errorMessage; + if (!AudioEncoderTraits::Validate(aConfig, errorMessage)) { + return nullptr; + } + return MakeRefPtr<AudioEncoderConfigInternal>(aConfig); +} + +/* static */ +RefPtr<mozilla::AudioData> AudioEncoderTraits::CreateInputInternal( + const dom::AudioData& aInput, + const dom::VideoEncoderEncodeOptions& /* unused */) { + return aInput.ToAudioData(); +} + +/* + * Below are AudioEncoder implementation + */ + +AudioEncoder::AudioEncoder( + nsIGlobalObject* aParent, RefPtr<WebCodecsErrorCallback>&& aErrorCallback, + RefPtr<EncodedAudioChunkOutputCallback>&& aOutputCallback) + : EncoderTemplate(aParent, std::move(aErrorCallback), + std::move(aOutputCallback)) { + MOZ_ASSERT(mErrorCallback); + MOZ_ASSERT(mOutputCallback); + LOG("AudioEncoder %p ctor", this); +} + +AudioEncoder::~AudioEncoder() { + LOG("AudioEncoder %p dtor", this); + Unused << ResetInternal(NS_ERROR_DOM_ABORT_ERR); +} + +JSObject* AudioEncoder::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + AssertIsOnOwningThread(); + + return AudioEncoder_Binding::Wrap(aCx, this, aGivenProto); +} + +// https://w3c.github.io/webcodecs/#dom-audioencoder-audioencoder +/* static */ +already_AddRefed<AudioEncoder> AudioEncoder::Constructor( + const GlobalObject& aGlobal, const AudioEncoderInit& aInit, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + if (!global) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + return MakeAndAddRef<AudioEncoder>( + global.get(), RefPtr<WebCodecsErrorCallback>(aInit.mError), + RefPtr<EncodedAudioChunkOutputCallback>(aInit.mOutput)); +} + +// https://w3c.github.io/webcodecs/#dom-audioencoder-isconfigsupported +/* static */ +already_AddRefed<Promise> AudioEncoder::IsConfigSupported( + const GlobalObject& aGlobal, const AudioEncoderConfig& aConfig, + ErrorResult& aRv) { + LOG("AudioEncoder::IsConfigSupported, config: %s", + NS_ConvertUTF16toUTF8(aConfig.mCodec).get()); + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + if (!global) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<Promise> p = Promise::Create(global.get(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return p.forget(); + } + + nsCString errorMessage; + if (!AudioEncoderTraits::Validate(aConfig, errorMessage)) { + p->MaybeRejectWithTypeError(errorMessage); + return p.forget(); + } + + // TODO: Move the following works to another thread to unblock the current + // thread, as what spec suggests. + + RootedDictionary<AudioEncoderConfig> config(aGlobal.Context()); + CloneConfiguration(config, aGlobal.Context(), aConfig); + + bool supportedAudioCodec = IsSupportedAudioCodec(aConfig.mCodec); + auto configInternal = MakeRefPtr<AudioEncoderConfigInternal>(aConfig); + bool canEncode = CanEncode(configInternal, errorMessage); + if (!canEncode) { + LOG("CanEncode failed: %s", errorMessage.get()); + } + RootedDictionary<AudioEncoderSupport> s(aGlobal.Context()); + s.mConfig.Construct(std::move(config)); + s.mSupported.Construct(supportedAudioCodec && canEncode); + + p->MaybeResolve(s); + return p.forget(); +} + +RefPtr<EncodedAudioChunk> AudioEncoder::EncodedDataToOutputType( + nsIGlobalObject* aGlobalObject, const RefPtr<MediaRawData>& aData) { + AssertIsOnOwningThread(); + + // Package into an EncodedAudioChunk + auto buffer = + MakeRefPtr<MediaAlignedByteBuffer>(aData->Data(), aData->Size()); + auto encoded = MakeRefPtr<EncodedAudioChunk>( + aGlobalObject, buffer.forget(), EncodedAudioChunkType::Key, + aData->mTime.ToMicroseconds(), + aData->mDuration.IsZero() ? Nothing() + : Some(aData->mDuration.ToMicroseconds())); + return encoded; +} + +AudioDecoderConfigInternal AudioEncoder::EncoderConfigToDecoderConfig( + nsIGlobalObject* aGlobal, const RefPtr<MediaRawData>& aRawData, + const AudioEncoderConfigInternal& aOutputConfig) const { + MOZ_ASSERT(aOutputConfig.mSampleRate.isSome()); + MOZ_ASSERT(aOutputConfig.mNumberOfChannels.isSome()); + uint32_t sampleRate = aOutputConfig.mSampleRate.value(); + uint32_t channelCount = aOutputConfig.mNumberOfChannels.value(); + // Check if the encoder had to modify the settings because of codec + // constraints. e.g. FFmpegAudioEncoder can encode any sample-rate, but if the + // codec is Opus, then it will resample the audio one of the specific rates + // supported by the encoder. + if (aRawData->mConfig) { + sampleRate = aRawData->mConfig->mSampleRate; + channelCount = aRawData->mConfig->mNumberOfChannels; + } + return AudioDecoderConfigInternal(aOutputConfig.mCodec, sampleRate, + channelCount, + do_AddRef(aRawData->mExtraData)); +} + +#undef LOG +#undef LOGW +#undef LOGE +#undef LOGV +#undef LOG_INTERNAL + +} // namespace mozilla::dom diff --git a/dom/media/webcodecs/AudioEncoder.h b/dom/media/webcodecs/AudioEncoder.h new file mode 100644 index 0000000000..0df6cd23d6 --- /dev/null +++ b/dom/media/webcodecs/AudioEncoder.h @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_AudioEncoder_h +#define mozilla_dom_AudioEncoder_h + +#include "js/TypeDecls.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/EncoderTemplate.h" +#include "mozilla/dom/AudioData.h" +#include "nsCycleCollectionParticipant.h" +#include "EncoderTypes.h" +#include "EncoderAgent.h" + +class nsIGlobalObject; + +namespace mozilla::dom { + +class AudioDataOutputCallback; +class EncodedAudioChunk; +class EncodedAudioChunkData; +class EventHandlerNonNull; +class GlobalObject; +class Promise; +class WebCodecsErrorCallback; +struct AudioEncoderConfig; +struct AudioEncoderInit; + +} // namespace mozilla::dom + +namespace mozilla::dom { + +class AudioEncoder final : public EncoderTemplate<AudioEncoderTraits> { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioEncoder, DOMEventTargetHelper) + + public: + AudioEncoder(nsIGlobalObject* aParent, + RefPtr<WebCodecsErrorCallback>&& aErrorCallback, + RefPtr<EncodedAudioChunkOutputCallback>&& aOutputCallback); + + protected: + ~AudioEncoder(); + + public: + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<AudioEncoder> Constructor( + const GlobalObject& aGlobal, const AudioEncoderInit& aInit, + ErrorResult& aRv); + + static already_AddRefed<Promise> IsConfigSupported( + const GlobalObject& aGlobal, const AudioEncoderConfig& aConfig, + ErrorResult& aRv); + + protected: + virtual RefPtr<EncodedAudioChunk> EncodedDataToOutputType( + nsIGlobalObject* aGlobalObject, + const RefPtr<MediaRawData>& aData) override; + + virtual AudioDecoderConfigInternal EncoderConfigToDecoderConfig( + nsIGlobalObject* aGlobal, const RefPtr<MediaRawData>& aRawData, + const AudioEncoderConfigInternal& aOutputConfig) const override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_AudioEncoder_h diff --git a/dom/media/webcodecs/DecoderTemplate.cpp b/dom/media/webcodecs/DecoderTemplate.cpp index 4d1c310737..2fc2471a24 100644 --- a/dom/media/webcodecs/DecoderTemplate.cpp +++ b/dom/media/webcodecs/DecoderTemplate.cpp @@ -296,8 +296,7 @@ void DecoderTemplate<DecoderType>::CloseInternal(const nsresult& aResult) { if (r.isErr()) { nsCString name; GetErrorName(r.unwrapErr(), name); - LOGE("Error in ResetInternal: %s", name.get()); - MOZ_CRASH(); + LOGE("Error in ResetInternal during CloseInternal: %s", name.get()); } mState = CodecState::Closed; nsCString error; @@ -473,7 +472,6 @@ MessageProcessedResult DecoderTemplate<DecoderType>::ProcessConfigureMessage( mProcessingMessage.reset(); QueueATask("Error while configuring decoder", [self = RefPtr{this}]() MOZ_CAN_RUN_SCRIPT_BOUNDARY { - MOZ_ASSERT(self->mState != CodecState::Closed); self->CloseInternal(NS_ERROR_DOM_NOT_SUPPORTED_ERR); }); return MessageProcessedResult::Processed; diff --git a/dom/media/webcodecs/DecoderTypes.h b/dom/media/webcodecs/DecoderTypes.h index 339a164f70..4817a66f17 100644 --- a/dom/media/webcodecs/DecoderTypes.h +++ b/dom/media/webcodecs/DecoderTypes.h @@ -50,22 +50,22 @@ class VideoDecoderConfigInternal { Maybe<uint32_t>&& aCodedHeight, Maybe<uint32_t>&& aCodedWidth, Maybe<VideoColorSpaceInternal>&& aColorSpace, - Maybe<RefPtr<MediaByteBuffer>>&& aDescription, + already_AddRefed<MediaByteBuffer> aDescription, Maybe<uint32_t>&& aDisplayAspectHeight, Maybe<uint32_t>&& aDisplayAspectWidth, const HardwareAcceleration& aHardwareAcceleration, Maybe<bool>&& aOptimizeForLatency); ~VideoDecoderConfigInternal() = default; - nsString ToString() const; + nsCString ToString() const; bool Equals(const VideoDecoderConfigInternal& aOther) const { - if (mDescription.isSome() != aOther.mDescription.isSome()) { + if (mDescription != aOther.mDescription) { return false; } - if (mDescription.isSome() && aOther.mDescription.isSome()) { - auto lhs = mDescription.value(); - auto rhs = aOther.mDescription.value(); + if (mDescription && aOther.mDescription) { + auto lhs = mDescription; + auto rhs = aOther.mDescription; if (lhs->Length() != rhs->Length()) { return false; } @@ -86,7 +86,7 @@ class VideoDecoderConfigInternal { Maybe<uint32_t> mCodedHeight; Maybe<uint32_t> mCodedWidth; Maybe<VideoColorSpaceInternal> mColorSpace; - Maybe<RefPtr<MediaByteBuffer>> mDescription; + RefPtr<MediaByteBuffer> mDescription; Maybe<uint32_t> mDisplayAspectHeight; Maybe<uint32_t> mDisplayAspectWidth; HardwareAcceleration mHardwareAcceleration; @@ -116,24 +116,42 @@ class VideoDecoderTraits { class AudioDecoderConfigInternal { public: + AudioDecoderConfigInternal(const nsAString& aCodec, uint32_t aSampleRate, + uint32_t aNumberOfChannels, + already_AddRefed<MediaByteBuffer> aDescription); static UniquePtr<AudioDecoderConfigInternal> Create( const AudioDecoderConfig& aConfig); ~AudioDecoderConfigInternal() = default; + bool Equals(const AudioDecoderConfigInternal& aOther) const { + if (mDescription != aOther.mDescription) { + return false; + } + if (mDescription && aOther.mDescription) { + auto lhs = mDescription; + auto rhs = aOther.mDescription; + if (lhs->Length() != rhs->Length()) { + return false; + } + if (!ArrayEqual(lhs->Elements(), rhs->Elements(), lhs->Length())) { + return false; + } + } + return mCodec.Equals(aOther.mCodec) && mSampleRate == aOther.mSampleRate && + mNumberOfChannels == aOther.mNumberOfChannels && + mOptimizeForLatency == aOther.mOptimizeForLatency; + } + nsCString ToString() const; + nsString mCodec; uint32_t mSampleRate; uint32_t mNumberOfChannels; - Maybe<RefPtr<MediaByteBuffer>> mDescription; + RefPtr<MediaByteBuffer> mDescription; // Compilation fix, should be abstracted by DecoderAgent since those are not // supported HardwareAcceleration mHardwareAcceleration = HardwareAcceleration::No_preference; Maybe<bool> mOptimizeForLatency; - - private: - AudioDecoderConfigInternal(const nsAString& aCodec, uint32_t aSampleRate, - uint32_t aNumberOfChannels, - Maybe<RefPtr<MediaByteBuffer>>&& aDescription); }; class AudioDecoderTraits { diff --git a/dom/media/webcodecs/EncoderTemplate.cpp b/dom/media/webcodecs/EncoderTemplate.cpp index 35c8feb3f8..34edfae822 100644 --- a/dom/media/webcodecs/EncoderTemplate.cpp +++ b/dom/media/webcodecs/EncoderTemplate.cpp @@ -127,7 +127,7 @@ void EncoderTemplate<EncoderType>::Configure(const ConfigType& aConfig, RefPtr<ConfigTypeInternal> config = EncoderType::CreateConfigInternal(aConfig); if (!config) { - aRv.Throw(NS_ERROR_UNEXPECTED); // Invalid description data. + CloseInternal(NS_ERROR_DOM_NOT_SUPPORTED_ERR); return; } @@ -237,9 +237,9 @@ template <typename EncoderType> void EncoderTemplate<EncoderType>::Close(ErrorResult& aRv) { AssertIsOnOwningThread(); - LOG("%s::Close %p", EncoderType::Name.get(), this); + LOG("%s %p, Close", EncoderType::Name.get(), this); - if (auto r = CloseInternal(NS_ERROR_DOM_ABORT_ERR); r.isErr()) { + if (auto r = CloseInternalWithAbort(); r.isErr()) { aRv.Throw(r.unwrapErr()); } } @@ -273,23 +273,33 @@ Result<Ok, nsresult> EncoderTemplate<EncoderType>::ResetInternal( } template <typename EncoderType> -Result<Ok, nsresult> EncoderTemplate<EncoderType>::CloseInternal( - const nsresult& aResult) { +Result<Ok, nsresult> EncoderTemplate<EncoderType>::CloseInternalWithAbort() { AssertIsOnOwningThread(); - MOZ_TRY(ResetInternal(aResult)); + MOZ_TRY(ResetInternal(NS_ERROR_DOM_ABORT_ERR)); mState = CodecState::Closed; - if (aResult != NS_ERROR_DOM_ABORT_ERR) { - nsCString error; - GetErrorName(aResult, error); - LOGE("%s %p Close on error: %s", EncoderType::Name.get(), this, - error.get()); - ReportError(aResult); - } return Ok(); } template <typename EncoderType> +void EncoderTemplate<EncoderType>::CloseInternal(const nsresult& aResult) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aResult != NS_ERROR_DOM_ABORT_ERR, "Use CloseInternalWithAbort"); + + auto r = ResetInternal(aResult); + if (r.isErr()) { + nsCString name; + GetErrorName(r.unwrapErr(), name); + LOGE("Error during ResetInternal during CloseInternal: %s", name.get()); + } + mState = CodecState::Closed; + nsCString error; + GetErrorName(aResult, error); + LOGE("%s %p Close on error: %s", EncoderType::Name.get(), this, error.get()); + ReportError(aResult); +} + +template <typename EncoderType> void EncoderTemplate<EncoderType>::ReportError(const nsresult& aResult) { AssertIsOnOwningThread(); @@ -299,8 +309,28 @@ void EncoderTemplate<EncoderType>::ReportError(const nsresult& aResult) { } template <typename EncoderType> -void EncoderTemplate<EncoderType>::OutputEncodedData( - nsTArray<RefPtr<MediaRawData>>&& aData) { +template <typename T, typename U> +void EncoderTemplate<EncoderType>::CopyExtradataToDescriptionIfNeeded( + nsIGlobalObject* aGlobal, const T& aConfigInternal, U& aConfig) { + if (aConfigInternal.mDescription && + !aConfigInternal.mDescription->IsEmpty()) { + auto& abov = aConfig.mDescription.Construct(); + AutoEntryScript aes(aGlobal, "EncoderConfigToaConfigConfig"); + size_t lengthBytes = aConfigInternal.mDescription->Length(); + UniquePtr<uint8_t[], JS::FreePolicy> extradata(new uint8_t[lengthBytes]); + PodCopy(extradata.get(), aConfigInternal.mDescription->Elements(), + lengthBytes); + JS::Rooted<JSObject*> description( + aes.cx(), JS::NewArrayBufferWithContents(aes.cx(), lengthBytes, + std::move(extradata))); + JS::Rooted<JS::Value> value(aes.cx(), JS::ObjectValue(*description)); + DebugOnly<bool> rv = abov.Init(aes.cx(), value); + } +} + +template <> +void EncoderTemplate<VideoEncoderTraits>::OutputEncodedVideoData( + const nsTArray<RefPtr<MediaRawData>>&& aData) { AssertIsOnOwningThread(); MOZ_ASSERT(mState == CodecState::Configured); MOZ_ASSERT(mActiveConfig); @@ -313,7 +343,7 @@ void EncoderTemplate<EncoderType>::OutputEncodedData( jsapi.Init(GetParentObject()); // TODO: check returned value? JSContext* cx = jsapi.cx(); - RefPtr<typename EncoderType::OutputCallbackType> cb(mOutputCallback); + RefPtr<EncodedVideoChunkOutputCallback> cb(mOutputCallback); for (auto& data : aData) { // It's possible to have reset() called in between this task having been // dispatched, and running -- no output callback should happen when that's @@ -323,10 +353,10 @@ void EncoderTemplate<EncoderType>::OutputEncodedData( if (!mActiveConfig) { return; } - RefPtr<typename EncoderType::OutputType> encodedData = + RefPtr<EncodedVideoChunk> encodedData = EncodedDataToOutputType(GetParentObject(), data); - RootedDictionary<typename EncoderType::MetadataType> metadata(cx); + RootedDictionary<EncodedVideoChunkMetadata> metadata(cx); if (mOutputNewDecoderConfig) { VideoDecoderConfigInternal decoderConfigInternal = EncoderConfigToDecoderConfig(GetParentObject(), data, *mActiveConfig); @@ -354,23 +384,10 @@ void EncoderTemplate<EncoderType>::OutputEncodedData( MaybeToNullable(decoderConfigInternal.mColorSpace->mTransfer); decoderConfig.mColorSpace.Construct(std::move(colorSpace)); } - if (decoderConfigInternal.mDescription && - !decoderConfigInternal.mDescription.value()->IsEmpty()) { - auto& abov = decoderConfig.mDescription.Construct(); - AutoEntryScript aes(GetParentObject(), "EncoderConfigToDecoderConfig"); - size_t lengthBytes = - decoderConfigInternal.mDescription.value()->Length(); - UniquePtr<uint8_t[], JS::FreePolicy> extradata( - new uint8_t[lengthBytes]); - PodCopy(extradata.get(), - decoderConfigInternal.mDescription.value()->Elements(), - lengthBytes); - JS::Rooted<JSObject*> description( - aes.cx(), JS::NewArrayBufferWithContents(aes.cx(), lengthBytes, - std::move(extradata))); - JS::Rooted<JS::Value> value(aes.cx(), JS::ObjectValue(*description)); - DebugOnly<bool> rv = abov.Init(aes.cx(), value); - } + + CopyExtradataToDescriptionIfNeeded(GetParentObject(), + decoderConfigInternal, decoderConfig); + if (decoderConfigInternal.mDisplayAspectHeight) { decoderConfig.mDisplayAspectHeight.Construct( decoderConfigInternal.mDisplayAspectHeight.value()); @@ -387,7 +404,7 @@ void EncoderTemplate<EncoderType>::OutputEncodedData( metadata.mDecoderConfig.Construct(std::move(decoderConfig)); mOutputNewDecoderConfig = false; LOGE("New config passed to output callback: %s", - NS_ConvertUTF16toUTF8(decoderConfigInternal.ToString()).get()); + decoderConfigInternal.ToString().get()); } nsAutoCString metadataInfo; @@ -407,125 +424,74 @@ void EncoderTemplate<EncoderType>::OutputEncodedData( LOG("EncoderTemplate:: output callback (ts: % " PRId64 ")%s", encodedData->Timestamp(), metadataInfo.get()); - cb->Call((typename EncoderType::OutputType&)(*encodedData), metadata); + cb->Call((EncodedVideoChunk&)(*encodedData), metadata); } } -template <typename EncoderType> -class EncoderTemplate<EncoderType>::ErrorRunnable final - : public DiscardableRunnable { - public: - ErrorRunnable(Self* aEncoder, const nsresult& aError) - : DiscardableRunnable("Decoder ErrorRunnable"), - mEncoder(aEncoder), - mError(aError) { - MOZ_ASSERT(mEncoder); - } - ~ErrorRunnable() = default; - - // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. - // See bug 1535398. - MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { - nsCString error; - GetErrorName(mError, error); - LOGE("%s %p report error: %s", EncoderType::Name.get(), mEncoder.get(), - error.get()); - RefPtr<Self> d = std::move(mEncoder); - d->ReportError(mError); - return NS_OK; - } - - private: - RefPtr<Self> mEncoder; - const nsresult mError; -}; +template <> +void EncoderTemplate<AudioEncoderTraits>::OutputEncodedAudioData( + const nsTArray<RefPtr<MediaRawData>>&& aData) { + AssertIsOnOwningThread(); + MOZ_ASSERT(mState == CodecState::Configured); + MOZ_ASSERT(mActiveConfig); -template <typename EncoderType> -class EncoderTemplate<EncoderType>::OutputRunnable final - : public DiscardableRunnable { - public: - OutputRunnable(Self* aEncoder, WebCodecsId aConfigureId, - const nsACString& aLabel, - nsTArray<RefPtr<MediaRawData>>&& aData) - : DiscardableRunnable("Decoder OutputRunnable"), - mEncoder(aEncoder), - mConfigureId(aConfigureId), - mLabel(aLabel), - mData(std::move(aData)) { - MOZ_ASSERT(mEncoder); - } - ~OutputRunnable() = default; - - // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. - // See bug 1535398. - MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { - if (mEncoder->mState != CodecState::Configured) { - LOGV("%s %p has been %s. Discard %s-result for EncoderAgent #%zu", - EncoderType::Name.get(), mEncoder.get(), - mEncoder->mState == CodecState::Closed ? "closed" : "reset", - mLabel.get(), mConfigureId); - return NS_OK; - } + // Get JSContext for RootedDictionary. + // The EncoderType::MetadataType, AudioDecoderConfig + // below are rooted to work around the JS hazard issues. + AutoJSAPI jsapi; + DebugOnly<bool> ok = + jsapi.Init(GetParentObject()); // TODO: check returned value? + JSContext* cx = jsapi.cx(); - MOZ_ASSERT(mEncoder->mAgent); - if (mConfigureId != mEncoder->mAgent->mId) { - LOGW( - "%s %p has been re-configured. Still yield %s-result for " - "EncoderAgent #%zu", - EncoderType::Name.get(), mEncoder.get(), mLabel.get(), mConfigureId); + RefPtr<EncodedAudioChunkOutputCallback> cb(mOutputCallback); + for (auto& data : aData) { + // It's possible to have reset() called in between this task having been + // dispatched, and running -- no output callback should happen when that's + // the case. + // This is imprecise in the spec, but discussed in + // https://github.com/w3c/webcodecs/issues/755 and agreed upon. + if (!mActiveConfig) { + return; } + RefPtr<EncodedAudioChunk> encodedData = + EncodedDataToOutputType(GetParentObject(), data); - LOGV("%s %p, yields %s-result for EncoderAgent #%zu", - EncoderType::Name.get(), mEncoder.get(), mLabel.get(), mConfigureId); - RefPtr<Self> d = std::move(mEncoder); - d->OutputEncodedData(std::move(mData)); - - return NS_OK; - } + RootedDictionary<EncodedAudioChunkMetadata> metadata(cx); + if (mOutputNewDecoderConfig) { + AudioDecoderConfigInternal decoderConfigInternal = + this->EncoderConfigToDecoderConfig(GetParentObject(), data, + *mActiveConfig); - private: - RefPtr<Self> mEncoder; - const WebCodecsId mConfigureId; - const nsCString mLabel; - nsTArray<RefPtr<MediaRawData>> mData; -}; + // Convert VideoDecoderConfigInternal to VideoDecoderConfig + RootedDictionary<AudioDecoderConfig> decoderConfig(cx); + decoderConfig.mCodec = decoderConfigInternal.mCodec; + decoderConfig.mNumberOfChannels = decoderConfigInternal.mNumberOfChannels; + decoderConfig.mSampleRate = decoderConfigInternal.mSampleRate; -template <typename EncoderType> -void EncoderTemplate<EncoderType>::ScheduleOutputEncodedData( - nsTArray<RefPtr<MediaRawData>>&& aData, const nsACString& aLabel) { - MOZ_ASSERT(mState == CodecState::Configured); - MOZ_ASSERT(mAgent); + CopyExtradataToDescriptionIfNeeded(GetParentObject(), + decoderConfigInternal, decoderConfig); - MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(MakeAndAddRef<OutputRunnable>( - this, mAgent->mId, aLabel, std::move(aData)))); -} + metadata.mDecoderConfig.Construct(std::move(decoderConfig)); + mOutputNewDecoderConfig = false; + LOGE("New config passed to output callback: %s", + decoderConfigInternal.ToString().get()); + } -template <typename EncoderType> -void EncoderTemplate<EncoderType>::ScheduleClose(const nsresult& aResult) { - AssertIsOnOwningThread(); - MOZ_ASSERT(mState == CodecState::Configured); + nsAutoCString metadataInfo; - auto task = [self = RefPtr{this}, result = aResult] { - if (self->mState == CodecState::Closed) { - nsCString error; - GetErrorName(result, error); - LOGW("%s %p has been closed. Ignore close with %s", - EncoderType::Name.get(), self.get(), error.get()); - return; + if (metadata.mDecoderConfig.WasPassed()) { + metadataInfo.Append(", new decoder config"); } - DebugOnly<Result<Ok, nsresult>> r = self->CloseInternal(result); - MOZ_ASSERT(r.value.isOk()); - }; - nsISerialEventTarget* target = GetCurrentSerialEventTarget(); - if (NS_IsMainThread()) { - MOZ_ALWAYS_SUCCEEDS(target->Dispatch( - NS_NewRunnableFunction("ScheduleClose Runnable (main)", task))); - return; + LOG("EncoderTemplate:: output callback (ts: % " PRId64 + ", duration: % " PRId64 ", %zu bytes, %" PRIu64 " so far)", + encodedData->Timestamp(), + !encodedData->GetDuration().IsNull() + ? encodedData->GetDuration().Value() + : 0, + data->Size(), mPacketsOutput++); + cb->Call((EncodedAudioChunk&)(*encodedData), metadata); } - - MOZ_ALWAYS_SUCCEEDS(target->Dispatch(NS_NewCancelableRunnableFunction( - "ScheduleClose Runnable (worker)", task))); } template <typename EncoderType> @@ -537,20 +503,10 @@ void EncoderTemplate<EncoderType>::ScheduleDequeueEvent() { } mDequeueEventScheduled = true; - auto dispatcher = [self = RefPtr{this}] { + QueueATask("dequeue event task", [self = RefPtr{this}]() { self->FireEvent(nsGkAtoms::ondequeue, u"dequeue"_ns); self->mDequeueEventScheduled = false; - }; - nsISerialEventTarget* target = GetCurrentSerialEventTarget(); - - if (NS_IsMainThread()) { - MOZ_ALWAYS_SUCCEEDS(target->Dispatch(NS_NewRunnableFunction( - "ScheduleDequeueEvent Runnable (main)", dispatcher))); - return; - } - - MOZ_ALWAYS_SUCCEEDS(target->Dispatch(NS_NewCancelableRunnableFunction( - "ScheduleDequeueEvent Runnable (worker)", dispatcher))); + }); } template <typename EncoderType> @@ -655,6 +611,15 @@ void EncoderTemplate<EncoderType>::CancelPendingControlMessages( } template <typename EncoderType> +template <typename Func> +void EncoderTemplate<EncoderType>::QueueATask(const char* aName, + Func&& aSteps) { + AssertIsOnOwningThread(); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread( + NS_NewRunnableFunction(aName, std::forward<Func>(aSteps)))); +} + +template <typename EncoderType> MessageProcessedResult EncoderTemplate<EncoderType>::ProcessConfigureMessage( RefPtr<ConfigureMessage> aMessage) { AssertIsOnOwningThread(); @@ -677,15 +642,13 @@ MessageProcessedResult EncoderTemplate<EncoderType>::ProcessConfigureMessage( LOGE("%s %p ProcessConfigureMessage error (sync): Not supported", EncoderType::Name.get(), this); mProcessingMessage = nullptr; - NS_DispatchToCurrentThread(NS_NewRunnableFunction( - "ProcessConfigureMessage (async): not supported", + QueueATask( + "Error while configuring encoder", [self = RefPtr(this)]() MOZ_CAN_RUN_SCRIPT_BOUNDARY { LOGE("%s %p ProcessConfigureMessage (async close): Not supported", EncoderType::Name.get(), self.get()); - DebugOnly<Result<Ok, nsresult>> r = - self->CloseInternal(NS_ERROR_DOM_NOT_SUPPORTED_ERR); - MOZ_ASSERT(r.value.isOk()); - })); + self->CloseInternal(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + }); return MessageProcessedResult::Processed; } @@ -711,19 +674,30 @@ void EncoderTemplate<EncoderType>::StopBlockingMessageQueue() { } template <typename EncoderType> +void EncoderTemplate<EncoderType>::OutputEncodedData( + const nsTArray<RefPtr<MediaRawData>>&& aData) { + if constexpr (std::is_same_v<EncoderType, VideoEncoderTraits>) { + OutputEncodedVideoData(std::move(aData)); + } else { + OutputEncodedAudioData(std::move(aData)); + } +} + +template <typename EncoderType> void EncoderTemplate<EncoderType>::Reconfigure( RefPtr<ConfigureMessage> aMessage) { MOZ_ASSERT(mAgent); - LOG("Reconfiguring encoder: %s", - NS_ConvertUTF16toUTF8(aMessage->Config()->ToString()).get()); + LOG("Reconfiguring encoder: %s", aMessage->Config()->ToString().get()); RefPtr<ConfigTypeInternal> config = aMessage->Config(); RefPtr<WebCodecsConfigurationChangeList> configDiff = config->Diff(*mActiveConfig); - // Nothing to do, return now + // Nothing to do, return now, but per spec the config + // must be output next time a packet is output. if (configDiff->Empty()) { + mOutputNewDecoderConfig = true; LOG("Reconfigure with identical config, returning."); mProcessingMessage = nullptr; StopBlockingMessageQueue(); @@ -731,9 +705,8 @@ void EncoderTemplate<EncoderType>::Reconfigure( } LOG("Attempting to reconfigure encoder: old: %s new: %s, diff: %s", - NS_ConvertUTF16toUTF8(mActiveConfig->ToString()).get(), - NS_ConvertUTF16toUTF8(config->ToString()).get(), - NS_ConvertUTF16toUTF8(configDiff->ToString()).get()); + mActiveConfig->ToString().get(), config->ToString().get(), + configDiff->ToString().get()); RefPtr<EncoderConfigurationChangeList> changeList = configDiff->ToPEMChangeList(); @@ -766,16 +739,20 @@ void EncoderTemplate<EncoderType>::Reconfigure( message](EncoderAgent::EncodePromise::ResolveOrRejectValue&& aResult) { if (aResult.IsReject()) { + // The spec asks to close the encoder with an + // NotSupportedError so we log the exact error here. const MediaResult& error = aResult.RejectValue(); - LOGE( - "%s %p, EncoderAgent #%zu failed to flush during " - "reconfigure, closing: %s", - EncoderType::Name.get(), self.get(), id, - error.Description().get()); - - self->mProcessingMessage = nullptr; - self->ScheduleClose( - NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR); + LOGE("%s %p, EncoderAgent #%zu failed to configure: %s", + EncoderType::Name.get(), self.get(), id, + error.Description().get()); + + self->QueueATask( + "Error during drain during reconfigure", + [self = RefPtr{self}]() MOZ_CAN_RUN_SCRIPT_BOUNDARY { + MOZ_ASSERT(self->mState != CodecState::Closed); + self->CloseInternal( + NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR); + }); return; } @@ -797,12 +774,15 @@ void EncoderTemplate<EncoderType>::Reconfigure( LOG("%s %p Outputing %zu frames during flush " " for reconfiguration with encoder destruction", EncoderType::Name.get(), self.get(), data.Length()); - self->ScheduleOutputEncodedData( - std::move(data), - nsLiteralCString("Flush before reconfigure")); + self->QueueATask( + "Output encoded Data", + [self = RefPtr{self}, data = std::move(data)]() + MOZ_CAN_RUN_SCRIPT_BOUNDARY { + self->OutputEncodedData(std::move(data)); + }); } - NS_DispatchToCurrentThread(NS_NewRunnableFunction( + self->QueueATask( "Destroy + recreate encoder after failed reconfigure", [self = RefPtr(self), message]() MOZ_CAN_RUN_SCRIPT_BOUNDARY { @@ -810,7 +790,7 @@ void EncoderTemplate<EncoderType>::Reconfigure( // encoder with the new configuration. self->DestroyEncoderAgentIfAny(); self->Configure(message); - })); + }); }); return; } @@ -833,32 +813,30 @@ void EncoderTemplate<EncoderType>::Configure( RefPtr<ConfigureMessage> aMessage) { MOZ_ASSERT(!mAgent); - LOG("Configuring encoder: %s", - NS_ConvertUTF16toUTF8(aMessage->Config()->ToString()).get()); + LOG("Configuring encoder: %s", aMessage->Config()->ToString().get()); mOutputNewDecoderConfig = true; mActiveConfig = aMessage->Config(); - bool decoderAgentCreated = + bool encoderAgentCreated = CreateEncoderAgent(aMessage->mMessageId, aMessage->Config()); - if (!decoderAgentCreated) { + if (!encoderAgentCreated) { LOGE( "%s %p ProcessConfigureMessage error (sync): encoder agent " "creation " "failed", EncoderType::Name.get(), this); mProcessingMessage = nullptr; - NS_DispatchToCurrentThread(NS_NewRunnableFunction( - "ProcessConfigureMessage (async): encoder agent creating failed", + QueueATask( + "Error when configuring encoder (encoder agent creation failed)", [self = RefPtr(this)]() MOZ_CAN_RUN_SCRIPT_BOUNDARY { + MOZ_ASSERT(self->mState != CodecState::Closed); LOGE( "%s %p ProcessConfigureMessage (async close): encoder agent " "creation failed", EncoderType::Name.get(), self.get()); - DebugOnly<Result<Ok, nsresult>> r = - self->CloseInternal(NS_ERROR_DOM_NOT_SUPPORTED_ERR); - MOZ_ASSERT(r.value.isOk()); - })); + self->CloseInternal(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + }); return; } @@ -866,7 +844,7 @@ void EncoderTemplate<EncoderType>::Configure( MOZ_ASSERT(mActiveConfig); LOG("Real configuration with fresh config: %s", - NS_ConvertUTF16toUTF8(mActiveConfig->ToString().get()).get()); + mActiveConfig->ToString().get()); EncoderConfig config = mActiveConfig->ToEncoderConfig(); mAgent->Configure(config) @@ -897,10 +875,15 @@ void EncoderTemplate<EncoderType>::Configure( LOGE("%s %p, EncoderAgent #%zu failed to configure: %s", EncoderType::Name.get(), self.get(), id, error.Description().get()); - DebugOnly<Result<Ok, nsresult>> r = self->CloseInternal( - NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR); - MOZ_ASSERT(r.value.isOk()); - return; // No further process + + self->QueueATask( + "Error during configure", + [self = RefPtr{self}]() MOZ_CAN_RUN_SCRIPT_BOUNDARY { + MOZ_ASSERT(self->mState != CodecState::Closed); + self->CloseInternal( + NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR); + }); + return; } self->StopBlockingMessageQueue(); @@ -933,7 +916,11 @@ MessageProcessedResult EncoderTemplate<EncoderType>::ProcessEncodeMessage( // data is invalid. auto closeOnError = [&]() { mProcessingMessage = nullptr; - ScheduleClose(NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR); + QueueATask("Error during encode", + [self = RefPtr{this}]() MOZ_CAN_RUN_SCRIPT_BOUNDARY { + MOZ_ASSERT(self->mState != CodecState::Closed); + self->CloseInternal(NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR); + }); return MessageProcessedResult::Processed; }; @@ -973,8 +960,14 @@ MessageProcessedResult EncoderTemplate<EncoderType>::ProcessEncodeMessage( LOGE("%s %p, EncoderAgent #%zu %s failed: %s", EncoderType::Name.get(), self.get(), id, msgStr.get(), error.Description().get()); - self->ScheduleClose(NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR); - return; // No further process + self->QueueATask( + "Error during encode runnable", + [self = RefPtr{self}]() MOZ_CAN_RUN_SCRIPT_BOUNDARY { + MOZ_ASSERT(self->mState != CodecState::Closed); + self->CloseInternal( + NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR); + }); + return; } MOZ_ASSERT(aResult.IsResolve()); @@ -984,11 +977,16 @@ MessageProcessedResult EncoderTemplate<EncoderType>::ProcessEncodeMessage( LOGV("%s %p got no data for %s", EncoderType::Name.get(), self.get(), msgStr.get()); } else { - LOGV("%s %p, schedule %zu encoded data output", - EncoderType::Name.get(), self.get(), data.Length()); - self->ScheduleOutputEncodedData(std::move(data), msgStr); + LOGV("%s %p, schedule %zu encoded data output for %s", + EncoderType::Name.get(), self.get(), data.Length(), + msgStr.get()); + self->QueueATask( + "Output encoded Data", + [self = RefPtr{self}, data2 = std::move(data)]() + MOZ_CAN_RUN_SCRIPT_BOUNDARY { + self->OutputEncodedData(std::move(data2)); + }); } - self->ProcessControlMessageQueue(); }) ->Track(aMessage->Request()); @@ -1022,69 +1020,78 @@ MessageProcessedResult EncoderTemplate<EncoderType>::ProcessFlushMessage( } mAgent->Drain() - ->Then(GetCurrentSerialEventTarget(), __func__, - [self = RefPtr{this}, id = mAgent->mId, aMessage]( - EncoderAgent::EncodePromise::ResolveOrRejectValue&& aResult) { - MOZ_ASSERT(self->mProcessingMessage); - MOZ_ASSERT(self->mProcessingMessage->AsFlushMessage()); - MOZ_ASSERT(self->mState == CodecState::Configured); - MOZ_ASSERT(self->mAgent); - MOZ_ASSERT(id == self->mAgent->mId); - MOZ_ASSERT(self->mActiveConfig); - - LOG("%s %p, EncoderAgent #%zu %s has been %s", - EncoderType::Name.get(), self.get(), id, - aMessage->ToString().get(), - aResult.IsResolve() ? "resolved" : "rejected"); - - nsCString msgStr = aMessage->ToString(); - - aMessage->Complete(); - - // If flush failed, it means encoder fails to encode the data - // sent before, so we treat it like an encode error. We reject - // the promise first and then queue a task to close VideoEncoder - // with an EncodingError. - if (aResult.IsReject()) { - const MediaResult& error = aResult.RejectValue(); - LOGE("%s %p, EncoderAgent #%zu failed to flush: %s", - EncoderType::Name.get(), self.get(), id, - error.Description().get()); + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self = RefPtr{this}, id = mAgent->mId, aMessage, + this](EncoderAgent::EncodePromise::ResolveOrRejectValue&& aResult) { + MOZ_ASSERT(self->mProcessingMessage); + MOZ_ASSERT(self->mProcessingMessage->AsFlushMessage()); + MOZ_ASSERT(self->mState == CodecState::Configured); + MOZ_ASSERT(self->mAgent); + MOZ_ASSERT(id == self->mAgent->mId); + MOZ_ASSERT(self->mActiveConfig); - // Reject with an EncodingError instead of the error we got - // above. - self->SchedulePromiseResolveOrReject( - aMessage->TakePromise(), - NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR); + LOG("%s %p, EncoderAgent #%zu %s has been %s", + EncoderType::Name.get(), self.get(), id, + aMessage->ToString().get(), + aResult.IsResolve() ? "resolved" : "rejected"); - self->mProcessingMessage = nullptr; + nsCString msgStr = aMessage->ToString(); - self->ScheduleClose(NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR); - return; // No further process - } + aMessage->Complete(); - // If flush succeeded, schedule to output encoded data first - // and then resolve the promise, then keep processing the - // control messages. - MOZ_ASSERT(aResult.IsResolve()); - nsTArray<RefPtr<MediaRawData>> data = - std::move(aResult.ResolveValue()); + // If flush failed, it means encoder fails to encode the data + // sent before, so we treat it like an encode error. We reject + // the promise first and then queue a task to close VideoEncoder + // with an EncodingError. + if (aResult.IsReject()) { + const MediaResult& error = aResult.RejectValue(); + LOGE("%s %p, EncoderAgent #%zu failed to flush: %s", + EncoderType::Name.get(), self.get(), id, + error.Description().get()); + RefPtr<Promise> promise = aMessage->TakePromise(); + // Reject with an EncodingError instead of the error we got + // above. + self->QueueATask( + "Error during flush runnable", + [self = RefPtr{this}, promise]() MOZ_CAN_RUN_SCRIPT_BOUNDARY { + promise->MaybeReject( + NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR); + self->mProcessingMessage = nullptr; + MOZ_ASSERT(self->mState != CodecState::Closed); + self->CloseInternal( + NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR); + }); + return; + } - if (data.IsEmpty()) { - LOG("%s %p gets no data for %s", EncoderType::Name.get(), - self.get(), msgStr.get()); - } else { - LOG("%s %p, schedule %zu encoded data output for %s", - EncoderType::Name.get(), self.get(), data.Length(), - msgStr.get()); - self->ScheduleOutputEncodedData(std::move(data), msgStr); - } + // If flush succeeded, schedule to output encoded data first + // and then resolve the promise, then keep processing the + // control messages. + MOZ_ASSERT(aResult.IsResolve()); + nsTArray<RefPtr<MediaRawData>> data = + std::move(aResult.ResolveValue()); + + if (data.IsEmpty()) { + LOG("%s %p gets no data for %s", EncoderType::Name.get(), + self.get(), msgStr.get()); + } else { + LOG("%s %p, schedule %zu encoded data output for %s", + EncoderType::Name.get(), self.get(), data.Length(), + msgStr.get()); + } - self->SchedulePromiseResolveOrReject(aMessage->TakePromise(), - NS_OK); - self->mProcessingMessage = nullptr; - self->ProcessControlMessageQueue(); - }) + RefPtr<Promise> promise = aMessage->TakePromise(); + self->QueueATask( + "Flush: output encoded data task", + [self = RefPtr{self}, promise, data = std::move(data)]() + MOZ_CAN_RUN_SCRIPT_BOUNDARY { + self->OutputEncodedData(std::move(data)); + promise->MaybeResolveWithUndefined(); + }); + self->mProcessingMessage = nullptr; + self->ProcessControlMessageQueue(); + }) ->Track(aMessage->Request()); return MessageProcessedResult::Processed; @@ -1218,6 +1225,7 @@ void EncoderTemplate<EncoderType>::DestroyEncoderAgentIfAny() { } template class EncoderTemplate<VideoEncoderTraits>; +template class EncoderTemplate<AudioEncoderTraits>; #undef LOG #undef LOGW diff --git a/dom/media/webcodecs/EncoderTemplate.h b/dom/media/webcodecs/EncoderTemplate.h index e53d7166d1..bc65edca46 100644 --- a/dom/media/webcodecs/EncoderTemplate.h +++ b/dom/media/webcodecs/EncoderTemplate.h @@ -18,6 +18,7 @@ #include "mozilla/Result.h" #include "mozilla/UniquePtr.h" #include "mozilla/dom/VideoEncoderBinding.h" +#include "mozilla/dom/AudioEncoderBinding.h" #include "mozilla/dom/WorkerRef.h" #include "mozilla/media/MediaUtils.h" #include "nsStringFwd.h" @@ -81,10 +82,8 @@ class EncoderTemplate : public DOMEventTargetHelper { RefPtr<ConfigTypeInternal> Config() { return mConfig; } nsCString ToString() const override { nsCString rv; - rv.AppendPrintf( - "ConfigureMessage(#%zu): %s", this->mMessageId, - mConfig ? NS_ConvertUTF16toUTF8(mConfig->ToString().get()).get() - : "null cfg"); + rv.AppendPrintf("ConfigureMessage(#%zu): %s", this->mMessageId, + mConfig ? mConfig->ToString().get() : "null cfg"); return rv; } @@ -149,10 +148,14 @@ class EncoderTemplate : public DOMEventTargetHelper { void StartBlockingMessageQueue(); void StopBlockingMessageQueue(); + MOZ_CAN_RUN_SCRIPT + void OutputEncodedData(const nsTArray<RefPtr<MediaRawData>>&& aData); + CodecState State() const { return mState; }; uint32_t EncodeQueueSize() const { return mEncodeQueueSize; }; + MOZ_CAN_RUN_SCRIPT void Configure(const ConfigType& aConfig, ErrorResult& aRv); void EncodeAudioData(InputType& aInput, ErrorResult& aRv); @@ -170,10 +173,13 @@ class EncoderTemplate : public DOMEventTargetHelper { /* Type conversion functions for the Encoder implementation */ protected: virtual RefPtr<OutputType> EncodedDataToOutputType( - nsIGlobalObject* aGlobalObject, RefPtr<MediaRawData>& aData) = 0; + nsIGlobalObject* aGlobalObject, const RefPtr<MediaRawData>& aData) = 0; virtual OutputConfigType EncoderConfigToDecoderConfig( nsIGlobalObject* aGlobalObject, const RefPtr<MediaRawData>& aData, const ConfigTypeInternal& aOutputConfig) const = 0; + template <typename T, typename U> + void CopyExtradataToDescriptionIfNeeded(nsIGlobalObject* aGlobal, + const T& aConfigInternal, U& aConfig); /* Internal member variables and functions */ protected: // EncoderTemplate can run on either main thread or worker thread. @@ -182,21 +188,17 @@ class EncoderTemplate : public DOMEventTargetHelper { } Result<Ok, nsresult> ResetInternal(const nsresult& aResult); - MOZ_CAN_RUN_SCRIPT_BOUNDARY - Result<Ok, nsresult> CloseInternal(const nsresult& aResult); + MOZ_CAN_RUN_SCRIPT + Result<Ok, nsresult> CloseInternalWithAbort(); + MOZ_CAN_RUN_SCRIPT + void CloseInternal(const nsresult& aResult); MOZ_CAN_RUN_SCRIPT void ReportError(const nsresult& aResult); - MOZ_CAN_RUN_SCRIPT void OutputEncodedData( - nsTArray<RefPtr<MediaRawData>>&& aData); - - class ErrorRunnable; - void ScheduleReportError(const nsresult& aResult); - class OutputRunnable; - void ScheduleOutputEncodedData(nsTArray<RefPtr<MediaRawData>>&& aData, - const nsACString& aLabel); - - void ScheduleClose(const nsresult& aResult); + MOZ_CAN_RUN_SCRIPT void OutputEncodedVideoData( + const nsTArray<RefPtr<MediaRawData>>&& aData); + MOZ_CAN_RUN_SCRIPT void OutputEncodedAudioData( + const nsTArray<RefPtr<MediaRawData>>&& aData); void ScheduleDequeueEvent(); nsresult FireEvent(nsAtom* aTypeWithOn, const nsAString& aEventType); @@ -207,6 +209,9 @@ class EncoderTemplate : public DOMEventTargetHelper { void ProcessControlMessageQueue(); void CancelPendingControlMessages(const nsresult& aResult); + template <typename Func> + void QueueATask(const char* aName, Func&& aSteps); + MessageProcessedResult ProcessConfigureMessage( RefPtr<ConfigureMessage> aMessage); @@ -244,14 +249,14 @@ class EncoderTemplate : public DOMEventTargetHelper { // used as the FlushMessage's Id. size_t mFlushCounter; - // EncoderAgent will be created the first time "configure" is being processed, - // and will be destroyed when "reset" is called. If another "configure" is - // called, either it's possible to reconfigure the underlying encoder without - // tearing eveyrthing down (e.g. a bitrate change), or it's not possible, and - // the current encoder will be destroyed and a new one create. - // In both cases, the encoder is implicitely flushed before the configuration - // change. - // See CanReconfigure on the {Audio,Video}EncoderConfigInternal + // EncoderAgent will be created the first time "configure" is being + // processed, and will be destroyed when "reset" is called. If another + // "configure" is called, either it's possible to reconfigure the underlying + // encoder without tearing everything down (e.g. a bitrate change), or it's + // not possible, and the current encoder will be destroyed and a new one + // create. In both cases, the encoder is implicitely flushed before the + // configuration change. See CanReconfigure on the + // {Audio,Video}EncoderConfigInternal RefPtr<EncoderAgent> mAgent; RefPtr<ConfigTypeInternal> mActiveConfig; // This is true when a configure call has just been processed, and it's @@ -283,6 +288,7 @@ class EncoderTemplate : public DOMEventTargetHelper { // TODO: Use StrongWorkerRef instead if this is always used in the same // thread? RefPtr<ThreadSafeWorkerRef> mWorkerRef; + uint64_t mPacketsOutput = 0; }; } // namespace mozilla::dom diff --git a/dom/media/webcodecs/EncoderTypes.h b/dom/media/webcodecs/EncoderTypes.h index d58d7c54c8..39f660203b 100644 --- a/dom/media/webcodecs/EncoderTypes.h +++ b/dom/media/webcodecs/EncoderTypes.h @@ -9,13 +9,14 @@ #include "mozilla/Maybe.h" #include "mozilla/dom/EncodedVideoChunk.h" +#include "mozilla/dom/MediaRecorderBinding.h" #include "mozilla/dom/VideoEncoderBinding.h" +#include "mozilla/dom/AudioEncoderBinding.h" #include "mozilla/dom/VideoFrame.h" #include "mozilla/dom/VideoFrameBinding.h" #include "nsStringFwd.h" #include "nsTLiteralString.h" #include "VideoDecoder.h" -#include "PlatformEncoderModule.h" namespace mozilla { @@ -24,6 +25,68 @@ class MediaByteBuffer; namespace dom { +class AudioEncoderConfigInternal { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioEncoderConfigInternal); + explicit AudioEncoderConfigInternal(const AudioEncoderConfig& aConfig); + explicit AudioEncoderConfigInternal( + const AudioEncoderConfigInternal& aConfig); + + void SetSpecific(const EncoderConfig::CodecSpecific& aSpecific); + + nsCString ToString() const; + + bool Equals(const AudioEncoderConfigInternal& aOther) const; + bool CanReconfigure(const AudioEncoderConfigInternal& aOther) const; + + // Returns an EncoderConfig struct with as many filled members as + // possible. + EncoderConfig ToEncoderConfig() const; + + already_AddRefed<WebCodecsConfigurationChangeList> Diff( + const AudioEncoderConfigInternal& aOther) const; + + nsString mCodec; + Maybe<uint32_t> mSampleRate; + Maybe<uint32_t> mNumberOfChannels; + Maybe<uint32_t> mBitrate; + BitrateMode mBitrateMode; + Maybe<EncoderConfig::CodecSpecific> mSpecific; + + private: + AudioEncoderConfigInternal(const nsAString& aCodec, + Maybe<uint32_t> aSampleRate, + Maybe<uint32_t> aNumberOfChannels, + Maybe<uint32_t> aBitRate, + BitrateMode aBitratemode); + ~AudioEncoderConfigInternal() = default; +}; + +class AudioEncoderTraits { + public: + static constexpr nsLiteralCString Name = "AudioEncoder"_ns; + using ConfigType = AudioEncoderConfig; + using ConfigTypeInternal = AudioEncoderConfigInternal; + using InputType = dom::AudioData; + using OutputConfigType = mozilla::dom::AudioDecoderConfigInternal; + using InputTypeInternal = mozilla::AudioData; + using OutputType = EncodedAudioChunk; + using OutputCallbackType = EncodedAudioChunkOutputCallback; + using MetadataType = EncodedAudioChunkMetadata; + + static bool IsSupported(const ConfigTypeInternal& aConfig); + static Result<UniquePtr<TrackInfo>, nsresult> CreateTrackInfo( + const ConfigTypeInternal& aConfig); + static bool Validate(const ConfigType& aConfig, nsCString& aErrorMessage); + static RefPtr<ConfigTypeInternal> CreateConfigInternal( + const ConfigType& aConfig); + static RefPtr<InputTypeInternal> CreateInputInternal( + const InputType& aInput, const VideoEncoderEncodeOptions& aOptions); + static already_AddRefed<OutputConfigType> EncoderConfigToDecoderConfig( + nsIGlobalObject* aGlobal, const RefPtr<MediaRawData>& aData, + const ConfigTypeInternal& mOutputConfig); +}; + class VideoEncoderConfigInternal { public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VideoEncoderConfigInternal); @@ -40,7 +103,7 @@ class VideoEncoderConfigInternal { bool CanReconfigure(const VideoEncoderConfigInternal& aOther) const; already_AddRefed<WebCodecsConfigurationChangeList> Diff( const VideoEncoderConfigInternal& aOther) const; - nsString ToString() const; + nsCString ToString() const; nsString mCodec; uint32_t mWidth; @@ -84,17 +147,14 @@ class VideoEncoderTraits { static bool IsSupported(const ConfigTypeInternal& aConfig); static bool CanEncodeVideo(const ConfigTypeInternal& aConfig); - static Result<UniquePtr<TrackInfo>, nsresult> CreateTrackInfo( - const ConfigTypeInternal& aConfig); static bool Validate(const ConfigType& aConfig, nsCString& aErrorMessage); static RefPtr<ConfigTypeInternal> CreateConfigInternal( const ConfigType& aConfig); static RefPtr<InputTypeInternal> CreateInputInternal( const InputType& aInput, const VideoEncoderEncodeOptions& aOptions); static already_AddRefed<OutputConfigType> EncoderConfigToDecoderConfig( - nsIGlobalObject* aGlobal, - const RefPtr<MediaRawData>& aData, - const ConfigTypeInternal& mOutputConfig); + nsIGlobalObject* aGlobal, const RefPtr<MediaRawData>& aData, + const ConfigTypeInternal& mOutputConfig); }; } // namespace dom diff --git a/dom/media/webcodecs/VideoDecoder.cpp b/dom/media/webcodecs/VideoDecoder.cpp index 18855e5cea..dfc1dff093 100644 --- a/dom/media/webcodecs/VideoDecoder.cpp +++ b/dom/media/webcodecs/VideoDecoder.cpp @@ -96,7 +96,7 @@ VideoColorSpaceInit VideoColorSpaceInternal::ToColorSpaceInit() const { VideoDecoderConfigInternal::VideoDecoderConfigInternal( const nsAString& aCodec, Maybe<uint32_t>&& aCodedHeight, Maybe<uint32_t>&& aCodedWidth, Maybe<VideoColorSpaceInternal>&& aColorSpace, - Maybe<RefPtr<MediaByteBuffer>>&& aDescription, + already_AddRefed<MediaByteBuffer> aDescription, Maybe<uint32_t>&& aDisplayAspectHeight, Maybe<uint32_t>&& aDisplayAspectWidth, const HardwareAcceleration& aHardwareAcceleration, @@ -105,7 +105,7 @@ VideoDecoderConfigInternal::VideoDecoderConfigInternal( mCodedHeight(std::move(aCodedHeight)), mCodedWidth(std::move(aCodedWidth)), mColorSpace(std::move(aColorSpace)), - mDescription(std::move(aDescription)), + mDescription(aDescription), mDisplayAspectHeight(std::move(aDisplayAspectHeight)), mDisplayAspectWidth(std::move(aDisplayAspectWidth)), mHardwareAcceleration(aHardwareAcceleration), @@ -120,7 +120,7 @@ UniquePtr<VideoDecoderConfigInternal> VideoDecoderConfigInternal::Create( return nullptr; } - Maybe<RefPtr<MediaByteBuffer>> description; + RefPtr<MediaByteBuffer> description; if (aConfig.mDescription.WasPassed()) { auto rv = GetExtraDataFromArrayBuffer(aConfig.mDescription.Value()); if (rv.isErr()) { // Invalid description data. @@ -130,7 +130,7 @@ UniquePtr<VideoDecoderConfigInternal> VideoDecoderConfigInternal::Create( static_cast<uint32_t>(rv.unwrapErr())); return nullptr; } - description.emplace(rv.unwrap()); + description = rv.unwrap(); } Maybe<VideoColorSpaceInternal> colorSpace; @@ -141,16 +141,16 @@ UniquePtr<VideoDecoderConfigInternal> VideoDecoderConfigInternal::Create( return UniquePtr<VideoDecoderConfigInternal>(new VideoDecoderConfigInternal( aConfig.mCodec, OptionalToMaybe(aConfig.mCodedHeight), OptionalToMaybe(aConfig.mCodedWidth), std::move(colorSpace), - std::move(description), OptionalToMaybe(aConfig.mDisplayAspectHeight), + description.forget(), OptionalToMaybe(aConfig.mDisplayAspectHeight), OptionalToMaybe(aConfig.mDisplayAspectWidth), aConfig.mHardwareAcceleration, OptionalToMaybe(aConfig.mOptimizeForLatency))); } -nsString VideoDecoderConfigInternal::ToString() const { - nsString rv; +nsCString VideoDecoderConfigInternal::ToString() const { + nsCString rv; - rv.Append(mCodec); + rv.Append(NS_ConvertUTF16toUTF8(mCodec)); if (mCodedWidth.isSome()) { rv.AppendPrintf("coded: %dx%d", mCodedWidth.value(), mCodedHeight.value()); } @@ -161,8 +161,8 @@ nsString VideoDecoderConfigInternal::ToString() const { if (mColorSpace.isSome()) { rv.AppendPrintf("colorspace %s", "todo"); } - if (mDescription.isSome() && mDescription.value()) { - rv.AppendPrintf("extradata: %zu bytes", mDescription.value()->Length()); + if (mDescription) { + rv.AppendPrintf("extradata: %zu bytes", mDescription->Length()); } rv.AppendPrintf("hw accel: %s", GetEnumString(mHardwareAcceleration).get()); if (mOptimizeForLatency.isSome()) { @@ -579,8 +579,7 @@ bool VideoDecoderTraits::IsSupported( /* static */ Result<UniquePtr<TrackInfo>, nsresult> VideoDecoderTraits::CreateTrackInfo( const VideoDecoderConfigInternal& aConfig) { - LOG("Create a VideoInfo from %s config", - NS_ConvertUTF16toUTF8(aConfig.ToString()).get()); + LOG("Create a VideoInfo from %s config", aConfig.ToString().get()); nsTArray<UniquePtr<TrackInfo>> tracks = GetTracksInfo(aConfig); if (tracks.Length() != 1 || tracks[0]->GetType() != TrackInfo::kVideoTrack) { @@ -668,15 +667,14 @@ Result<UniquePtr<TrackInfo>, nsresult> VideoDecoderTraits::CreateTrackInfo( } } - if (aConfig.mDescription.isSome()) { - RefPtr<MediaByteBuffer> buf; - buf = aConfig.mDescription.value(); - if (buf) { - LOG("The given config has %zu bytes of description data", buf->Length()); + if (aConfig.mDescription) { + if (!aConfig.mDescription->IsEmpty()) { + LOG("The given config has %zu bytes of description data", + aConfig.mDescription->Length()); if (vi->mExtraData) { LOGW("The default extra data is overwritten"); } - vi->mExtraData = buf; + vi->mExtraData = aConfig.mDescription; } // TODO: Make this utility and replace the similar one in MP4Demuxer.cpp. diff --git a/dom/media/webcodecs/VideoEncoder.cpp b/dom/media/webcodecs/VideoEncoder.cpp index f593f70c77..5407e917b6 100644 --- a/dom/media/webcodecs/VideoEncoder.cpp +++ b/dom/media/webcodecs/VideoEncoder.cpp @@ -120,11 +120,12 @@ VideoEncoderConfigInternal::VideoEncoderConfigInternal( mContentHint(OptionalToMaybe(aConfig.mContentHint)), mAvc(OptionalToMaybe(aConfig.mAvc)) {} -nsString VideoEncoderConfigInternal::ToString() const { - nsString rv; +nsCString VideoEncoderConfigInternal::ToString() const { + nsCString rv; - rv.AppendPrintf("Codec: %s, [%" PRIu32 "x%" PRIu32 "],", - NS_ConvertUTF16toUTF8(mCodec).get(), mWidth, mHeight); + rv.AppendLiteral("Codec: "); + rv.Append(NS_ConvertUTF16toUTF8(mCodec)); + rv.AppendPrintf(" [%" PRIu32 "x%" PRIu32 "],", mWidth, mHeight); if (mDisplayWidth.isSome()) { rv.AppendPrintf(", display[%" PRIu32 "x%" PRIu32 "]", mDisplayWidth.value(), mDisplayHeight.value()); @@ -194,20 +195,19 @@ bool VideoEncoderConfigInternal::CanReconfigure( } EncoderConfig VideoEncoderConfigInternal::ToEncoderConfig() const { - MediaDataEncoder::Usage usage; + Usage usage; if (mLatencyMode == LatencyMode::Quality) { - usage = MediaDataEncoder::Usage::Record; + usage = Usage::Record; } else { - usage = MediaDataEncoder::Usage::Realtime; + usage = Usage::Realtime; } - MediaDataEncoder::HardwarePreference hwPref = - MediaDataEncoder::HardwarePreference::None; + HardwarePreference hwPref = HardwarePreference::None; if (mHardwareAcceleration == mozilla::dom::HardwareAcceleration::Prefer_hardware) { - hwPref = MediaDataEncoder::HardwarePreference::RequireHardware; + hwPref = HardwarePreference::RequireHardware; } else if (mHardwareAcceleration == mozilla::dom::HardwareAcceleration::Prefer_software) { - hwPref = MediaDataEncoder::HardwarePreference::RequireSoftware; + hwPref = HardwarePreference::RequireSoftware; } CodecType codecType; auto maybeCodecType = CodecStringToCodecType(mCodec); @@ -236,19 +236,19 @@ EncoderConfig VideoEncoderConfigInternal::ToEncoderConfig() const { } } uint8_t numTemporalLayers = 1; - MediaDataEncoder::ScalabilityMode scalabilityMode; + ScalabilityMode scalabilityMode; if (mScalabilityMode) { if (mScalabilityMode->EqualsLiteral("L1T2")) { - scalabilityMode = MediaDataEncoder::ScalabilityMode::L1T2; + scalabilityMode = ScalabilityMode::L1T2; numTemporalLayers = 2; } else if (mScalabilityMode->EqualsLiteral("L1T3")) { - scalabilityMode = MediaDataEncoder::ScalabilityMode::L1T3; + scalabilityMode = ScalabilityMode::L1T3; numTemporalLayers = 3; } else { - scalabilityMode = MediaDataEncoder::ScalabilityMode::None; + scalabilityMode = ScalabilityMode::None; } } else { - scalabilityMode = MediaDataEncoder::ScalabilityMode::None; + scalabilityMode = ScalabilityMode::None; } // Only for vp9, not vp8 if (codecType == CodecType::VP9) { @@ -278,8 +278,8 @@ EncoderConfig VideoEncoderConfigInternal::ToEncoderConfig() const { AssertedCast<uint8_t>(mFramerate.refOr(0.f)), 0, mBitrate.refOr(0), mBitrateMode == VideoEncoderBitrateMode::Constant - ? MediaDataEncoder::BitrateMode::Constant - : MediaDataEncoder::BitrateMode::Variable, + ? mozilla::BitrateMode::Constant + : mozilla::BitrateMode::Variable, hwPref, scalabilityMode, specific); } already_AddRefed<WebCodecsConfigurationChangeList> @@ -558,7 +558,7 @@ already_AddRefed<Promise> VideoEncoder::IsConfigSupported( } RefPtr<EncodedVideoChunk> VideoEncoder::EncodedDataToOutputType( - nsIGlobalObject* aGlobalObject, RefPtr<MediaRawData>& aData) { + nsIGlobalObject* aGlobalObject, const RefPtr<MediaRawData>& aData) { AssertIsOnOwningThread(); MOZ_RELEASE_ASSERT(aData->mType == MediaData::Type::RAW_DATA); @@ -591,8 +591,8 @@ VideoDecoderConfigInternal VideoEncoder::EncoderConfigToDecoderConfig( Some(mOutputConfig.mWidth), /* aCodedWidth */ Some(init), /* aColorSpace */ aRawData->mExtraData && !aRawData->mExtraData->IsEmpty() - ? Some(aRawData->mExtraData) - : Nothing(), /* aDescription*/ + ? aRawData->mExtraData.forget() + : nullptr, /* aDescription*/ Maybe<uint32_t>(mOutputConfig.mDisplayHeight), /* aDisplayAspectHeight*/ Maybe<uint32_t>(mOutputConfig.mDisplayWidth), /* aDisplayAspectWidth */ mOutputConfig.mHardwareAcceleration, /* aHardwareAcceleration */ diff --git a/dom/media/webcodecs/VideoEncoder.h b/dom/media/webcodecs/VideoEncoder.h index 9251b5023a..f6d1bfffb7 100644 --- a/dom/media/webcodecs/VideoEncoder.h +++ b/dom/media/webcodecs/VideoEncoder.h @@ -65,7 +65,7 @@ class VideoEncoder final : public EncoderTemplate<VideoEncoderTraits> { protected: virtual RefPtr<EncodedVideoChunk> EncodedDataToOutputType( - nsIGlobalObject* aGlobal, RefPtr<MediaRawData>& aData) override; + nsIGlobalObject* aGlobal, const RefPtr<MediaRawData>& aData) override; virtual VideoDecoderConfigInternal EncoderConfigToDecoderConfig( nsIGlobalObject* aGlobal /* TODO: delete */, diff --git a/dom/media/webcodecs/WebCodecsUtils.cpp b/dom/media/webcodecs/WebCodecsUtils.cpp index 3507aba440..db4d79220e 100644 --- a/dom/media/webcodecs/WebCodecsUtils.cpp +++ b/dom/media/webcodecs/WebCodecsUtils.cpp @@ -7,6 +7,7 @@ #include "WebCodecsUtils.h" #include "DecoderTypes.h" +#include "PlatformEncoderModule.h" #include "VideoUtils.h" #include "js/experimental/TypedData.h" #include "mozilla/Assertions.h" @@ -15,8 +16,6 @@ #include "mozilla/dom/VideoFrameBinding.h" #include "mozilla/gfx/Types.h" #include "nsDebug.h" -#include "PlatformEncoderModule.h" -#include "PlatformEncoderModule.h" extern mozilla::LazyLogModule gWebCodecsLog; @@ -412,8 +411,8 @@ struct ConfigurationChangeToString { } }; -nsString WebCodecsConfigurationChangeList::ToString() const { - nsString rv; +nsCString WebCodecsConfigurationChangeList::ToString() const { + nsCString rv; for (const WebCodecsEncoderConfigurationItem& change : mChanges) { nsCString str = change.match(ConfigurationChangeToString()); rv.AppendPrintf("- %s\n", str.get()); @@ -470,24 +469,24 @@ WebCodecsConfigurationChangeList::ToPEMChangeList() const { } else if (change.is<FramerateChange>()) { rv->Push(mozilla::FramerateChange(change.as<FramerateChange>().get())); } else if (change.is<dom::BitrateModeChange>()) { - MediaDataEncoder::BitrateMode mode; + mozilla::BitrateMode mode; if (change.as<dom::BitrateModeChange>().get() == dom::VideoEncoderBitrateMode::Constant) { - mode = MediaDataEncoder::BitrateMode::Constant; + mode = mozilla::BitrateMode::Constant; } else if (change.as<BitrateModeChange>().get() == dom::VideoEncoderBitrateMode::Variable) { - mode = MediaDataEncoder::BitrateMode::Variable; + mode = mozilla::BitrateMode::Variable; } else { // Quantizer, not underlying support yet. - mode = MediaDataEncoder::BitrateMode::Variable; + mode = mozilla::BitrateMode::Variable; } rv->Push(mozilla::BitrateModeChange(mode)); } else if (change.is<LatencyModeChange>()) { - MediaDataEncoder::Usage usage; + Usage usage; if (change.as<LatencyModeChange>().get() == dom::LatencyMode::Quality) { - usage = MediaDataEncoder::Usage::Record; + usage = Usage::Record; } else { - usage = MediaDataEncoder::Usage::Realtime; + usage = Usage::Realtime; } rv->Push(UsageChange(usage)); } else if (change.is<ContentHintChange>()) { @@ -570,7 +569,7 @@ Maybe<CodecType> CodecStringToCodecType(const nsAString& aCodecString) { return Nothing(); } -nsString ConfigToString(const VideoDecoderConfig& aConfig) { +nsCString ConfigToString(const VideoDecoderConfig& aConfig) { nsString rv; auto internal = VideoDecoderConfigInternal::Create(aConfig); @@ -606,4 +605,52 @@ bool IsSupportedVideoCodec(const nsAString& aCodec) { return true; } +nsCString ConvertCodecName(const nsCString& aContainer, + const nsCString& aCodec) { + if (!aContainer.EqualsLiteral("x-wav")) { + return aCodec; + } + + // https://www.rfc-editor.org/rfc/rfc2361.txt + if (aCodec.EqualsLiteral("ulaw")) { + return nsCString("7"); + } + if (aCodec.EqualsLiteral("alaw")) { + return nsCString("6"); + } + if (aCodec.Find("f32")) { + return nsCString("3"); + } + // Linear PCM + return nsCString("1"); +} + +bool IsSupportedAudioCodec(const nsAString& aCodec) { + LOG("IsSupportedAudioCodec: %s", NS_ConvertUTF16toUTF8(aCodec).get()); + return aCodec.EqualsLiteral("flac") || aCodec.EqualsLiteral("mp3") || + IsAACCodecString(aCodec) || aCodec.EqualsLiteral("opus") || + aCodec.EqualsLiteral("ulaw") || aCodec.EqualsLiteral("alaw") || + aCodec.EqualsLiteral("pcm-u8") || aCodec.EqualsLiteral("pcm-s16") || + aCodec.EqualsLiteral("pcm-s24") || aCodec.EqualsLiteral("pcm-s32") || + aCodec.EqualsLiteral("pcm-f32"); +} + +uint32_t BytesPerSamples(const mozilla::dom::AudioSampleFormat& aFormat) { + switch (aFormat) { + case AudioSampleFormat::U8: + case AudioSampleFormat::U8_planar: + return sizeof(uint8_t); + case AudioSampleFormat::S16: + case AudioSampleFormat::S16_planar: + return sizeof(int16_t); + case AudioSampleFormat::S32: + case AudioSampleFormat::F32: + case AudioSampleFormat::S32_planar: + case AudioSampleFormat::F32_planar: + return sizeof(float); + } + MOZ_ASSERT_UNREACHABLE("Invalid enum value"); + return 0; +} + } // namespace mozilla::dom diff --git a/dom/media/webcodecs/WebCodecsUtils.h b/dom/media/webcodecs/WebCodecsUtils.h index 196c57421d..b2a8f17398 100644 --- a/dom/media/webcodecs/WebCodecsUtils.h +++ b/dom/media/webcodecs/WebCodecsUtils.h @@ -9,17 +9,18 @@ #include "ErrorList.h" #include "MediaData.h" +#include "PlatformEncoderModule.h" #include "js/TypeDecls.h" #include "mozilla/Maybe.h" #include "mozilla/MozPromise.h" #include "mozilla/Result.h" #include "mozilla/TaskQueue.h" +#include "mozilla/dom/AudioDataBinding.h" #include "mozilla/dom/BindingDeclarations.h" #include "mozilla/dom/Nullable.h" #include "mozilla/dom/UnionTypes.h" #include "mozilla/dom/VideoEncoderBinding.h" #include "mozilla/dom/VideoFrameBinding.h" -#include "PlatformEncoderModule.h" namespace mozilla { @@ -218,7 +219,7 @@ struct WebCodecsConfigurationChangeList { // Convert this to the format the underlying PEM can understand RefPtr<EncoderConfigurationChangeList> ToPEMChangeList() const; - nsString ToString() const; + nsCString ToString() const; nsTArray<WebCodecsEncoderConfigurationItem> mChanges; @@ -235,12 +236,20 @@ VideoColorSpaceInit FallbackColorSpaceForWebContent(); Maybe<CodecType> CodecStringToCodecType(const nsAString& aCodecString); -nsString ConfigToString(const VideoDecoderConfig& aConfig); +nsCString ConfigToString(const VideoDecoderConfig& aConfig); +// Returns true if a particular codec is supported by WebCodecs. bool IsSupportedVideoCodec(const nsAString& aCodec); +bool IsSupportedAudioCodec(const nsAString& aCodec); -} // namespace dom +// Returns the codec string to use in Gecko for a particular container and +// codec name given by WebCodecs. This maps pcm description to the profile +// number, and simply returns the codec name for all other codecs. +nsCString ConvertCodecName(const nsCString& aContainer, + const nsCString& aCodec); +uint32_t BytesPerSamples(const mozilla::dom::AudioSampleFormat& aFormat); +} // namespace dom } // namespace mozilla #endif // MOZILLA_DOM_WEBCODECS_WEBCODECSUTILS_H diff --git a/dom/media/webcodecs/crashtests/1881079.html b/dom/media/webcodecs/crashtests/1881079.html new file mode 100644 index 0000000000..15fd26ff74 --- /dev/null +++ b/dom/media/webcodecs/crashtests/1881079.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<script> +document.addEventListener("DOMContentLoaded", async () => { + const encoder = new VideoEncoder({ + 'output': (e) => {}, + 'error': (e) => {}, + }) + encoder.configure({ + 'codec': '᩿', + 'width': 2147483648, + 'height': 60, + }) + encoder.reset() + encoder.configure({ + 'codec': '᩿', + 'width': 4294967295, + 'height': 29, + }) + const decoder = new VideoDecoder({ + 'output': (e) => {}, + 'error': (e) => {}, + }) + decoder.configure({ + 'codec': '᩿', + 'width': 2147483648, + 'height': 60, + }) + decoder.reset() + decoder.configure({ + 'codec': '᩿', + 'width': 4294967295, + 'height': 29, + }) +}) +</script> diff --git a/dom/media/webcodecs/crashtests/crashtests.list b/dom/media/webcodecs/crashtests/crashtests.list index cea5139fe9..16fbd90ff5 100644 --- a/dom/media/webcodecs/crashtests/crashtests.list +++ b/dom/media/webcodecs/crashtests/crashtests.list @@ -1,4 +1,6 @@ skip-if(Android) pref(dom.media.webcodecs.enabled,true) load 1839270.html skip-if(Android) pref(dom.media.webcodecs.enabled,true) load 1848460.html skip-if(Android) pref(dom.media.webcodecs.enabled,true) load 1849271.html -skip-if(Android) pref(dom.media.webcodecs.enabled,true) load 1864475.html
\ No newline at end of file +skip-if(Android) pref(dom.media.webcodecs.enabled,true) load 1864475.html +skip-if(Android) pref(dom.media.webcodecs.enabled,true) load 1881079.html + diff --git a/dom/media/webcodecs/moz.build b/dom/media/webcodecs/moz.build index ddb5aad5cb..1c398439a3 100644 --- a/dom/media/webcodecs/moz.build +++ b/dom/media/webcodecs/moz.build @@ -23,6 +23,7 @@ EXPORTS.mozilla += [ EXPORTS.mozilla.dom += [ "AudioData.h", "AudioDecoder.h", + "AudioEncoder.h", "DecoderTemplate.h", "DecoderTypes.h", "EncodedAudioChunk.h", @@ -40,6 +41,7 @@ EXPORTS.mozilla.dom += [ UNIFIED_SOURCES += [ "AudioData.cpp", "AudioDecoder.cpp", + "AudioEncoder.cpp", "DecoderAgent.cpp", "DecoderTemplate.cpp", "EncodedAudioChunk.cpp", diff --git a/dom/media/webrtc/MediaEnginePrefs.h b/dom/media/webrtc/MediaEnginePrefs.h index cedb7f457c..de5daf0ad9 100644 --- a/dom/media/webrtc/MediaEnginePrefs.h +++ b/dom/media/webrtc/MediaEnginePrefs.h @@ -35,6 +35,7 @@ class MediaEnginePrefs { mNoiseOn(false), mTransientOn(false), mAgc2Forced(false), + mExpectDrift(-1), // auto mAgc(0), mNoise(0), mChannels(0) {} @@ -50,6 +51,7 @@ class MediaEnginePrefs { bool mNoiseOn; bool mTransientOn; bool mAgc2Forced; + int32_t mExpectDrift; int32_t mAgc; int32_t mNoise; int32_t mChannels; diff --git a/dom/media/webrtc/MediaEngineWebRTCAudio.cpp b/dom/media/webrtc/MediaEngineWebRTCAudio.cpp index 9d778d411d..220dcf3bd8 100644 --- a/dom/media/webrtc/MediaEngineWebRTCAudio.cpp +++ b/dom/media/webrtc/MediaEngineWebRTCAudio.cpp @@ -20,6 +20,7 @@ #include "mozilla/Sprintf.h" #include "mozilla/Logging.h" +#include "api/audio/echo_canceller3_factory.h" #include "common_audio/include/audio_util.h" #include "modules/audio_processing/include/audio_processing.h" @@ -146,22 +147,17 @@ nsresult MediaEngineWebRTCMicrophoneSource::Reconfigure( return NS_OK; } -void MediaEngineWebRTCMicrophoneSource::ApplySettings( +AudioProcessing::Config AudioInputProcessing::ConfigForPrefs( const MediaEnginePrefs& aPrefs) { - AssertIsOnOwningThread(); - - TRACE("ApplySettings"); - MOZ_ASSERT( - mTrack, - "ApplySetting is to be called only after SetTrack has been called"); + AudioProcessing::Config config; - mAudioProcessingConfig.pipeline.multi_channel_render = true; - mAudioProcessingConfig.pipeline.multi_channel_capture = true; + config.pipeline.multi_channel_render = true; + config.pipeline.multi_channel_capture = true; - mAudioProcessingConfig.echo_canceller.enabled = aPrefs.mAecOn; - mAudioProcessingConfig.echo_canceller.mobile_mode = aPrefs.mUseAecMobile; + config.echo_canceller.enabled = aPrefs.mAecOn; + config.echo_canceller.mobile_mode = aPrefs.mUseAecMobile; - if ((mAudioProcessingConfig.gain_controller1.enabled = + if ((config.gain_controller1.enabled = aPrefs.mAgcOn && !aPrefs.mAgc2Forced)) { auto mode = static_cast<AudioProcessing::Config::GainController1::Mode>( aPrefs.mAgc); @@ -169,7 +165,7 @@ void MediaEngineWebRTCMicrophoneSource::ApplySettings( mode != AudioProcessing::Config::GainController1::kAdaptiveDigital && mode != AudioProcessing::Config::GainController1::kFixedDigital) { LOG_ERROR("AudioInputProcessing %p Attempt to set invalid AGC mode %d", - mInputProcessing.get(), static_cast<int>(mode)); + this, static_cast<int>(mode)); mode = AudioProcessing::Config::GainController1::kAdaptiveDigital; } #if defined(WEBRTC_IOS) || defined(ATA) || defined(WEBRTC_ANDROID) @@ -177,20 +173,20 @@ void MediaEngineWebRTCMicrophoneSource::ApplySettings( LOG_ERROR( "AudioInputProcessing %p Invalid AGC mode kAdaptiveAnalog on " "mobile", - mInputProcessing.get()); + this); MOZ_ASSERT_UNREACHABLE( "Bad pref set in all.js or in about:config" " for the auto gain, on mobile."); mode = AudioProcessing::Config::GainController1::kFixedDigital; } #endif - mAudioProcessingConfig.gain_controller1.mode = mode; + config.gain_controller1.mode = mode; } - mAudioProcessingConfig.gain_controller2.enabled = - mAudioProcessingConfig.gain_controller2.adaptive_digital.enabled = + config.gain_controller2.enabled = + config.gain_controller2.adaptive_digital.enabled = aPrefs.mAgcOn && aPrefs.mAgc2Forced; - if ((mAudioProcessingConfig.noise_suppression.enabled = aPrefs.mNoiseOn)) { + if ((config.noise_suppression.enabled = aPrefs.mNoiseOn)) { auto level = static_cast<AudioProcessing::Config::NoiseSuppression::Level>( aPrefs.mNoise); if (level != AudioProcessing::Config::NoiseSuppression::kLow && @@ -200,49 +196,44 @@ void MediaEngineWebRTCMicrophoneSource::ApplySettings( LOG_ERROR( "AudioInputProcessing %p Attempt to set invalid noise suppression " "level %d", - mInputProcessing.get(), static_cast<int>(level)); + this, static_cast<int>(level)); level = AudioProcessing::Config::NoiseSuppression::kModerate; } - mAudioProcessingConfig.noise_suppression.level = level; + config.noise_suppression.level = level; } - mAudioProcessingConfig.transient_suppression.enabled = aPrefs.mTransientOn; + config.transient_suppression.enabled = aPrefs.mTransientOn; + + config.high_pass_filter.enabled = aPrefs.mHPFOn; - mAudioProcessingConfig.high_pass_filter.enabled = aPrefs.mHPFOn; + return config; +} + +void MediaEngineWebRTCMicrophoneSource::ApplySettings( + const MediaEnginePrefs& aPrefs) { + AssertIsOnOwningThread(); + + TRACE("ApplySettings"); + MOZ_ASSERT( + mTrack, + "ApplySetting is to be called only after SetTrack has been called"); RefPtr<MediaEngineWebRTCMicrophoneSource> that = this; CubebUtils::AudioDeviceID deviceID = mDeviceInfo->DeviceID(); NS_DispatchToMainThread(NS_NewRunnableFunction( - __func__, [this, that, deviceID, track = mTrack, prefs = aPrefs, - audioProcessingConfig = mAudioProcessingConfig] { + __func__, [this, that, deviceID, track = mTrack, prefs = aPrefs] { mSettings->mEchoCancellation.Value() = prefs.mAecOn; mSettings->mAutoGainControl.Value() = prefs.mAgcOn; mSettings->mNoiseSuppression.Value() = prefs.mNoiseOn; mSettings->mChannelCount.Value() = prefs.mChannels; - // The high-pass filter is not taken into account when activating the - // pass through, since it's not controllable from content. - bool passThrough = !(prefs.mAecOn || prefs.mAgcOn || prefs.mNoiseOn); - if (track->IsDestroyed()) { return; } track->QueueControlMessageWithNoShutdown( - [track, deviceID, inputProcessing = mInputProcessing, - audioProcessingConfig, passThrough, - requestedInputChannelCount = prefs.mChannels] { - inputProcessing->ApplyConfig(track->Graph(), - audioProcessingConfig); - { - TRACE("SetRequestedInputChannelCount"); - inputProcessing->SetRequestedInputChannelCount( - track->Graph(), deviceID, requestedInputChannelCount); - } - { - TRACE("SetPassThrough"); - inputProcessing->SetPassThrough(track->Graph(), passThrough); - } + [track, deviceID, prefs, inputProcessing = mInputProcessing] { + inputProcessing->ApplySettings(track->Graph(), deviceID, prefs); }); })); } @@ -408,57 +399,51 @@ void MediaEngineWebRTCMicrophoneSource::GetSettings( } AudioInputProcessing::AudioInputProcessing(uint32_t aMaxChannelCount) - : mAudioProcessing(AudioProcessingBuilder().Create().release()), - mRequestedInputChannelCount(aMaxChannelCount), - mSkipProcessing(false), - mInputDownmixBuffer(MAX_SAMPLING_FREQ * MAX_CHANNELS / 100), + : mInputDownmixBuffer(MAX_SAMPLING_FREQ * MAX_CHANNELS / 100), mEnabled(false), mEnded(false), - mPacketCount(0) {} + mPacketCount(0) { + mSettings.mChannels = static_cast<int32_t>(std::min<uint32_t>( + std::numeric_limits<int32_t>::max(), aMaxChannelCount)); +} void AudioInputProcessing::Disconnect(MediaTrackGraph* aGraph) { // This method is just for asserts. aGraph->AssertOnGraphThread(); } -bool AudioInputProcessing::PassThrough(MediaTrackGraph* aGraph) const { +bool AudioInputProcessing::IsPassThrough(MediaTrackGraph* aGraph) const { aGraph->AssertOnGraphThread(); - return mSkipProcessing; + // The high-pass filter is not taken into account when activating the + // pass through, since it's not controllable from content. + return !(mSettings.mAecOn || mSettings.mAgcOn || mSettings.mNoiseOn); } -void AudioInputProcessing::SetPassThrough(MediaTrackGraph* aGraph, - bool aPassThrough) { +void AudioInputProcessing::PassThroughChanged(MediaTrackGraph* aGraph) { aGraph->AssertOnGraphThread(); - if (aPassThrough == mSkipProcessing) { - return; - } - mSkipProcessing = aPassThrough; - if (!mEnabled) { MOZ_ASSERT(!mPacketizerInput); return; } - if (aPassThrough) { - // Turn on pass-through + if (IsPassThrough(aGraph)) { + // Switching to pass-through. Clear state so that it doesn't affect any + // future processing, if re-enabled. ResetAudioProcessing(aGraph); } else { - // Turn off pass-through + // No longer pass-through. Processing will not use old state. + // Packetizer setup is deferred until needed. MOZ_ASSERT(!mPacketizerInput); - EnsureAudioProcessing(aGraph, mRequestedInputChannelCount); } } uint32_t AudioInputProcessing::GetRequestedInputChannelCount() { - return mRequestedInputChannelCount; + return mSettings.mChannels; } -void AudioInputProcessing::SetRequestedInputChannelCount( - MediaTrackGraph* aGraph, CubebUtils::AudioDeviceID aDeviceId, - uint32_t aRequestedInputChannelCount) { - mRequestedInputChannelCount = aRequestedInputChannelCount; - +void AudioInputProcessing::RequestedInputChannelCountChanged( + MediaTrackGraph* aGraph, CubebUtils::AudioDeviceID aDeviceId) { aGraph->ReevaluateInputDevice(aDeviceId); } @@ -470,12 +455,7 @@ void AudioInputProcessing::Start(MediaTrackGraph* aGraph) { } mEnabled = true; - if (mSkipProcessing) { - return; - } - MOZ_ASSERT(!mPacketizerInput); - EnsureAudioProcessing(aGraph, mRequestedInputChannelCount); } void AudioInputProcessing::Stop(MediaTrackGraph* aGraph) { @@ -487,7 +467,7 @@ void AudioInputProcessing::Stop(MediaTrackGraph* aGraph) { mEnabled = false; - if (mSkipProcessing) { + if (IsPassThrough(aGraph)) { return; } @@ -605,10 +585,11 @@ void AudioInputProcessing::Stop(MediaTrackGraph* aGraph) { // // The D(N) frames of data are just forwarded from input to output without any // processing -void AudioInputProcessing::Process(MediaTrackGraph* aGraph, GraphTime aFrom, - GraphTime aTo, AudioSegment* aInput, +void AudioInputProcessing::Process(AudioProcessingTrack* aTrack, + GraphTime aFrom, GraphTime aTo, + AudioSegment* aInput, AudioSegment* aOutput) { - aGraph->AssertOnGraphThread(); + aTrack->AssertOnGraphThread(); MOZ_ASSERT(aFrom <= aTo); MOZ_ASSERT(!mEnded); @@ -617,10 +598,11 @@ void AudioInputProcessing::Process(MediaTrackGraph* aGraph, GraphTime aFrom, return; } + MediaTrackGraph* graph = aTrack->Graph(); if (!mEnabled) { LOG_FRAME("(Graph %p, Driver %p) AudioInputProcessing %p Filling %" PRId64 " frames of silence to output (disabled)", - aGraph, aGraph->CurrentDriver(), this, need); + graph, graph->CurrentDriver(), this, need); aOutput->AppendNullData(need); return; } @@ -628,22 +610,20 @@ void AudioInputProcessing::Process(MediaTrackGraph* aGraph, GraphTime aFrom, MOZ_ASSERT(aInput->GetDuration() == need, "Wrong data length from input port source"); - if (PassThrough(aGraph)) { + if (IsPassThrough(graph)) { LOG_FRAME( "(Graph %p, Driver %p) AudioInputProcessing %p Forwarding %" PRId64 " frames of input data to output directly (PassThrough)", - aGraph, aGraph->CurrentDriver(), this, aInput->GetDuration()); + graph, graph->CurrentDriver(), this, aInput->GetDuration()); aOutput->AppendSegment(aInput); return; } - // SetPassThrough(false) must be called before reaching here. - MOZ_ASSERT(mPacketizerInput); - // If mRequestedInputChannelCount is updated, create a new packetizer. No - // need to change the pre-buffering since the rate is always the same. The - // frames left in the packetizer would be replaced by null data and then - // transferred to mSegment. - EnsureAudioProcessing(aGraph, mRequestedInputChannelCount); + // If the requested input channel count is updated, create a new + // packetizer. No need to change the pre-buffering since the rate is always + // the same. The frames left in the packetizer would be replaced by null + // data and then transferred to mSegment. + EnsurePacketizer(aTrack); // Preconditions of the audio-processing logic. MOZ_ASSERT(static_cast<uint32_t>(mSegment.GetDuration()) + @@ -655,10 +635,10 @@ void AudioInputProcessing::Process(MediaTrackGraph* aGraph, GraphTime aFrom, MOZ_ASSERT(mSegment.GetDuration() >= 1); MOZ_ASSERT(mSegment.GetDuration() <= mPacketizerInput->mPacketSize); - PacketizeAndProcess(aGraph, *aInput); + PacketizeAndProcess(aTrack, *aInput); LOG_FRAME("(Graph %p, Driver %p) AudioInputProcessing %p Buffer has %" PRId64 " frames of data now, after packetizing and processing", - aGraph, aGraph->CurrentDriver(), this, mSegment.GetDuration()); + graph, graph->CurrentDriver(), this, mSegment.GetDuration()); // By setting pre-buffering to the number of frames of one packet, and // because the maximum number of frames stuck in the packetizer before @@ -669,8 +649,7 @@ void AudioInputProcessing::Process(MediaTrackGraph* aGraph, GraphTime aFrom, mSegment.RemoveLeading(need); LOG_FRAME("(Graph %p, Driver %p) AudioInputProcessing %p moving %" PRId64 " frames of data to output, leaving %" PRId64 " frames in buffer", - aGraph, aGraph->CurrentDriver(), this, need, - mSegment.GetDuration()); + graph, graph->CurrentDriver(), this, need, mSegment.GetDuration()); // Postconditions of the audio-processing logic. MOZ_ASSERT(static_cast<uint32_t>(mSegment.GetDuration()) + @@ -680,16 +659,16 @@ void AudioInputProcessing::Process(MediaTrackGraph* aGraph, GraphTime aFrom, MOZ_ASSERT(mSegment.GetDuration() <= mPacketizerInput->mPacketSize); } -void AudioInputProcessing::ProcessOutputData(MediaTrackGraph* aGraph, +void AudioInputProcessing::ProcessOutputData(AudioProcessingTrack* aTrack, const AudioChunk& aChunk) { MOZ_ASSERT(aChunk.ChannelCount() > 0); - aGraph->AssertOnGraphThread(); + aTrack->AssertOnGraphThread(); - if (!mEnabled || PassThrough(aGraph)) { + if (!mEnabled || IsPassThrough(aTrack->Graph())) { return; } - TrackRate sampleRate = aGraph->GraphRate(); + TrackRate sampleRate = aTrack->mSampleRate; uint32_t framesPerPacket = GetPacketSize(sampleRate); // in frames // Downmix from aChannels to MAX_CHANNELS if needed. uint32_t channelCount = @@ -727,6 +706,7 @@ void AudioInputProcessing::ProcessOutputData(MediaTrackGraph* aGraph, if (mOutputBufferFrameCount == framesPerPacket) { // Have a complete packet. Analyze it. + EnsureAudioProcessing(aTrack); for (uint32_t channel = 0; channel < channelCount; channel++) { channelPtrs[channel] = &mOutputBuffer[channel * framesPerPacket]; } @@ -743,14 +723,15 @@ void AudioInputProcessing::ProcessOutputData(MediaTrackGraph* aGraph, } // Only called if we're not in passthrough mode -void AudioInputProcessing::PacketizeAndProcess(MediaTrackGraph* aGraph, +void AudioInputProcessing::PacketizeAndProcess(AudioProcessingTrack* aTrack, const AudioSegment& aSegment) { - MOZ_ASSERT(!PassThrough(aGraph), + MediaTrackGraph* graph = aTrack->Graph(); + MOZ_ASSERT(!IsPassThrough(graph), "This should be bypassed when in PassThrough mode."); MOZ_ASSERT(mEnabled); MOZ_ASSERT(mPacketizerInput); MOZ_ASSERT(mPacketizerInput->mPacketSize == - GetPacketSize(aGraph->GraphRate())); + GetPacketSize(aTrack->mSampleRate)); // Calculate number of the pending frames in mChunksInPacketizer. auto pendingFrames = [&]() { @@ -792,7 +773,7 @@ void AudioInputProcessing::PacketizeAndProcess(MediaTrackGraph* aGraph, LOG_FRAME( "(Graph %p, Driver %p) AudioInputProcessing %p Packetizing %zu frames. " "Packetizer has %u frames (enough for %u packets) now", - aGraph, aGraph->CurrentDriver(), this, frameCount, + graph, graph->CurrentDriver(), this, frameCount, mPacketizerInput->FramesAvailable(), mPacketizerInput->PacketsAvailable()); @@ -850,9 +831,10 @@ void AudioInputProcessing::PacketizeAndProcess(MediaTrackGraph* aGraph, deinterleavedPacketizedInputDataChannelPointers.Elements()); } - StreamConfig inputConfig(aGraph->GraphRate(), channelCountInput); + StreamConfig inputConfig(aTrack->mSampleRate, channelCountInput); StreamConfig outputConfig = inputConfig; + EnsureAudioProcessing(aTrack); // Bug 1404965: Get the right delay here, it saves some work down the line. mAudioProcessing->set_stream_delay_ms(0); @@ -958,7 +940,7 @@ void AudioInputProcessing::PacketizeAndProcess(MediaTrackGraph* aGraph, "(Graph %p, Driver %p) AudioInputProcessing %p Appending %u frames of " "packetized audio, leaving %u frames in packetizer (%" PRId64 " frames in mChunksInPacketizer)", - aGraph, aGraph->CurrentDriver(), this, mPacketizerInput->mPacketSize, + graph, graph->CurrentDriver(), this, mPacketizerInput->mPacketSize, mPacketizerInput->FramesAvailable(), pendingFrames()); // Postcondition of the Principal-labelling logic. @@ -971,17 +953,36 @@ void AudioInputProcessing::DeviceChanged(MediaTrackGraph* aGraph) { aGraph->AssertOnGraphThread(); // Reset some processing - mAudioProcessing->Initialize(); + if (mAudioProcessing) { + mAudioProcessing->Initialize(); + } LOG_FRAME( "(Graph %p, Driver %p) AudioInputProcessing %p Reinitializing audio " "processing", aGraph, aGraph->CurrentDriver(), this); } -void AudioInputProcessing::ApplyConfig(MediaTrackGraph* aGraph, - const AudioProcessing::Config& aConfig) { +void AudioInputProcessing::ApplySettings(MediaTrackGraph* aGraph, + CubebUtils::AudioDeviceID aDeviceID, + const MediaEnginePrefs& aSettings) { + TRACE("AudioInputProcessing::ApplySettings"); aGraph->AssertOnGraphThread(); - mAudioProcessing->ApplyConfig(aConfig); + + // Read previous state from mSettings. + uint32_t oldChannelCount = GetRequestedInputChannelCount(); + bool wasPassThrough = IsPassThrough(aGraph); + + mSettings = aSettings; + if (mAudioProcessing) { + mAudioProcessing->ApplyConfig(ConfigForPrefs(aSettings)); + } + + if (oldChannelCount != GetRequestedInputChannelCount()) { + RequestedInputChannelCountChanged(aGraph, aDeviceID); + } + if (wasPassThrough != IsPassThrough(aGraph)) { + PassThroughChanged(aGraph); + } } void AudioInputProcessing::End() { @@ -995,14 +996,15 @@ TrackTime AudioInputProcessing::NumBufferedFrames( return mSegment.GetDuration(); } -void AudioInputProcessing::EnsureAudioProcessing(MediaTrackGraph* aGraph, - uint32_t aChannels) { - aGraph->AssertOnGraphThread(); - MOZ_ASSERT(aChannels > 0); +void AudioInputProcessing::EnsurePacketizer(AudioProcessingTrack* aTrack) { + aTrack->AssertOnGraphThread(); MOZ_ASSERT(mEnabled); - MOZ_ASSERT(!mSkipProcessing); + MediaTrackGraph* graph = aTrack->Graph(); + MOZ_ASSERT(!IsPassThrough(graph)); - if (mPacketizerInput && mPacketizerInput->mChannels == aChannels) { + uint32_t channelCount = GetRequestedInputChannelCount(); + MOZ_ASSERT(channelCount > 0); + if (mPacketizerInput && mPacketizerInput->mChannels == channelCount) { return; } @@ -1010,7 +1012,7 @@ void AudioInputProcessing::EnsureAudioProcessing(MediaTrackGraph* aGraph, // need to change pre-buffering since the packet size is the same as the old // one, since the rate is a constant. MOZ_ASSERT_IF(mPacketizerInput, mPacketizerInput->mPacketSize == - GetPacketSize(aGraph->GraphRate())); + GetPacketSize(aTrack->mSampleRate)); bool needPreBuffering = !mPacketizerInput; if (mPacketizerInput) { const TrackTime numBufferedFrames = @@ -1020,24 +1022,62 @@ void AudioInputProcessing::EnsureAudioProcessing(MediaTrackGraph* aGraph, mChunksInPacketizer.clear(); } - mPacketizerInput.emplace(GetPacketSize(aGraph->GraphRate()), aChannels); + mPacketizerInput.emplace(GetPacketSize(aTrack->mSampleRate), channelCount); if (needPreBuffering) { LOG_FRAME( "(Graph %p, Driver %p) AudioInputProcessing %p: Adding %u frames of " "silence as pre-buffering", - aGraph, aGraph->CurrentDriver(), this, mPacketizerInput->mPacketSize); + graph, graph->CurrentDriver(), this, mPacketizerInput->mPacketSize); AudioSegment buffering; buffering.AppendNullData( static_cast<TrackTime>(mPacketizerInput->mPacketSize)); - PacketizeAndProcess(aGraph, buffering); + PacketizeAndProcess(aTrack, buffering); + } +} + +void AudioInputProcessing::EnsureAudioProcessing(AudioProcessingTrack* aTrack) { + aTrack->AssertOnGraphThread(); + + MediaTrackGraph* graph = aTrack->Graph(); + // If the AEC might need to deal with drift then inform it of this and it + // will be less conservative about echo suppression. This can lead to some + // suppression of non-echo signal, so do this only when drift is expected. + // https://bugs.chromium.org/p/webrtc/issues/detail?id=11985#c2 + bool haveAECAndDrift = mSettings.mAecOn; + if (haveAECAndDrift) { + if (mSettings.mExpectDrift < 0) { + haveAECAndDrift = + graph->OutputForAECMightDrift() || + aTrack->GetDeviceInputTrackGraphThread()->AsNonNativeInputTrack(); + } else { + haveAECAndDrift = mSettings.mExpectDrift > 0; + } + } + if (!mAudioProcessing || haveAECAndDrift != mHadAECAndDrift) { + TRACE("AudioProcessing creation"); + LOG("Track %p AudioInputProcessing %p creating AudioProcessing. " + "aec+drift: %s", + aTrack, this, haveAECAndDrift ? "Y" : "N"); + mHadAECAndDrift = haveAECAndDrift; + AudioProcessingBuilder builder; + builder.SetConfig(ConfigForPrefs(mSettings)); + if (haveAECAndDrift) { + // Setting an EchoControlFactory always enables AEC, overriding + // Config::echo_canceller.enabled, so do this only when AEC is enabled. + EchoCanceller3Config aec3Config; + aec3Config.echo_removal_control.has_clock_drift = true; + builder.SetEchoControlFactory( + std::make_unique<EchoCanceller3Factory>(aec3Config)); + } + mAudioProcessing.reset(builder.Create().release()); } } void AudioInputProcessing::ResetAudioProcessing(MediaTrackGraph* aGraph) { aGraph->AssertOnGraphThread(); - MOZ_ASSERT(mSkipProcessing || !mEnabled); + MOZ_ASSERT(IsPassThrough(aGraph) || !mEnabled); MOZ_ASSERT(mPacketizerInput); LOG_FRAME( @@ -1047,7 +1087,9 @@ void AudioInputProcessing::ResetAudioProcessing(MediaTrackGraph* aGraph) { // Reset AudioProcessing so that if we resume processing in the future it // doesn't depend on old state. - mAudioProcessing->Initialize(); + if (mAudioProcessing) { + mAudioProcessing->Initialize(); + } MOZ_ASSERT(static_cast<uint32_t>(mSegment.GetDuration()) + mPacketizerInput->FramesAvailable() == @@ -1124,9 +1166,8 @@ void AudioProcessingTrack::ProcessInput(GraphTime aFrom, GraphTime aTo, } else { MOZ_ASSERT(mInputs.Length() == 1); AudioSegment data; - DeviceInputConsumerTrack::GetInputSourceData(data, mInputs[0], aFrom, - aTo); - mInputProcessing->Process(Graph(), aFrom, aTo, &data, + DeviceInputConsumerTrack::GetInputSourceData(data, aFrom, aTo); + mInputProcessing->Process(this, aFrom, aTo, &data, GetData<AudioSegment>()); } MOZ_ASSERT(TrackTimeToGraphTime(GetEnd()) == aTo); @@ -1142,7 +1183,7 @@ void AudioProcessingTrack::NotifyOutputData(MediaTrackGraph* aGraph, MOZ_ASSERT(mGraph == aGraph, "Cannot feed audio output to another graph"); AssertOnGraphThread(); if (mInputProcessing) { - mInputProcessing->ProcessOutputData(aGraph, aChunk); + mInputProcessing->ProcessOutputData(this, aChunk); } } diff --git a/dom/media/webrtc/MediaEngineWebRTCAudio.h b/dom/media/webrtc/MediaEngineWebRTCAudio.h index e71b5ef826..6b1fbf0089 100644 --- a/dom/media/webrtc/MediaEngineWebRTCAudio.h +++ b/dom/media/webrtc/MediaEngineWebRTCAudio.h @@ -91,8 +91,7 @@ class MediaEngineWebRTCMicrophoneSource : public MediaEngineSource { // Current state of the resource for this source. MediaEngineSourceState mState; - // The current preferences that will be forwarded to mAudioProcessingConfig - // below. + // The current preferences that will be forwarded to mInputProcessing below. MediaEnginePrefs mCurrentPrefs; // The AudioProcessingTrack used to inteface with the MediaTrackGraph. Set in @@ -101,10 +100,6 @@ class MediaEngineWebRTCMicrophoneSource : public MediaEngineSource { // See note at the top of this class. RefPtr<AudioInputProcessing> mInputProcessing; - - // Copy of the config currently applied to AudioProcessing through - // mInputProcessing. - webrtc::AudioProcessing::Config mAudioProcessingConfig; }; // This class is created on the MediaManager thread, and then exclusively used @@ -113,15 +108,16 @@ class MediaEngineWebRTCMicrophoneSource : public MediaEngineSource { class AudioInputProcessing : public AudioDataListener { public: explicit AudioInputProcessing(uint32_t aMaxChannelCount); - void Process(MediaTrackGraph* aGraph, GraphTime aFrom, GraphTime aTo, + void Process(AudioProcessingTrack* aTrack, GraphTime aFrom, GraphTime aTo, AudioSegment* aInput, AudioSegment* aOutput); - void ProcessOutputData(MediaTrackGraph* aGraph, const AudioChunk& aChunk); + void ProcessOutputData(AudioProcessingTrack* aTrack, + const AudioChunk& aChunk); bool IsVoiceInput(MediaTrackGraph* aGraph) const override { // If we're passing data directly without AEC or any other process, this // means that all voice-processing has been disabled intentionaly. In this // case, consider that the device is not used for voice input. - return !PassThrough(aGraph); + return !IsPassThrough(aGraph); } void Start(MediaTrackGraph* aGraph); @@ -135,23 +131,20 @@ class AudioInputProcessing : public AudioDataListener { void Disconnect(MediaTrackGraph* aGraph) override; - void PacketizeAndProcess(MediaTrackGraph* aGraph, + void PacketizeAndProcess(AudioProcessingTrack* aTrack, const AudioSegment& aSegment); - void SetPassThrough(MediaTrackGraph* aGraph, bool aPassThrough); uint32_t GetRequestedInputChannelCount(); - void SetRequestedInputChannelCount(MediaTrackGraph* aGraph, - CubebUtils::AudioDeviceID aDeviceId, - uint32_t aRequestedInputChannelCount); - // This is true when all processing is disabled, we can skip + // This is true when all processing is disabled, in which case we can skip // packetization, resampling and other processing passes. - bool PassThrough(MediaTrackGraph* aGraph) const; + bool IsPassThrough(MediaTrackGraph* aGraph) const; // This allow changing the APM options, enabling or disabling processing - // steps. The config gets applied the next time we're about to process input + // steps. The settings get applied the next time we're about to process input // data. - void ApplyConfig(MediaTrackGraph* aGraph, - const webrtc::AudioProcessing::Config& aConfig); + void ApplySettings(MediaTrackGraph* aGraph, + CubebUtils::AudioDeviceID aDeviceID, + const MediaEnginePrefs& aSettings); void End(); @@ -164,9 +157,18 @@ class AudioInputProcessing : public AudioDataListener { bool IsEnded() const { return mEnded; } + // For testing: + bool HadAECAndDrift() const { return mHadAECAndDrift; } + private: ~AudioInputProcessing() = default; - void EnsureAudioProcessing(MediaTrackGraph* aGraph, uint32_t aChannels); + webrtc::AudioProcessing::Config ConfigForPrefs( + const MediaEnginePrefs& aPrefs); + void PassThroughChanged(MediaTrackGraph* aGraph); + void RequestedInputChannelCountChanged(MediaTrackGraph* aGraph, + CubebUtils::AudioDeviceID aDeviceId); + void EnsurePacketizer(AudioProcessingTrack* aTrack); + void EnsureAudioProcessing(AudioProcessingTrack* aTrack); void ResetAudioProcessing(MediaTrackGraph* aGraph); PrincipalHandle GetCheckedPrincipal(const AudioSegment& aSegment); // This implements the processing algoritm to apply to the input (e.g. a @@ -174,17 +176,16 @@ class AudioInputProcessing : public AudioDataListener { // class only accepts audio chunks of 10ms. It has two inputs and one output: // it is fed the speaker data and the microphone data. It outputs processed // input data. - const UniquePtr<webrtc::AudioProcessing> mAudioProcessing; + UniquePtr<webrtc::AudioProcessing> mAudioProcessing; + // Whether mAudioProcessing was created for AEC with clock drift. + // Meaningful only when mAudioProcessing is non-null; + bool mHadAECAndDrift = false; // Packetizer to be able to feed 10ms packets to the input side of // mAudioProcessing. Not used if the processing is bypassed. Maybe<AudioPacketizer<AudioDataValue, float>> mPacketizerInput; - // The number of channels asked for by content, after clamping to the range of - // legal channel count for this particular device. - uint32_t mRequestedInputChannelCount; - // mSkipProcessing is true if none of the processing passes are enabled, - // because of prefs or constraints. This allows simply copying the audio into - // the MTG, skipping resampling and the whole webrtc.org code. - bool mSkipProcessing; + // The current settings from about:config preferences and content-provided + // constraints. + MediaEnginePrefs mSettings; // Buffer for up to one 10ms packet of planar mixed audio output for the // reverse-stream (speaker data) of mAudioProcessing AEC. // Length is packet size * channel count, regardless of how many frames are diff --git a/dom/media/webrtc/jsapi/PeerConnectionCtx.cpp b/dom/media/webrtc/jsapi/PeerConnectionCtx.cpp index d293fa0be6..b9b9ab8fc5 100644 --- a/dom/media/webrtc/jsapi/PeerConnectionCtx.cpp +++ b/dom/media/webrtc/jsapi/PeerConnectionCtx.cpp @@ -129,11 +129,12 @@ class DummyAudioProcessing : public AudioProcessing { } void set_stream_key_pressed(bool) override { MOZ_CRASH("Unexpected call"); } bool CreateAndAttachAecDump(absl::string_view, int64_t, - rtc::TaskQueue*) override { + absl::Nonnull<TaskQueueBase*>) override { MOZ_CRASH("Unexpected call"); return false; } - bool CreateAndAttachAecDump(FILE*, int64_t, rtc::TaskQueue*) override { + bool CreateAndAttachAecDump(FILE*, int64_t, + absl::Nonnull<TaskQueueBase*>) override { MOZ_CRASH("Unexpected call"); return false; } diff --git a/dom/media/webrtc/jsapi/RTCRtpScriptTransform.cpp b/dom/media/webrtc/jsapi/RTCRtpScriptTransform.cpp index 43f34c456f..8fa0bade00 100644 --- a/dom/media/webrtc/jsapi/RTCRtpScriptTransform.cpp +++ b/dom/media/webrtc/jsapi/RTCRtpScriptTransform.cpp @@ -47,6 +47,22 @@ already_AddRefed<RTCRtpScriptTransform> RTCRtpScriptTransform::Constructor( aRv.Throw(NS_ERROR_FAILURE); return nullptr; } + + // The spec currently fails to describe what to do when the worker is closing + // or closed; the following placeholder text can be found in the spec at: + // https://w3c.github.io/webrtc-encoded-transform/#dom-rtcrtpscripttransform-rtcrtpscripttransform + // + // > FIXME: Describe error handling (worker closing flag true at + // > RTCRtpScriptTransform creation time. And worker being terminated while + // > transform is processing data). + // + // Because our worker runnables do not like to be pointed at a nonexistant + // worker, we throw in this case. + if (!aWorker.IsEligibleForMessaging()) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + auto newTransform = MakeRefPtr<RTCRtpScriptTransform>(ownerWindow); RefPtr<RTCTransformEventRunnable> runnable = new RTCTransformEventRunnable(aWorker, &newTransform->GetProxy()); diff --git a/dom/media/webrtc/jsapi/RTCRtpScriptTransformer.cpp b/dom/media/webrtc/jsapi/RTCRtpScriptTransformer.cpp index 126020a94f..f2fbd6d637 100644 --- a/dom/media/webrtc/jsapi/RTCRtpScriptTransformer.cpp +++ b/dom/media/webrtc/jsapi/RTCRtpScriptTransformer.cpp @@ -148,7 +148,7 @@ WritableStreamRTCFrameSink::WritableStreamRTCFrameSink( WritableStreamRTCFrameSink::~WritableStreamRTCFrameSink() = default; -already_AddRefed<Promise> WritableStreamRTCFrameSink::WriteCallback( +already_AddRefed<Promise> WritableStreamRTCFrameSink::WriteCallbackImpl( JSContext* aCx, JS::Handle<JS::Value> aChunk, WritableStreamDefaultController& aController, ErrorResult& aError) { // Spec does not say to do this right now. Might be a spec bug, needs diff --git a/dom/media/webrtc/jsapi/RTCRtpScriptTransformer.h b/dom/media/webrtc/jsapi/RTCRtpScriptTransformer.h index 6d61ac3cd5..7a22612254 100644 --- a/dom/media/webrtc/jsapi/RTCRtpScriptTransformer.h +++ b/dom/media/webrtc/jsapi/RTCRtpScriptTransformer.h @@ -87,7 +87,7 @@ class WritableStreamRTCFrameSink final NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(WritableStreamRTCFrameSink, UnderlyingSinkAlgorithmsWrapper) - already_AddRefed<Promise> WriteCallback( + already_AddRefed<Promise> WriteCallbackImpl( JSContext* aCx, JS::Handle<JS::Value> aChunk, WritableStreamDefaultController& aController, ErrorResult& aError) override; diff --git a/dom/media/webrtc/jsapi/WebrtcGlobalInformation.cpp b/dom/media/webrtc/jsapi/WebrtcGlobalInformation.cpp index 4e4bf9ab93..eabf7ee335 100644 --- a/dom/media/webrtc/jsapi/WebrtcGlobalInformation.cpp +++ b/dom/media/webrtc/jsapi/WebrtcGlobalInformation.cpp @@ -234,7 +234,7 @@ void WebrtcGlobalInformation::GetStatsHistorySince( auto statsAfter = aAfter.WasPassed() ? Some(aAfter.Value()) : Nothing(); auto sdpAfter = aSdpAfter.WasPassed() ? Some(aSdpAfter.Value()) : Nothing(); - WebrtcGlobalStatsHistory::GetHistory(pcIdFilter).apply([&](auto& hist) { + WebrtcGlobalStatsHistory::GetHistory(pcIdFilter).apply([&](const auto& hist) { if (!history.mReports.AppendElements(hist->Since(statsAfter), fallible)) { mozalloc_handle_oom(0); } diff --git a/dom/media/webrtc/libwebrtcglue/AudioConduit.cpp b/dom/media/webrtc/libwebrtcglue/AudioConduit.cpp index 49f049cd21..91ad0d848c 100644 --- a/dom/media/webrtc/libwebrtcglue/AudioConduit.cpp +++ b/dom/media/webrtc/libwebrtcglue/AudioConduit.cpp @@ -907,7 +907,7 @@ RtpExtList WebrtcAudioConduit::FilterExtensions(LocalDirection aDirection, webrtc::SdpAudioFormat WebrtcAudioConduit::CodecConfigToLibwebrtcFormat( const AudioCodecConfig& aConfig) { - webrtc::SdpAudioFormat::Parameters parameters; + webrtc::CodecParameterMap parameters; if (aConfig.mName == kOpusCodecName) { if (aConfig.mChannels == 2) { parameters[kCodecParamStereo] = kParamValueTrue; diff --git a/dom/media/webrtc/libwebrtcglue/VideoConduit.cpp b/dom/media/webrtc/libwebrtcglue/VideoConduit.cpp index 73e59f5ee2..5862237711 100644 --- a/dom/media/webrtc/libwebrtcglue/VideoConduit.cpp +++ b/dom/media/webrtc/libwebrtcglue/VideoConduit.cpp @@ -190,7 +190,7 @@ webrtc::VideoCodecType SupportedCodecType(webrtc::VideoCodecType aType) { rtc::scoped_refptr<webrtc::VideoEncoderConfig::EncoderSpecificSettings> ConfigureVideoEncoderSettings(const VideoCodecConfig& aConfig, const WebrtcVideoConduit* aConduit, - webrtc::SdpVideoFormat::Parameters& aParameters) { + webrtc::CodecParameterMap& aParameters) { bool is_screencast = aConduit->CodecMode() == webrtc::VideoCodecMode::kScreensharing; // No automatic resizing when using simulcast or screencast. diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcCallWrapper.cpp b/dom/media/webrtc/libwebrtcglue/WebrtcCallWrapper.cpp index 824f1cf6eb..b03c1772c4 100644 --- a/dom/media/webrtc/libwebrtcglue/WebrtcCallWrapper.cpp +++ b/dom/media/webrtc/libwebrtcglue/WebrtcCallWrapper.cpp @@ -11,6 +11,8 @@ #include "TaskQueueWrapper.h" // libwebrtc includes +#include "api/environment/environment.h" +#include "api/environment/environment_factory.h" #include "call/rtp_transport_controller_send_factory.h" namespace mozilla { @@ -28,17 +30,16 @@ namespace mozilla { std::move(eventLog), std::move(taskQueueFactory), aTimestampMaker, std::move(aShutdownTicket)); + webrtc::Environment env = CreateEnvironment( + wrapper->mEventLog.get(), wrapper->mClock.GetRealTimeClockRaw(), + wrapper->mTaskQueueFactory.get(), aSharedState->mTrials.get()); + wrapper->mCallThread->Dispatch( - NS_NewRunnableFunction(__func__, [wrapper, aSharedState] { - webrtc::CallConfig config(wrapper->mEventLog.get()); + NS_NewRunnableFunction(__func__, [wrapper, aSharedState, env] { + webrtc::CallConfig config(env, nullptr); config.audio_state = webrtc::AudioState::Create(aSharedState->mAudioStateConfig); - config.task_queue_factory = wrapper->mTaskQueueFactory.get(); - config.trials = aSharedState->mTrials.get(); - wrapper->SetCall(WrapUnique(webrtc::Call::Create( - config, &wrapper->mClock, - webrtc::RtpTransportControllerSendFactory().Create( - config.ExtractTransportConfig(), &wrapper->mClock)).release())); + wrapper->SetCall(WrapUnique(webrtc::Call::Create(config).release())); })); return wrapper; diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcGmpVideoCodec.h b/dom/media/webrtc/libwebrtcglue/WebrtcGmpVideoCodec.h index 865f9afff0..b8ee44c6b3 100644 --- a/dom/media/webrtc/libwebrtcglue/WebrtcGmpVideoCodec.h +++ b/dom/media/webrtc/libwebrtcglue/WebrtcGmpVideoCodec.h @@ -289,7 +289,7 @@ class WebrtcGmpVideoEncoder : public GMPVideoEncoderCallbackProxy, GMPVideoHost* mHost; GMPVideoCodec mCodecParams; uint32_t mMaxPayloadSize; - const webrtc::SdpVideoFormat::Parameters mFormatParams; + const webrtc::CodecParameterMap mFormatParams; webrtc::CodecSpecificInfo mCodecSpecificInfo; webrtc::H264BitstreamParser mH264BitstreamParser; // Protects mCallback diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.cpp b/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.cpp index 844542cd0d..f5240ffa22 100644 --- a/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.cpp +++ b/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.cpp @@ -75,7 +75,7 @@ static const char* PacketModeStr(const webrtc::CodecSpecificInfo& aInfo) { } static std::pair<H264_PROFILE, H264_LEVEL> ConvertProfileLevel( - const webrtc::SdpVideoFormat::Parameters& aParameters) { + const webrtc::CodecParameterMap& aParameters) { const absl::optional<webrtc::H264ProfileLevelId> profileLevel = webrtc::ParseSdpForH264ProfileLevelId(aParameters); @@ -143,9 +143,9 @@ WebrtcMediaDataEncoder::~WebrtcMediaDataEncoder() { } } -static void InitCodecSpecficInfo( - webrtc::CodecSpecificInfo& aInfo, const webrtc::VideoCodec* aCodecSettings, - const webrtc::SdpVideoFormat::Parameters& aParameters) { +static void InitCodecSpecficInfo(webrtc::CodecSpecificInfo& aInfo, + const webrtc::VideoCodec* aCodecSettings, + const webrtc::CodecParameterMap& aParameters) { MOZ_ASSERT(aCodecSettings); aInfo.codecType = aCodecSettings->codecType; @@ -290,13 +290,11 @@ already_AddRefed<MediaDataEncoder> WebrtcMediaDataEncoder::CreateEncoder( MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Unsupported codec type"); } EncoderConfig config( - type, {aCodecSettings->width, aCodecSettings->height}, - MediaDataEncoder::Usage::Realtime, MediaDataEncoder::PixelFormat::YUV420P, - MediaDataEncoder::PixelFormat::YUV420P, aCodecSettings->maxFramerate, - keyframeInterval, mBitrateAdjuster.GetTargetBitrateBps(), - MediaDataEncoder::BitrateMode::Variable, - MediaDataEncoder::HardwarePreference::None, - MediaDataEncoder::ScalabilityMode::None, specific); + type, {aCodecSettings->width, aCodecSettings->height}, Usage::Realtime, + dom::ImageBitmapFormat::YUV420P, dom::ImageBitmapFormat::YUV420P, + aCodecSettings->maxFramerate, keyframeInterval, + mBitrateAdjuster.GetTargetBitrateBps(), BitrateMode::Variable, + HardwarePreference::None, ScalabilityMode::None, specific); return mFactory->CreateEncoder(config, mTaskQueue); } diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.h b/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.h index 9d750e85b2..0c2070f6a9 100644 --- a/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.h +++ b/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.h @@ -65,7 +65,7 @@ class WebrtcMediaDataEncoder : public RefCountedWebrtcVideoEncoder { MediaResult mError = NS_OK; VideoInfo mInfo; - webrtc::SdpVideoFormat::Parameters mFormatParams; + webrtc::CodecParameterMap mFormatParams; webrtc::CodecSpecificInfo mCodecSpecific; webrtc::BitrateAdjuster mBitrateAdjuster; uint32_t mMaxFrameRate = {0}; diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html index 96c2c42b78..1f3662d9fc 100644 --- a/dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html +++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html @@ -28,10 +28,11 @@ sdpMid: "test", sdpMLineIndex: 3 }); jsonCopy = JSON.parse(JSON.stringify(rtcIceCandidate)); - for (key in rtcIceCandidate) { - if (typeof(rtcIceCandidate[key]) == "function") continue; - is(rtcIceCandidate[key], jsonCopy[key], "key " + key + " should match."); - } + is(jsonCopy.candidate, "dummy"); + is(jsonCopy.sdpMid, "test"); + is(jsonCopy.sdpMLineIndex, 3); + is(jsonCopy.usernameFragment, rtcIceCandidate.usernameFragment); + is(Object.keys(jsonCopy).length, 4, "JSON limited to those four members."); }); </script> </pre> diff --git a/dom/media/webrtc/third_party_build/default_config_env b/dom/media/webrtc/third_party_build/default_config_env index 7013520a30..be3c5ba7c1 100644 --- a/dom/media/webrtc/third_party_build/default_config_env +++ b/dom/media/webrtc/third_party_build/default_config_env @@ -5,41 +5,41 @@ export MOZ_LIBWEBRTC_SRC=$STATE_DIR/moz-libwebrtc # The previous fast-forward bug number is used for some error messaging. -export MOZ_PRIOR_FASTFORWARD_BUG="1871981" +export MOZ_PRIOR_FASTFORWARD_BUG="1876843" # Fast-forwarding each Chromium version of libwebrtc should be done # under a separate bugzilla bug. This bug number is used when crafting # the commit summary as each upstream commit is vendored into the # mercurial repository. The bug used for the v106 fast-forward was # 1800920. -export MOZ_FASTFORWARD_BUG="1876843" +export MOZ_FASTFORWARD_BUG="1883116" # MOZ_NEXT_LIBWEBRTC_MILESTONE and MOZ_NEXT_FIREFOX_REL_TARGET are # not used during fast-forward processing, but facilitate generating this # default config. To generate an default config for the next update, run # bash dom/media/webrtc/third_party_build/update_default_config_env.sh -export MOZ_NEXT_LIBWEBRTC_MILESTONE=121 -export MOZ_NEXT_FIREFOX_REL_TARGET=125 +export MOZ_NEXT_LIBWEBRTC_MILESTONE=122 +export MOZ_NEXT_FIREFOX_REL_TARGET=126 # For Chromium release branches, see: # https://chromiumdash.appspot.com/branches -# Chromium's v120 release branch was 6099. This is used to pre-stack +# Chromium's v121 release branch was 6167. This is used to pre-stack # the previous release branch's commits onto the appropriate base commit # (the first common commit between trunk and the release branch). -export MOZ_PRIOR_UPSTREAM_BRANCH_HEAD_NUM="6099" +export MOZ_PRIOR_UPSTREAM_BRANCH_HEAD_NUM="6167" -# New target release branch for v121 is branch-heads/6167. This is used +# New target release branch for v122 is branch-heads/6261. This is used # to calculate the next upstream commit. -export MOZ_TARGET_UPSTREAM_BRANCH_HEAD="branch-heads/6167" +export MOZ_TARGET_UPSTREAM_BRANCH_HEAD="branch-heads/6261" # For local development 'mozpatches' is fine for a branch name, but when # pushing the patch stack to github, it should be named something like -# 'moz-mods-chr121-for-rel125'. +# 'moz-mods-chr122-for-rel126'. export MOZ_LIBWEBRTC_BRANCH="mozpatches" # After elm has been merged to mozilla-central, the patch stack in # moz-libwebrtc should be pushed to github. The script # push_official_branch.sh uses this branch name when pushing to the # public repo. -export MOZ_LIBWEBRTC_OFFICIAL_BRANCH="moz-mods-chr121-for-rel125" +export MOZ_LIBWEBRTC_OFFICIAL_BRANCH="moz-mods-chr122-for-rel126" diff --git a/dom/media/webrtc/third_party_build/elm_rebase.sh b/dom/media/webrtc/third_party_build/elm_rebase.sh index ba0028b7a4..0dbf93d3ce 100644 --- a/dom/media/webrtc/third_party_build/elm_rebase.sh +++ b/dom/media/webrtc/third_party_build/elm_rebase.sh @@ -153,6 +153,15 @@ export MOZ_BOOKMARK=$MOZ_BOOKMARK " > $STATE_DIR/rebase_resume_state fi # if [ -f $STATE_DIR/rebase_resume_state ]; then ; else +if [ "x$STOP_FOR_REORDER" = "x1" ]; then + echo "" + echo "Stopping after generating commit list ($COMMIT_LIST_FILE) to" + echo "allow tweaking commit ordering. Re-running $0 will resume the" + echo "rebase processing. To stop processing during the rebase," + echo "insert a line with only 'STOP'." + exit +fi + # grab all commits COMMITS=`cat $COMMIT_LIST_FILE | awk '{print $1;}'` @@ -171,6 +180,12 @@ for commit in $COMMITS; do ed -s $COMMIT_LIST_FILE <<< $'1d\nw\nq' } + if [ "$FULL_COMMIT_LINE" == "STOP" ]; then + echo "Stopping for history editing. Re-run $0 to resume." + remove_commit + exit + fi + IS_BUILD_COMMIT=`hg log -T '{desc|firstline}' -r $commit \ | grep "file updates" | wc -l | tr -d " " || true` echo "IS_BUILD_COMMIT: $IS_BUILD_COMMIT" diff --git a/dom/media/webrtc/third_party_build/fetch_github_repo.py b/dom/media/webrtc/third_party_build/fetch_github_repo.py index b9d10e0b6c..8caa55d5c5 100644 --- a/dom/media/webrtc/third_party_build/fetch_github_repo.py +++ b/dom/media/webrtc/third_party_build/fetch_github_repo.py @@ -87,6 +87,10 @@ def fetch_repo(github_path, clone_protocol, force_fetch, tar_path): else: print("Upstream remote branch-heads already configured") + # prevent changing line endings when moving things out of the git repo + # (and into hg for instance) + run_git("git config --local core.autocrlf false") + # do a sanity fetch in case this was not a freshly cloned copy of the # repo, meaning it may not have all the mozilla branches present. run_git("git fetch --all", github_path) diff --git a/dom/media/webrtc/third_party_build/vendor-libwebrtc.py b/dom/media/webrtc/third_party_build/vendor-libwebrtc.py index d820d8c006..1c44fbd749 100644 --- a/dom/media/webrtc/third_party_build/vendor-libwebrtc.py +++ b/dom/media/webrtc/third_party_build/vendor-libwebrtc.py @@ -27,7 +27,6 @@ def get_excluded_files(): ".clang-format", ".git-blame-ignore-revs", ".gitignore", - ".vpython", "CODE_OF_CONDUCT.md", "ENG_REVIEW_OWNERS", "PRESUBMIT.py", diff --git a/dom/media/webrtc/transport/test/ice_unittest.cpp b/dom/media/webrtc/transport/test/ice_unittest.cpp index 50febb3cdd..7df379e1c4 100644 --- a/dom/media/webrtc/transport/test/ice_unittest.cpp +++ b/dom/media/webrtc/transport/test/ice_unittest.cpp @@ -58,9 +58,9 @@ using namespace mozilla; static unsigned int kDefaultTimeout = 7000; -// TODO(nils@mozilla.com): This should get replaced with some non-external -// solution like discussed in bug 860775. -const std::string kDefaultStunServerHostname((char*)"stun.l.google.com"); +// TODO: It would be nice to have a test STUN/TURN server that can run with +// gtest. +const std::string kDefaultStunServerHostname((char*)""); const std::string kBogusStunServerHostname( (char*)"stun-server-nonexistent.invalid"); const uint16_t kDefaultStunServerPort = 19305; @@ -1628,12 +1628,17 @@ class WebRtcIceConnectTest : public StunTest { peer->SetMappingType(mapping_type_); peer->SetBlockUdp(block_udp_); } else if (setup_stun_servers) { - std::vector<NrIceStunServer> stun_servers; + if (stun_server_address_.empty()) { + InitTestStunServer(); + peer->UseTestStunServer(); + } else { + std::vector<NrIceStunServer> stun_servers; - stun_servers.push_back(*NrIceStunServer::Create( - stun_server_address_, kDefaultStunServerPort, kNrIceTransportUdp)); + stun_servers.push_back(*NrIceStunServer::Create( + stun_server_address_, kDefaultStunServerPort, kNrIceTransportUdp)); - peer->SetStunServers(stun_servers); + peer->SetStunServers(stun_servers); + } } } diff --git a/dom/media/webspeech/recognition/SpeechRecognition.cpp b/dom/media/webspeech/recognition/SpeechRecognition.cpp index 7239a88237..44ad5fdd61 100644 --- a/dom/media/webspeech/recognition/SpeechRecognition.cpp +++ b/dom/media/webspeech/recognition/SpeechRecognition.cpp @@ -136,7 +136,7 @@ CreateSpeechRecognitionService(nsPIDOMWindowInner* aWindow, NS_IMPL_CYCLE_COLLECTION_WEAK_PTR_INHERITED(SpeechRecognition, DOMEventTargetHelper, mStream, mTrack, mRecognitionService, - mSpeechGrammarList) + mSpeechGrammarList, mListener) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechRecognition) NS_INTERFACE_MAP_ENTRY(nsIObserver) @@ -145,6 +145,16 @@ NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) NS_IMPL_ADDREF_INHERITED(SpeechRecognition, DOMEventTargetHelper) NS_IMPL_RELEASE_INHERITED(SpeechRecognition, DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_INHERITED(SpeechRecognition::TrackListener, + DOMMediaStream::TrackListener, + mSpeechRecognition) +NS_IMPL_ADDREF_INHERITED(SpeechRecognition::TrackListener, + DOMMediaStream::TrackListener) +NS_IMPL_RELEASE_INHERITED(SpeechRecognition::TrackListener, + DOMMediaStream::TrackListener) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechRecognition::TrackListener) +NS_INTERFACE_MAP_END_INHERITING(DOMMediaStream::TrackListener) + SpeechRecognition::SpeechRecognition(nsPIDOMWindowInner* aOwnerWindow) : DOMEventTargetHelper(aOwnerWindow), mEndpointer(kSAMPLE_RATE), @@ -472,8 +482,9 @@ void SpeechRecognition::Reset() { ++mStreamGeneration; if (mStream) { - mStream->UnregisterTrackListener(this); + mStream->UnregisterTrackListener(mListener); mStream = nullptr; + mListener = nullptr; } mTrack = nullptr; mTrackIsOwned = false; @@ -642,7 +653,8 @@ RefPtr<GenericNonExclusivePromise> SpeechRecognition::StopRecording() { if (mStream) { // Ensure we don't start recording because a track became available // before we get reset. - mStream->UnregisterTrackListener(this); + mStream->UnregisterTrackListener(mListener); + mListener = nullptr; } return GenericNonExclusivePromise::CreateAndResolve(true, __func__); } @@ -801,10 +813,13 @@ void SpeechRecognition::Start(const Optional<NonNull<DOMMediaStream>>& aStream, MediaStreamConstraints constraints; constraints.mAudio.SetAsBoolean() = true; + MOZ_ASSERT(!mListener); + mListener = new TrackListener(this); + if (aStream.WasPassed()) { mStream = &aStream.Value(); mTrackIsOwned = false; - mStream->RegisterTrackListener(this); + mStream->RegisterTrackListener(mListener); nsTArray<RefPtr<AudioStreamTrack>> tracks; mStream->GetAudioTracks(tracks); for (const RefPtr<AudioStreamTrack>& track : tracks) { @@ -839,7 +854,7 @@ void SpeechRecognition::Start(const Optional<NonNull<DOMMediaStream>>& aStream, return; } mStream = std::move(aStream); - mStream->RegisterTrackListener(this); + mStream->RegisterTrackListener(mListener); for (const RefPtr<AudioStreamTrack>& track : tracks) { if (!track->Ended()) { NotifyTrackAdded(track); diff --git a/dom/media/webspeech/recognition/SpeechRecognition.h b/dom/media/webspeech/recognition/SpeechRecognition.h index 687f38041e..465cadc8cb 100644 --- a/dom/media/webspeech/recognition/SpeechRecognition.h +++ b/dom/media/webspeech/recognition/SpeechRecognition.h @@ -52,7 +52,6 @@ LogModule* GetSpeechRecognitionLog(); class SpeechRecognition final : public DOMEventTargetHelper, public nsIObserver, - public DOMMediaStream::TrackListener, public SupportsWeakPtr { public: explicit SpeechRecognition(nsPIDOMWindowInner* aOwnerWindow); @@ -133,7 +132,24 @@ class SpeechRecognition final : public DOMEventTargetHelper, EVENT_COUNT }; - void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override; + void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack); + + class TrackListener final : public DOMMediaStream::TrackListener { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TrackListener, + DOMMediaStream::TrackListener) + explicit TrackListener(SpeechRecognition* aSpeechRecognition) + : mSpeechRecognition(aSpeechRecognition) {} + void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override { + mSpeechRecognition->NotifyTrackAdded(aTrack); + } + + private: + virtual ~TrackListener() = default; + RefPtr<SpeechRecognition> mSpeechRecognition; + }; + // aMessage should be valid UTF-8, but invalid UTF-8 byte sequences are // replaced with the REPLACEMENT CHARACTER on conversion to UTF-16. void DispatchError(EventType aErrorType, @@ -266,6 +282,8 @@ class SpeechRecognition final : public DOMEventTargetHelper, // a conforming implementation. uint32_t mMaxAlternatives; + RefPtr<TrackListener> mListener; + void ProcessTestEventRequest(nsISupports* aSubject, const nsAString& aEventName); diff --git a/dom/media/webspeech/synth/nsISynthVoiceRegistry.idl b/dom/media/webspeech/synth/nsISynthVoiceRegistry.idl index 8192eff045..e5189e0bc1 100644 --- a/dom/media/webspeech/synth/nsISynthVoiceRegistry.idl +++ b/dom/media/webspeech/synth/nsISynthVoiceRegistry.idl @@ -55,9 +55,9 @@ interface nsISynthVoiceRegistry : nsISupports AString getVoice(in uint32_t aIndex); - bool isDefaultVoice(in AString aUri); + boolean isDefaultVoice(in AString aUri); - bool isLocalVoice(in AString aUri); + boolean isLocalVoice(in AString aUri); AString getVoiceLang(in AString aUri); diff --git a/dom/media/webvtt/TextTrack.cpp b/dom/media/webvtt/TextTrack.cpp index 7ee9ee63d2..7a5b398d2d 100644 --- a/dom/media/webvtt/TextTrack.cpp +++ b/dom/media/webvtt/TextTrack.cpp @@ -23,20 +23,6 @@ extern mozilla::LazyLogModule gTextTrackLog; namespace mozilla::dom { -static const char* ToStateStr(const TextTrackMode aMode) { - switch (aMode) { - case TextTrackMode::Disabled: - return "DISABLED"; - case TextTrackMode::Hidden: - return "HIDDEN"; - case TextTrackMode::Showing: - return "SHOWING"; - default: - MOZ_ASSERT_UNREACHABLE("Invalid state."); - } - return "Unknown"; -} - static const char* ToReadyStateStr(const TextTrackReadyState aState) { switch (aState) { case TextTrackReadyState::NotLoaded: @@ -53,24 +39,6 @@ static const char* ToReadyStateStr(const TextTrackReadyState aState) { return "Unknown"; } -static const char* ToTextTrackKindStr(const TextTrackKind aKind) { - switch (aKind) { - case TextTrackKind::Subtitles: - return "Subtitles"; - case TextTrackKind::Captions: - return "Captions"; - case TextTrackKind::Descriptions: - return "Descriptions"; - case TextTrackKind::Chapters: - return "Chapters"; - case TextTrackKind::Metadata: - return "Metadata"; - default: - MOZ_ASSERT_UNREACHABLE("Invalid kind."); - } - return "Unknown"; -} - NS_IMPL_CYCLE_COLLECTION_INHERITED(TextTrack, DOMEventTargetHelper, mCueList, mActiveCueList, mTextTrackList, mTrackElement) @@ -129,8 +97,8 @@ void TextTrack::SetMode(TextTrackMode aValue) { if (mMode == aValue) { return; } - WEBVTT_LOG("Set mode=%s for track kind %s", ToStateStr(aValue), - ToTextTrackKindStr(mKind)); + WEBVTT_LOG("Set mode=%s for track kind %s", GetEnumString(aValue).get(), + GetEnumString(mKind).get()); mMode = aValue; HTMLMediaElement* mediaElement = GetMediaElement(); diff --git a/dom/metrics.yaml b/dom/metrics.yaml index b75c384c4d..34b94d8db6 100644 --- a/dom/metrics.yaml +++ b/dom/metrics.yaml @@ -116,23 +116,6 @@ perf: send_in_pings: - pageload -bloburl: - resolve_stopped: - type: counter - description: > - Counts how many times we do not resolve a blob URL - because of different partition keys - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1843158 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1843158 - data_sensitivity: - - technical - notification_emails: - - amadan@mozilla.com - expires: 127 - telemetry_mirror: BLOBURL_RESOLVE_STOPPED - performance.pageload: load_time: type: timing_distribution diff --git a/dom/midi/midir_impl/src/lib.rs b/dom/midi/midir_impl/src/lib.rs index de47fbeb11..38cc3e0033 100644 --- a/dom/midi/midir_impl/src/lib.rs +++ b/dom/midi/midir_impl/src/lib.rs @@ -5,7 +5,6 @@ use midir::{ MidiOutputPort, }; use nsstring::{nsAString, nsString}; -use std::boxed::Box; use std::ptr; use thin_vec::ThinVec; use uuid::Uuid; diff --git a/dom/network/TCPSocket.cpp b/dom/network/TCPSocket.cpp index f7ac7d3f9b..577e630952 100644 --- a/dom/network/TCPSocket.cpp +++ b/dom/network/TCPSocket.cpp @@ -49,9 +49,6 @@ #include "secerr.h" #include "sslerr.h" -#define BUFFER_SIZE 65536 -#define NETWORK_STATS_THRESHOLD 65536 - using namespace mozilla::dom; NS_IMPL_CYCLE_COLLECTION(LegacyMozTCPSocket, mGlobal) diff --git a/dom/notification/NotificationStorage.sys.mjs b/dom/notification/NotificationStorage.sys.mjs index 46c9e2485c..e33277b598 100644 --- a/dom/notification/NotificationStorage.sys.mjs +++ b/dom/notification/NotificationStorage.sys.mjs @@ -2,10 +2,14 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -const DEBUG = false; -function debug(s) { - dump("-*- NotificationStorage.js: " + s + "\n"); -} +const lazy = {}; + +ChromeUtils.defineLazyGetter(lazy, "console", () => { + return console.createInstance({ + prefix: "NotificationStorage", + maxLogLevelPref: "dom.webnotifications.loglevel", + }); +}); const kMessageNotificationGetAllOk = "Notification:GetAll:Return:OK"; const kMessageNotificationGetAllKo = "Notification:GetAll:Return:KO"; @@ -19,38 +23,36 @@ const kMessages = [ kMessageNotificationDeleteKo, ]; -export function NotificationStorage() { - this._requests = {}; - this._requestCount = 0; +export class NotificationStorage { + #requests = {}; + #requestCount = 0; - Services.obs.addObserver(this, "xpcom-shutdown"); + constructor() { + Services.obs.addObserver(this, "xpcom-shutdown"); - // Register for message listeners. - this.registerListeners(); -} + // Register for message listeners. + this.registerListeners(); + } -NotificationStorage.prototype = { registerListeners() { for (let message of kMessages) { Services.cpmm.addMessageListener(message, this); } - }, + } unregisterListeners() { for (let message of kMessages) { Services.cpmm.removeMessageListener(message, this); } - }, + } observe(aSubject, aTopic) { - if (DEBUG) { - debug("Topic: " + aTopic); - } + lazy.console.debug(`Topic: ${aTopic}`); if (aTopic === "xpcom-shutdown") { Services.obs.removeObserver(this, "xpcom-shutdown"); this.unregisterListeners(); } - }, + } put( origin, @@ -66,9 +68,7 @@ NotificationStorage.prototype = { behavior, serviceWorkerRegistrationScope ) { - if (DEBUG) { - debug("PUT: " + origin + " " + id + ": " + title); - } + lazy.console.debug(`PUT: ${origin} ${id}: ${title}`); var notification = { id, title, @@ -89,83 +89,74 @@ NotificationStorage.prototype = { origin, notification, }); - }, + } get(origin, tag, callback) { - if (DEBUG) { - debug("GET: " + origin + " " + tag); - } - this._fetchFromDB(origin, tag, callback); - }, + lazy.console.debug(`GET: ${origin} ${tag}`); + this.#fetchFromDB(origin, tag, callback); + } delete(origin, id) { - if (DEBUG) { - debug("DELETE: " + id); - } + lazy.console.debug(`DELETE: ${id}`); Services.cpmm.sendAsyncMessage("Notification:Delete", { origin, id, }); - }, + } receiveMessage(message) { - var request = this._requests[message.data.requestID]; + var request = this.#requests[message.data.requestID]; switch (message.name) { case kMessageNotificationGetAllOk: - delete this._requests[message.data.requestID]; - this._returnNotifications( - message.data.notifications, - request.origin, - request.tag, - request.callback - ); + delete this.#requests[message.data.requestID]; + this.#returnNotifications(message.data.notifications, request.callback); break; case kMessageNotificationGetAllKo: - delete this._requests[message.data.requestID]; + delete this.#requests[message.data.requestID]; try { request.callback.done(); } catch (e) { - debug("Error calling callback done: " + e); + lazy.console.debug(`Error calling callback done: ${e}`); } break; case kMessageNotificationSaveKo: case kMessageNotificationDeleteKo: - if (DEBUG) { - debug( - "Error received when treating: '" + - message.name + - "': " + - message.data.errorMsg - ); - } + lazy.console.debug( + `Error received when treating: '${message.name}': ${message.data.errorMsg}` + ); break; default: - if (DEBUG) { - debug("Unrecognized message: " + message.name); - } + lazy.console.debug(`Unrecognized message: ${message.name}`); break; } - }, + } + + #getUniqueRequestID() { + // This assumes the count will never go above MAX_SAFE_INTEGER, as + // notifications are not supposed to happen that frequently. + this.#requestCount += 1; + return this.#requestCount; + } - _fetchFromDB(origin, tag, callback) { + #fetchFromDB(origin, tag, callback) { var request = { origin, tag, callback, }; - var requestID = this._requestCount++; - this._requests[requestID] = request; + var requestID = this.#getUniqueRequestID(); + this.#requests[requestID] = request; Services.cpmm.sendAsyncMessage("Notification:GetAll", { origin, tag, requestID, }); - }, + } - _returnNotifications(notifications, origin, tag, callback) { + #returnNotifications(notifications, callback) { // Pass each notification back separately. // The callback is called asynchronously to match the behaviour when // fetching from the database. @@ -187,19 +178,15 @@ NotificationStorage.prototype = { ) ); } catch (e) { - if (DEBUG) { - debug("Error calling callback handle: " + e); - } + lazy.console.debug(`Error calling callback handle: ${e}`); } }); try { Services.tm.dispatchToMainThread(callback.done); } catch (e) { - if (DEBUG) { - debug("Error calling callback done: " + e); - } + lazy.console.debug(`Error calling callback done: ${e}`); } - }, + } - QueryInterface: ChromeUtils.generateQI(["nsINotificationStorage"]), -}; + QueryInterface = ChromeUtils.generateQI(["nsINotificationStorage"]); +} diff --git a/dom/notification/old/NotificationDB.sys.mjs b/dom/notification/old/NotificationDB.sys.mjs index 7de9734450..8cd2bff8f6 100644 --- a/dom/notification/old/NotificationDB.sys.mjs +++ b/dom/notification/old/NotificationDB.sys.mjs @@ -2,17 +2,19 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -const DEBUG = false; -function debug(s) { - dump("-*- NotificationDB component: " + s + "\n"); -} - const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", }); +ChromeUtils.defineLazyGetter(lazy, "console", () => { + return console.createInstance({ + prefix: "NotificationDB", + maxLogLevelPref: "dom.webnotifications.loglevel", + }); +}); + const NOTIFICATION_STORE_DIR = PathUtils.profileDir; const NOTIFICATION_STORE_PATH = PathUtils.join( NOTIFICATION_STORE_DIR, @@ -70,9 +72,7 @@ var NotificationDB = { }, observe(aSubject, aTopic) { - if (DEBUG) { - debug("Topic: " + aTopic); - } + lazy.console.debug(`Topic: ${aTopic}`); if (aTopic == "xpcom-shutdown") { this._shutdownInProgress = true; Services.obs.removeObserver(this, "xpcom-shutdown"); @@ -92,11 +92,9 @@ var NotificationDB = { } } if (persistentNotificationCount == 0) { - if (DEBUG) { - debug( - "Origin " + origin + " is not linked to an app manifest, deleting." - ); - } + lazy.console.debug( + `Origin ${origin} is not linked to an app manifest, deleting.` + ); delete result[origin]; } } @@ -171,9 +169,7 @@ var NotificationDB = { }, receiveMessage(message) { - if (DEBUG) { - debug("Received message:" + message.name); - } + lazy.console.debug(`Received message: ${message.name}`); // sendAsyncMessage can fail if the child process exits during a // notification storage operation, so always wrap it in a try/catch. @@ -181,9 +177,7 @@ var NotificationDB = { try { message.target.sendAsyncMessage(name, data); } catch (e) { - if (DEBUG) { - debug("Return message failed, " + name); - } + lazy.console.debug(`Return message failed, ${name}`); } } @@ -237,18 +231,14 @@ var NotificationDB = { break; default: - if (DEBUG) { - debug("Invalid message name" + message.name); - } + lazy.console.debug(`Invalid message name ${message.name}`); } }, // We need to make sure any read/write operations are atomic, // so use a queue to run each operation sequentially. queueTask(operation, data) { - if (DEBUG) { - debug("Queueing task: " + operation); - } + lazy.console.debug(`Queueing task: ${operation}`); var defer = {}; @@ -265,9 +255,7 @@ var NotificationDB = { // Only run immediately if we aren't currently running another task. if (!this.runningTask) { - if (DEBUG) { - debug("Task queue was not running, starting now..."); - } + lazy.console.debug("Task queue was not running, starting now..."); this.runNextTask(); this._queueDrainedPromise = new Promise(resolve => { this._queueDrainedPromiseResolve = resolve; @@ -279,14 +267,12 @@ var NotificationDB = { runNextTask() { if (this.tasks.length === 0) { - if (DEBUG) { - debug("No more tasks to run, queue depleted"); - } + lazy.console.debug("No more tasks to run, queue depleted"); this.runningTask = null; if (this._queueDrainedPromiseResolve) { this._queueDrainedPromiseResolve(); - } else if (DEBUG) { - debug( + } else { + lazy.console.debug( "_queueDrainedPromiseResolve was null somehow, no promise to resolve" ); } @@ -316,17 +302,13 @@ var NotificationDB = { } }) .then(payload => { - if (DEBUG) { - debug("Finishing task: " + this.runningTask.operation); - } + lazy.console.debug(`Finishing task: ${this.runningTask.operation}`); this.runningTask.defer.resolve(payload); }) .catch(err => { - if (DEBUG) { - debug( - "Error while running " + this.runningTask.operation + ": " + err - ); - } + lazy.console.debug( + `Error while running ${this.runningTask.operation}: ${err}` + ); this.runningTask.defer.reject(err); }) .then(() => { @@ -335,9 +317,7 @@ var NotificationDB = { }, taskGetAll(data) { - if (DEBUG) { - debug("Task, getting all"); - } + lazy.console.debug("Task, getting all"); var origin = data.origin; var notifications = []; // Grab only the notifications for specified origin. @@ -357,9 +337,7 @@ var NotificationDB = { }, taskSave(data) { - if (DEBUG) { - debug("Task, saving"); - } + lazy.console.debug("Task, saving"); var origin = data.origin; var notification = data.notification; if (!this.notifications[origin]) { @@ -382,24 +360,18 @@ var NotificationDB = { }, taskDelete(data) { - if (DEBUG) { - debug("Task, deleting"); - } + lazy.console.debug("Task, deleting"); var origin = data.origin; var id = data.id; if (!this.notifications[origin]) { - if (DEBUG) { - debug("No notifications found for origin: " + origin); - } + lazy.console.debug(`No notifications found for origin: ${origin}`); return Promise.resolve(); } // Make sure we can find the notification to delete. var oldNotification = this.notifications[origin][id]; if (!oldNotification) { - if (DEBUG) { - debug("No notification found with id: " + id); - } + lazy.console.debug(`No notification found with id: ${id}`); return Promise.resolve(); } diff --git a/dom/payments/test/mochitest.toml b/dom/payments/test/mochitest.toml index e6033dcf2b..f24ab35db5 100644 --- a/dom/payments/test/mochitest.toml +++ b/dom/payments/test/mochitest.toml @@ -51,7 +51,7 @@ skip-if = ["debug"] # Bug 1507251 - Leak skip-if = [ "os == 'linux'", "os == 'mac'", - "os == 'win' && os_version == '10.0'", # Bug 1514425 + "os == 'win' && os_version == '10.2009'", # Bug 1514425 ] ["test_currency_amount_validation.html"] diff --git a/dom/power/WakeLockJS.cpp b/dom/power/WakeLockJS.cpp index 50e4a716c0..3aa748fbe6 100644 --- a/dom/power/WakeLockJS.cpp +++ b/dom/power/WakeLockJS.cpp @@ -127,8 +127,7 @@ NS_IMPL_CYCLE_COLLECTING_RELEASE(WakeLockJS) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WakeLockJS) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY - NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener) - NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_END @@ -148,14 +147,12 @@ JSObject* WakeLockJS::WrapObject(JSContext* aCx, // https://w3c.github.io/screen-wake-lock/#the-request-method Step 7.3 Result<already_AddRefed<WakeLockSentinel>, WakeLockJS::RequestError> -WakeLockJS::Obtain(WakeLockType aType) { +WakeLockJS::Obtain(WakeLockType aType, Document* aDoc) { // Step 7.3.1. check visibility again - nsCOMPtr<Document> doc = mWindow->GetExtantDoc(); - if (!doc) { - return Err(RequestError::InternalFailure); - } - if (doc->Hidden()) { - return Err(RequestError::DocHidden); + // Out of spec, but also check everything else + RequestError rv = WakeLockAllowedForDocument(aDoc); + if (rv != RequestError::Success) { + return Err(rv); } // Step 7.3.3. let lock be a new WakeLockSentinel RefPtr<WakeLockSentinel> lock = @@ -166,7 +163,7 @@ WakeLockJS::Obtain(WakeLockType aType) { } // Steps 7.3.4. append lock to locks - doc->ActiveWakeLocks(aType).Insert(lock); + aDoc->ActiveWakeLocks(aType).Insert(lock); return lock.forget(); } @@ -191,8 +188,9 @@ already_AddRefed<Promise> WakeLockJS::Request(WakeLockType aType, // For now, we don't check the permission as we always grant the lock // Step 7.3. Queue a task NS_DispatchToMainThread(NS_NewRunnableFunction( - "ObtainWakeLock", [aType, promise, self = RefPtr<WakeLockJS>(this)]() { - auto lockOrErr = self->Obtain(aType); + "ObtainWakeLock", + [aType, promise, doc, self = RefPtr<WakeLockJS>(this)]() { + auto lockOrErr = self->Obtain(aType, doc); if (lockOrErr.isOk()) { RefPtr<WakeLockSentinel> lock = lockOrErr.unwrap(); promise->MaybeResolve(lock); @@ -208,30 +206,16 @@ already_AddRefed<Promise> WakeLockJS::Request(WakeLockType aType, } void WakeLockJS::AttachListeners() { - nsCOMPtr<Document> doc = mWindow->GetExtantDoc(); - MOZ_ASSERT(doc); - DebugOnly<nsresult> rv = - doc->AddSystemEventListener(u"visibilitychange"_ns, this, true, false); - MOZ_ASSERT(NS_SUCCEEDED(rv)); - doc->RegisterActivityObserver(ToSupports(this)); - hal::RegisterBatteryObserver(this); nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID); MOZ_ASSERT(prefBranch); - rv = prefBranch->AddObserver("dom.screenwakelock.enabled", this, true); + DebugOnly<nsresult> rv = + prefBranch->AddObserver("dom.screenwakelock.enabled", this, true); MOZ_ASSERT(NS_SUCCEEDED(rv)); } void WakeLockJS::DetachListeners() { - if (mWindow) { - if (nsCOMPtr<Document> doc = mWindow->GetExtantDoc()) { - doc->RemoveSystemEventListener(u"visibilitychange"_ns, this, true); - - doc->UnregisterActivityObserver(ToSupports(this)); - } - } - hal::UnregisterBatteryObserver(this); if (nsCOMPtr<nsIPrefBranch> prefBranch = @@ -252,30 +236,6 @@ NS_IMETHODIMP WakeLockJS::Observe(nsISupports* aSubject, const char* aTopic, return NS_OK; } -void WakeLockJS::NotifyOwnerDocumentActivityChanged() { - nsCOMPtr<Document> doc = mWindow->GetExtantDoc(); - MOZ_ASSERT(doc); - if (!doc->IsActive()) { - doc->UnlockAllWakeLocks(WakeLockType::Screen); - } -} - -NS_IMETHODIMP WakeLockJS::HandleEvent(Event* aEvent) { - nsAutoString type; - aEvent->GetType(type); - - if (type.EqualsLiteral("visibilitychange")) { - nsCOMPtr<Document> doc = do_QueryInterface(aEvent->GetTarget()); - NS_ENSURE_STATE(doc); - - if (doc->Hidden()) { - doc->UnlockAllWakeLocks(WakeLockType::Screen); - } - } - - return NS_OK; -} - void WakeLockJS::Notify(const hal::BatteryInformation& aBatteryInfo) { if (aBatteryInfo.level() > MIN_BATTERY_LEVEL || aBatteryInfo.charging()) { return; diff --git a/dom/power/WakeLockJS.h b/dom/power/WakeLockJS.h index c6858fdd22..43b11a6dc9 100644 --- a/dom/power/WakeLockJS.h +++ b/dom/power/WakeLockJS.h @@ -39,20 +39,15 @@ namespace mozilla::dom { * * https://www.w3.org/TR/screen-wake-lock/#the-wakelock-interface */ -class WakeLockJS final : public nsIDOMEventListener, +class WakeLockJS final : public nsIObserver, public nsWrapperCache, public hal::BatteryObserver, - public nsIDocumentActivity, - public nsIObserver, public nsSupportsWeakReference { public: - NS_DECL_NSIDOMEVENTLISTENER - NS_DECL_NSIDOCUMENTACTIVITY NS_DECL_NSIOBSERVER NS_DECL_CYCLE_COLLECTING_ISUPPORTS - NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(WakeLockJS, - nsIDOMEventListener) + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(WakeLockJS, nsIObserver) public: explicit WakeLockJS(nsPIDOMWindowInner* aWindow); @@ -89,7 +84,7 @@ class WakeLockJS final : public nsIDOMEventListener, void DetachListeners(); Result<already_AddRefed<WakeLockSentinel>, RequestError> Obtain( - WakeLockType aType); + WakeLockType aType, Document* aDoc); RefPtr<nsPIDOMWindowInner> mWindow; }; diff --git a/dom/power/tests/file_empty.html b/dom/power/tests/file_empty.html new file mode 100644 index 0000000000..4f54ef40a6 --- /dev/null +++ b/dom/power/tests/file_empty.html @@ -0,0 +1 @@ +<h1>Example page</h1> diff --git a/dom/power/tests/mochitest.toml b/dom/power/tests/mochitest.toml index 0b57119a8a..47e10ee3cd 100644 --- a/dom/power/tests/mochitest.toml +++ b/dom/power/tests/mochitest.toml @@ -1,9 +1,14 @@ [DEFAULT] prefs = ["dom.screenwakelock.enabled=true"] scheme = "https" +support-files = [ + "file_empty.html", +] ["test_dynamic_pref_change.html"] fail-if = ["xorigin"] # cross-origin use requires permissions policy ["test_wakelock_default_permission.html"] fail-if = ["xorigin"] # cross-origin use requires permissions policy + +["test_wakelock_on_initial_about_blank.html"] diff --git a/dom/power/tests/test_wakelock_on_initial_about_blank.html b/dom/power/tests/test_wakelock_on_initial_about_blank.html new file mode 100644 index 0000000000..eefa6c6fb7 --- /dev/null +++ b/dom/power/tests/test_wakelock_on_initial_about_blank.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test requesting lock on the initial about:blank</title> + <link rel="help" href="https://bugzilla.mozilla.org/1882344"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + + add_task(async function test() { + const iframe = document.createElement('iframe'); + iframe.src = "file_empty.html"; + document.documentElement.appendChild(iframe); + + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-window-object:initialise-the-document-object + is(iframe.contentWindow.location.href, "about:blank", "Initial about:blank is loaded"); + + let lock; + try { + // request lock on initial about:blank + lock = await iframe.contentWindow.navigator.wakeLock.request(); + } catch (err) { + // This could happen if the initial about:blank document is inactive + // once the async code in .request runs. + ok(true, "request was rejected"); + return; + } + + if (iframe.contentWindow.location.href == "about:blank") { + await new Promise((res, _rej) => { iframe.onload = res; }); + } + isnot(iframe.contentWindow.location.href, "about:blank", "Target document is loaded"); + + is(lock.released, true, "lock was released by load of target doc"); + + // window and wakeLock object stayed the same, but document changed + await iframe.contentWindow.navigator.wakeLock.request(); + ok(true, "Was able to request lock on target document"); + }); + + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/push/moz.build b/dom/push/moz.build index 0065fd21f6..39da18de23 100644 --- a/dom/push/moz.build +++ b/dom/push/moz.build @@ -4,7 +4,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. with Files("**"): - BUG_COMPONENT = ("Core", "DOM: Notifications") + BUG_COMPONENT = ("Core", "DOM: Push Subscriptions") EXTRA_COMPONENTS += [ "Push.manifest", diff --git a/dom/push/test/test_utils.js b/dom/push/test/test_utils.js index 5ecda2bd96..eb89cd1b72 100644 --- a/dom/push/test/test_utils.js +++ b/dom/push/test/test_utils.js @@ -4,9 +4,9 @@ const url = SimpleTest.getTestFileURL("mockpushserviceparent.js"); const chromeScript = SpecialPowers.loadChromeScript(url); /** - * Replaces `PushService.jsm` with a mock implementation that handles requests + * Replaces `PushService.sys.mjs` with a mock implementation that handles requests * from the DOM API. This allows tests to simulate local errors and error - * reporting, bypassing the `PushService.jsm` machinery. + * reporting, bypassing the `PushService.sys.mjs` machinery. */ async function replacePushService(mockService) { chromeScript.addMessageListener("service-delivery-error", function (msg) { @@ -58,7 +58,7 @@ let currentMockSocket = null; /** * Sets up a mock connection for the WebSocket backend. This only replaces - * the transport layer; `PushService.jsm` still handles DOM API requests, + * the transport layer; `PushService.sys.mjs` still handles DOM API requests, * observes permission changes, writes to IndexedDB, and notifies service * workers of incoming push messages. */ diff --git a/dom/quota/ActorsParent.cpp b/dom/quota/ActorsParent.cpp index 451d55a450..dc6b49055f 100644 --- a/dom/quota/ActorsParent.cpp +++ b/dom/quota/ActorsParent.cpp @@ -1555,6 +1555,14 @@ QuotaManager::Observer::Observe(nsISupports* aSubject, const char* aTopic, return rv; } +#ifdef XP_WIN + // Annotate if our profile lives on a network resource. + bool isNetworkPath = PathIsNetworkPathW(gBasePath->get()); + CrashReporter::RecordAnnotationBool( + CrashReporter::Annotation::QuotaManagerStorageIsNetworkResource, + isNetworkPath); +#endif + gStorageName = new nsString(); rv = Preferences::GetString("dom.quotaManager.storageName", *gStorageName); @@ -2380,13 +2388,25 @@ void QuotaManager::Shutdown() { ScopedLogExtraInfo scope{ScopedLogExtraInfo::kTagContextTainted, "dom::quota::QuotaManager::Shutdown"_ns}; + // We always need to ensure that firefox does not shutdown with a private + // repository still on disk. They are ideally cleaned up on PBM session end + // but, in some cases like PBM autostart (i.e. + // browser.privatebrowsing.autostart), private repository could only be + // cleaned up on shutdown. ClearPrivateRepository below runs a async op and is + // better to do it before we run the ShutdownStorageOp since it expects all + // cleanup operations to be done by that point. We don't need to use the + // returned promise here because `ClearPrivateRepository` registers the + // underlying `ClearPrivateRepositoryOp` in `gNormalOriginOps`. + ClearPrivateRepository(); + // This must be called before `flagShutdownStarted`, it would fail otherwise. // `ShutdownStorageOp` needs to acquire an exclusive directory lock over // entire <profile>/storage which will abort any existing operations and wait // for all existing directory locks to be released. So the shutdown operation // will effectively run after all existing operations. - // We don't need to use the returned promise here because `ShutdownStorage` - // registers `ShudownStorageOp` in `gNormalOriginOps`. + // Similar, to ClearPrivateRepository operation above, ShutdownStorageOp also + // registers it's operation in `gNormalOriginOps` so we don't need to assign + // returned promise. ShutdownStorage(); flagShutdownStarted(); @@ -3206,6 +3226,15 @@ Result<nsCOMPtr<nsIFile>, nsresult> QuotaManager::GetOriginDirectory( return directory; } +Result<bool, nsresult> QuotaManager::DoesOriginDirectoryExist( + const OriginMetadata& aOriginMetadata) const { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& directory, GetOriginDirectory(aOriginMetadata)); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists)); +} + // static nsresult QuotaManager::CreateDirectoryMetadata( nsIFile& aDirectory, int64_t aTimestamp, @@ -3516,6 +3545,18 @@ Result<Ok, nsresult> QuotaManager::RemoveOriginDirectory(nsIFile& aDirectory) { toBeRemovedStorageDir, NSID_TrimBracketsUTF16(nsID::GenerateUUID())))); } +Result<bool, nsresult> QuotaManager::DoesClientDirectoryExist( + const ClientMetadata& aClientMetadata) const { + AssertIsOnIOThread(); + + QM_TRY_INSPECT(const auto& directory, GetOriginDirectory(aClientMetadata)); + + QM_TRY(MOZ_TO_RESULT( + directory->Append(Client::TypeToString(aClientMetadata.mClientType)))); + + QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists)); +} + template <typename OriginFunc> nsresult QuotaManager::InitializeRepository(PersistenceType aPersistenceType, OriginFunc&& aOriginFunc) { @@ -7638,11 +7679,10 @@ Result<bool, nsresult> UpgradeStorageFrom1_0To2_0Helper::MaybeRemoveAppsData( if (!URLParams::Parse( Substring(originalSuffix, 1, originalSuffix.Length() - 1), true, - [](const nsAString& aName, const nsAString& aValue) { + [](const nsACString& aName, const nsACString& aValue) { if (aName.EqualsLiteral("appId")) { return false; } - return true; })) { QM_TRY(MOZ_TO_RESULT(RemoveObsoleteOrigin(aOriginProps))); diff --git a/dom/quota/QuotaManager.h b/dom/quota/QuotaManager.h index 354977166a..25c069810d 100644 --- a/dom/quota/QuotaManager.h +++ b/dom/quota/QuotaManager.h @@ -243,6 +243,9 @@ class QuotaManager final : public BackgroundThreadObject { Result<nsCOMPtr<nsIFile>, nsresult> GetOriginDirectory( const OriginMetadata& aOriginMetadata) const; + Result<bool, nsresult> DoesOriginDirectoryExist( + const OriginMetadata& aOriginMetadata) const; + static nsresult CreateDirectoryMetadata( nsIFile& aDirectory, int64_t aTimestamp, const OriginMetadata& aOriginMetadata); @@ -265,6 +268,9 @@ class QuotaManager final : public BackgroundThreadObject { Result<Ok, nsresult> RemoveOriginDirectory(nsIFile& aDirectory); + Result<bool, nsresult> DoesClientDirectoryExist( + const ClientMetadata& aClientMetadata) const; + RefPtr<UniversalDirectoryLockPromise> OpenStorageDirectory( const Nullable<PersistenceType>& aPersistenceType, const OriginScope& aOriginScope, diff --git a/dom/quota/StorageOriginAttributes.cpp b/dom/quota/StorageOriginAttributes.cpp index bcdf47bce8..b5fd68407a 100644 --- a/dom/quota/StorageOriginAttributes.cpp +++ b/dom/quota/StorageOriginAttributes.cpp @@ -17,18 +17,17 @@ void StorageOriginAttributes::CreateSuffix(nsACString& aStr) const { nsCString str1; URLParams params; - nsAutoString value; + nsAutoCString value; if (mInIsolatedMozBrowser) { - params.Set(u"inBrowser"_ns, u"1"_ns); + params.Set("inBrowser"_ns, "1"_ns); } str1.Truncate(); - params.Serialize(value, true); if (!value.IsEmpty()) { str1.AppendLiteral("^"); - str1.Append(NS_ConvertUTF16toUTF8(value)); + str1.Append(value); } // Make sure that the string don't contain characters that would get replaced @@ -67,22 +66,22 @@ bool StorageOriginAttributes::PopulateFromSuffix(const nsACString& aStr) { return false; } - bool ok = - URLParams::Parse(Substring(aStr, 1, aStr.Length() - 1), true, - [this](const nsAString& aName, const nsAString& aValue) { - if (aName.EqualsLiteral("inBrowser")) { - if (!aValue.EqualsLiteral("1")) { - return false; - } - - mInIsolatedMozBrowser = true; - return true; - } - - // Let OriginAttributes::PopulateFromSuffix parse other - // origin attributes. - return true; - }); + bool ok = URLParams::Parse( + Substring(aStr, 1, aStr.Length() - 1), true, + [this](const nsACString& aName, const nsACString& aValue) { + if (aName.EqualsLiteral("inBrowser")) { + if (!aValue.EqualsLiteral("1")) { + return false; + } + + mInIsolatedMozBrowser = true; + return true; + } + + // Let OriginAttributes::PopulateFromSuffix parse other + // origin attributes. + return true; + }); if (!ok) { return false; } diff --git a/dom/quota/scripts/analyze_qm_failures.py b/dom/quota/scripts/analyze_qm_failures.py deleted file mode 100755 index f4afd64ab4..0000000000 --- a/dom/quota/scripts/analyze_qm_failures.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -import getopt -import sys - -import fn_anchors -import stackanalysis -import utils - -""" -The analysis is based on stack frames of the following form: - -[ - { - "event_timeabs": 1617121013137, - "session_startabs": 1617120840000, - "build_id": "20210329095128", - "client_id": "0013a68f-9893-461a-93d4-2d7a2f85583f", - "session_id": "8cd37159-bd5c-481c-99ad-9eace9ea726a", - "seq": 1, - "context": "Initialization::TemporaryStorage", - "source_file": "dom/localstorage/ActorsParent.cpp", - "source_line": "1018", - "severity": "ERROR", - "result": "NS_ERROR_FILE_NOT_FOUND" - }, -... -] - -The location of the input file is expected to be found in the -last item of the list inside qmexecutions.json. -""" - - -def usage(): - print("analyze_qm_faiures.py -w <workdir=.>") - print("") - print("Analyzes the results from fetch_qm_failures.py's JSON file.") - print( - "Writes out several JSON results as files and a bugzilla markup table on stdout." - ) - print("-w <workdir>: Working directory, default is '.'") - sys.exit(2) - - -days = 1 -workdir = "." - -try: - opts, args = getopt.getopt(sys.argv[1:], "w:", ["workdir="]) - for opt, arg in opts: - if opt == "-w": - workdir = arg -except getopt.GetoptError: - usage() - -run = utils.getLastRunFromExecutionFile(workdir) -if "numrows" not in run: - print("No previous execution from fetch_qm_failures.py found.") - usage() -if run["numrows"] == 0: - print("The last execution yielded no result.") - -infile = run["rawfile"] - - -def getFname(prefix): - return "{}/{}_until_{}.json".format(workdir, prefix, run["lasteventtime"]) - - -# read rows from JSON -rows = utils.readJSONFile(getFname("qmrows")) -print("Found {} rows of data.".format(len(rows))) -rows = stackanalysis.sanitize(rows) - -# enrich rows with hg locations -buildids = stackanalysis.extractBuildIDs(rows) -utils.fetchBuildRevisions(buildids) -stackanalysis.constructHGLinks(buildids, rows) - -# transform rows to unique stacks -raw_stacks = stackanalysis.collectRawStacks(rows) -all_stacks = stackanalysis.mergeEqualStacks(raw_stacks) - -# enrich with function anchors -for stack in all_stacks: - for frame in stack["frames"]: - frame["anchor"] = "{}:{}".format( - frame["source_file"], fn_anchors.getFunctionName(frame["location"]) - ) - -# separate stacks for relevance -error_stacks = [] -warn_stacks = [] -info_stacks = [] -abort_stacks = [] -stackanalysis.filterStacksForPropagation( - all_stacks, error_stacks, warn_stacks, info_stacks, abort_stacks -) -run["errorfile"] = getFname("qmerrors") -utils.writeJSONFile(run["errorfile"], error_stacks) -run["warnfile"] = getFname("qmwarnings") -utils.writeJSONFile(run["warnfile"], warn_stacks) -run["infofile"] = getFname("qminfo") -utils.writeJSONFile(run["infofile"], info_stacks) -run["abortfile"] = getFname("qmabort") -utils.writeJSONFile(run["abortfile"], abort_stacks) -utils.updateLastRunToExecutionFile(workdir, run) - - -# print results to stdout -print("Found {} error stacks.".format(len(error_stacks))) -print("Found {} warning stacks.".format(len(warn_stacks))) -print("Found {} info stacks.".format(len(info_stacks))) -print("Found {} aborted stacks.".format(len(abort_stacks))) -print("") -print("Error stacks:") -print(stackanalysis.printStacks(error_stacks)) -print("") -print("Error stacks grouped by anchors:") -anchors = stackanalysis.groupStacksForAnchors(error_stacks) -anchornames = list(anchors.keys()) -for a in anchornames: - print(stackanalysis.printStacks(anchors[a]["stacks"])) - print("") -print("") -print("Warning stacks:") -print(stackanalysis.printStacks(warn_stacks)) -print("") -print("Info stacks:") -print(stackanalysis.printStacks(info_stacks)) -print("") -print("Aborted stacks:") -print(stackanalysis.printStacks(abort_stacks)) diff --git a/dom/quota/scripts/fetch_qm_failures.py b/dom/quota/scripts/fetch_qm_failures.py deleted file mode 100755 index 546b213582..0000000000 --- a/dom/quota/scripts/fetch_qm_failures.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python3 -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -import getopt -import sys - -import telemetry -import utils - -""" -The analysis is based on the following query: -https://sql.telemetry.mozilla.org/queries/78691/source?p_day=28&p_month=03&p_year=2021 - -SELECT UNIX_MILLIS(timestamp) AS submit_timeabs, - session_start_time, - submission_date, - build_id, - client_id, - session_id, - event_timestamp, - CAST(mozfun.map.get_key(event_map_values, "seq") AS INT64) AS seq, - mozfun.map.get_key(event_map_values, "context") AS context, - mozfun.map.get_key(event_map_values, "source_file") AS source_file, - mozfun.map.get_key(event_map_values, "source_line") AS source_line, - mozfun.map.get_key(event_map_values, "severity") AS severity, - mozfun.map.get_key(event_map_values, "result") AS result, -FROM telemetry.events -WHERE submission_date >= CAST('{{ year }}-{{ month }}-{{ day }}' AS DATE) - AND event_category='dom.quota.try' - AND build_id >= '{{ build }}' - AND UNIX_MILLIS(timestamp) > {{ last }} -ORDER BY submit_timeabs -LIMIT 600000 - -We fetch events in chronological order, as we want to keep track of where we already -arrived with our analysis. To accomplish this we write our runs into qmexecutions.json. - -[ - { - "workdir": ".", - "daysback": 1, - "numrows": 17377, - "lasteventtime": 1617303855145, - "rawfile": "./qmrows_until_1617303855145.json" - } -] - -lasteventtime is the highest value of event_timeabs we found in our data. - -analyze_qm_failures instead needs the rows to be ordered by -client_id, session_id, thread_id, submit_timeabs, seq -Thus we sort the rows accordingly before writing them. -""" - - -def usage(): - print( - "fetch_qm_faiures.py -k <apikey> -b <minimum build=20210329000000>" - "-d <days back=1> -l <last event time> -w <workdir=.>" - ) - print("") - print("Invokes the query 78691 and stores the result in a JSON file.") - print("-k <apikey>: Your personal telemetry API key (not the query key!).") - print("-d <daysback>: Number of days to go back. Default is 1.") - print("-b <minimum build>: The lowest build id we will fetch data for.") - print("-l <last event time>: Fetch only events after this. Default is 0.") - print("-w <workdir>: Working directory, default is '.'") - sys.exit(2) - - -days = 1 -lasteventtime = 0 -key = "undefined" -workdir = "." -minbuild = "20210329000000" - -try: - opts, args = getopt.getopt( - sys.argv[1:], - "k:b:d:l:w:", - ["key=", "build=", "days=", "lasteventtime=", "workdir="], - ) - for opt, arg in opts: - if opt == "-k": - key = arg - elif opt == "-d": - days = int(arg) - elif opt == "-l": - lasteventtime = int(arg) - elif opt == "-b": - minbuild = arg - elif opt == "-w": - workdir = arg -except getopt.GetoptError: - usage() - -if key == "undefined": - usage() - -start = utils.dateback(days) -year = start.year -month = start.month -day = start.day - -run = {} -lastrun = utils.getLastRunFromExecutionFile(workdir) -if "lasteventtime" in lastrun: - lasteventtime = lastrun["lasteventtime"] -run["workdir"] = workdir -run["daysback"] = days -run["minbuild"] = minbuild - -p_params = "p_year={:04d}&p_month={:02d}&p_day={:02d}&p_build={}" "&p_last={}".format( - year, month, day, minbuild, lasteventtime -) -print(p_params) -result = telemetry.query(key, 78691, p_params) -rows = result["query_result"]["data"]["rows"] -run["numrows"] = len(rows) -if run["numrows"] > 0: - lasteventtime = telemetry.getLastEventTimeAbs(rows) - run["lasteventtime"] = lasteventtime - rows.sort( - key=lambda row: "{}.{}.{}.{}.{:06d}".format( - row["client_id"], - row["session_id"], - row["seq"] >> 32, # thread_id - row["submit_timeabs"], - row["seq"] & 0x00000000FFFFFFFF, # seq, - ), - reverse=False, - ) - outfile = "{}/qmrows_until_{}.json".format(workdir, lasteventtime) - utils.writeJSONFile(outfile, rows) - run["rawfile"] = outfile -else: - print("No results found, maybe next time.") - run["lasteventtime"] = lasteventtime - -utils.addNewRunToExecutionFile(workdir, run) diff --git a/dom/quota/scripts/qm-try-analysis/.containerignore b/dom/quota/scripts/qm-try-analysis/.containerignore new file mode 100644 index 0000000000..39b984a045 --- /dev/null +++ b/dom/quota/scripts/qm-try-analysis/.containerignore @@ -0,0 +1,5 @@ +.venv +.gitignore +.vscode +output +Containerfile diff --git a/dom/quota/scripts/qm-try-analysis/Containerfile b/dom/quota/scripts/qm-try-analysis/Containerfile new file mode 100644 index 0000000000..a0999b7557 --- /dev/null +++ b/dom/quota/scripts/qm-try-analysis/Containerfile @@ -0,0 +1,24 @@ +FROM rust:1.74 as build + +RUN git clone https://github.com/mozilla/rust-code-analysis && \ + cd rust-code-analysis && \ + git checkout 56f182ac570 && \ + cargo build -p rust-code-analysis-cli --release -v + +FROM python:3.9.18-slim + +COPY --from=build /rust-code-analysis/target/release/rust-code-analysis-cli /usr/local/bin/ + +RUN pip install --no-cache-dir poetry==1.7 + +RUN useradd -ms /bin/bash scripts +USER scripts + +WORKDIR /home/scripts/qm-try-analysis + +COPY poetry.lock pyproject.toml ./ +RUN poetry install --no-dev + +COPY ./ . + +ENTRYPOINT /bin/bash -c "poetry shell && exec /bin/bash -i" diff --git a/dom/quota/scripts/qm-try-analysis/README.md b/dom/quota/scripts/qm-try-analysis/README.md new file mode 100644 index 0000000000..cef1a6717c --- /dev/null +++ b/dom/quota/scripts/qm-try-analysis/README.md @@ -0,0 +1,104 @@ +# QM_TRY Analysis Guide + +Welcome to the QM_TRY Analysis Guide! +This document walks you through the process of setting up the environment for semi-automatic monitoring of QM_TRY failures within Mozilla. +Follow these step-by-step instructions to ensure you have the necessary requirements before initiating the analysis. + +## Setup Instructions + +### 1. Clone mozilla-central + +Ensure you have a local clone of mozilla-central. +If you don't have it yet, refer to [this link](https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html#bootstrap-a-copy-of-the-firefox-source-code). +Building the project is not necessary for this setup. + +### 2. Install rust-code-analysis + +If not done already, set up Rust by visiting [rustup.rs](https://rustup.rs/). +Once Rust is installed, install rust-code-analysis using the following command: + +```bash +cargo install --git https://github.com/mozilla/rust-code-analysis --rev 56f182ac570 +``` + +### 3. Obtain Telemetry API Key + +Obtain a Telemetry API Key by visiting [Telemetry API Key](https://sql.telemetry.mozilla.org/users/me). +Save this key for later use in the analysis scripts. + +### 4. Obtain Bugzilla API Key + +Obtain your Bugzilla API Key from [Bugzilla User Preferences](https://bugzilla.mozilla.org/userprefs.cgi?tab=apikey). + +### 5. Install Python + +Install Python if not already set up. + +### 6. Install Poetry and set up dependencies + +If you haven't installed Poetry, use the following commands to install it and set up the project: + +```bash +pip install poetry +cd mozilla-unified/dom/quota/scripts/qm-try-analysis +poetry install +``` + +### Containerized setup + +To streamline the setup process, use [`Podman`](https://github.com/containers/podman?tab=readme-ov-file#podman-a-tool-for-managing-oci-containers-and-pods) with the provided `Containerfile`. Navigate to the relevant directory: + +```bash +cd mozilla-unified/dom/quota/scripts/qm-try-analysis +``` + +Build the container image and run the container: + +```bash +podman run -it $(podman build -q .) -v <path on your system>:/home/scripts/qm-try-analysis/output +``` + +## Effort + +- Each run takes approximately 5–15 minutes, with scripts running in less than 5 minutes. +- Analysis is performed once a week (as of November 2023) on Monday. + +## Generate Output + +Navigate to the analysis directory: + +```bash +cd mozilla-unified/dom/quota/scripts/qm-try-analysis +``` + +The process involves fetching data, analyzing, and reporting. Here's a quick overview: + +```bash +# Jump into a poetry shell session +poetry shell + +# Fetch data +qm-try-analysis fetch [OPTIONS] + +# Analyze data +qm-try-analysis analyze [OPTIONS] + +# Report failures to Bugzilla +qm-try-analysis report [OPTIONS] + +# To exit the shell session +exit +``` + +Refer to the detailed usage instructions provided by adding the `--help` option to one of the commands above. + +## Analysis + +- Look for noticeable problems such as new errors, unusual stacks, or issues not encountered for a long time. + +## Additional Hints + +- Treat QM_TRY bugs as meta bugs; do not attach patches there; create a new bug for that and cross-link using blocks/depends on. +- Interesting bugs to cross-link: + - [Bug 1705304](https://bugzilla.mozilla.org/show_bug.cgi?id=1705304) (FATAL errors): "error conditions we cannot meaningfully recover from." + - [Bug 1712582](https://bugzilla.mozilla.org/show_bug.cgi?id=1712582) (Replace generic NS_ERROR_FAILURE errors with more specific codes). diff --git a/dom/quota/scripts/qm-try-analysis/poetry.lock b/dom/quota/scripts/qm-try-analysis/poetry.lock new file mode 100644 index 0000000000..079172f133 --- /dev/null +++ b/dom/quota/scripts/qm-try-analysis/poetry.lock @@ -0,0 +1,283 @@ +# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. + +[[package]] +name = "bugzilla" +version = "1.0.0" +description = "A client library for Bugzilla" +optional = false +python-versions = "*" +files = [ + {file = "bugzilla-1.0.0.tar.gz", hash = "sha256:6e864ddafc4e46c821c1f3735d7c9686522a4eece056be0cadf51221e22dfa11"}, +] + +[[package]] +name = "certifi" +version = "2023.11.17" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "7.4.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "urllib3" +version = "2.1.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "aff36e769a32652ef044eecff451fa01657c0ed660af9a372a7ca8b874361d47" diff --git a/dom/quota/scripts/qm-try-analysis/pyproject.toml b/dom/quota/scripts/qm-try-analysis/pyproject.toml new file mode 100644 index 0000000000..f567928dd8 --- /dev/null +++ b/dom/quota/scripts/qm-try-analysis/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "qm-try-analysis" +version = "0.1.0" +description = "" +authors = [ + "Jens Stutte <jstutte@mozilla.com>", + "Michael van Straten <mvanstraten@mozilla.com>", +] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.9" +click = "^8.1.7" +requests = "^2.31.0" +bugzilla = "^1.0.0" + +[tool.poetry.scripts] +qm-try-analysis = "qm_try_analysis.cli:cli" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.3" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/dom/quota/test/marionette/dummy.py b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/dom/quota/test/marionette/dummy.py +++ b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/__init__.py diff --git a/dom/quota/scripts/qm-try-analysis/qm_try_analysis/analyze.py b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/analyze.py new file mode 100755 index 0000000000..1173555e08 --- /dev/null +++ b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/analyze.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import sys +from os import path + +import click + +from qm_try_analysis import fn_anchors, stackanalysis, utils +from qm_try_analysis.logging import error, info + +""" +The analysis is based on stack frames of the following form: + +[ + { + "event_timeabs": 1617121013137, + "session_startabs": 1617120840000, + "build_id": "20210329095128", + "client_id": "0013a68f-9893-461a-93d4-2d7a2f85583f", + "session_id": "8cd37159-bd5c-481c-99ad-9eace9ea726a", + "seq": 1, + "context": "Initialization::TemporaryStorage", + "source_file": "dom/localstorage/ActorsParent.cpp", + "source_line": "1018", + "severity": "ERROR", + "result": "NS_ERROR_FILE_NOT_FOUND" + }, +... +] + +The location of the input file is expected to be found in the +last item of the list inside qmexecutions.json. +""" + + +@click.command() +@click.option( + "--output-to", + type=click.Path(dir_okay=False, writable=True), + default="qmstacks_until_<lasteventtime>.txt", + help="Specify the output file for the analyzed data.", +) +@click.option( + "-w", + "--workdir", + type=click.Path(file_okay=False, exists=True, writable=True), + default="output", + help="Working directory", +) +def analyze_qm_failures(output_to, workdir): + """ + Analyzes the results from fetch's JSON file. + Writes out several JSON results as files and a bugzilla markup table on stdout. + """ + run = utils.getLastRunFromExecutionFile(workdir) + if "numrows" not in run or run["numrows"] == 0: + error( + "No previous execution from fetch_qm_failures.py found or the last execution yielded no result." + ) + sys.exit(2) + + if output_to == "qmstacks_until_<lasteventtime>.txt": + output_to = path.join(workdir, f'qmstacks_until_{run["lasteventtime"]}.txt') + elif output_to.exists(): + error( + f'The output file "{output_to}" already exists. This script would override it.' + ) + sys.exit(2) + run["stacksfile"] = output_to + + def getFname(prefix): + return "{}/{}_until_{}.json".format(workdir, prefix, run["lasteventtime"]) + + # read rows from JSON + rows = utils.readJSONFile(getFname("qmrows")) + info(f"Found {len(rows)} rows of data") + rows = stackanalysis.sanitize(rows) + + # enrich rows with hg locations + buildids = stackanalysis.extractBuildIDs(rows) + utils.fetchBuildRevisions(buildids) + stackanalysis.constructHGLinks(buildids, rows) + + # transform rows to unique stacks + raw_stacks = stackanalysis.collectRawStacks(rows) + all_stacks = stackanalysis.mergeEqualStacks(raw_stacks) + + # enrich with function anchors + for stack in all_stacks: + for frame in stack["frames"]: + frame["anchor"] = "{}:{}".format( + frame["source_file"], fn_anchors.getFunctionName(frame["location"]) + ) + + # separate stacks for relevance + error_stacks = [] + warn_stacks = [] + info_stacks = [] + abort_stacks = [] + stackanalysis.filterStacksForPropagation( + all_stacks, error_stacks, warn_stacks, info_stacks, abort_stacks + ) + run["errorfile"] = getFname("qmerrors") + utils.writeJSONFile(run["errorfile"], error_stacks) + run["warnfile"] = getFname("qmwarnings") + utils.writeJSONFile(run["warnfile"], warn_stacks) + run["infofile"] = getFname("qminfo") + utils.writeJSONFile(run["infofile"], info_stacks) + run["abortfile"] = getFname("qmabort") + utils.writeJSONFile(run["abortfile"], abort_stacks) + utils.updateLastRunToExecutionFile(workdir, run) + + info(f"Found {len(error_stacks)} error stacks") + info(f"Found {len(warn_stacks)} warning stacks") + info(f"Found {len(info_stacks)} info stacks") + info(f"Found {len(abort_stacks)} aborted stacks") + + # Write results to the specified output file + with open(output_to, "w") as output: + + def print_to_output(message): + print(message, file=output) + + print_to_output("Error stacks:") + print_to_output(stackanalysis.printStacks(error_stacks)) + print_to_output("") + print_to_output("Error stacks grouped by anchors:") + anchors = stackanalysis.groupStacksForAnchors(error_stacks) + anchornames = list(anchors.keys()) + for a in anchornames: + print_to_output(stackanalysis.printStacks(anchors[a]["stacks"])) + print_to_output("") + print_to_output("") + print_to_output("Warning stacks:") + print_to_output(stackanalysis.printStacks(warn_stacks)) + print_to_output("") + print_to_output("Info stacks:") + print_to_output(stackanalysis.printStacks(info_stacks)) + print_to_output("") + print_to_output("Aborted stacks:") + print_to_output(stackanalysis.printStacks(abort_stacks)) + + info(f"Wrote results to specified output file {output_to}") + + +if __name__ == "__main__": + analyze_qm_failures() diff --git a/dom/quota/scripts/qm-try-analysis/qm_try_analysis/cli.py b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/cli.py new file mode 100644 index 0000000000..509a8e33e1 --- /dev/null +++ b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/cli.py @@ -0,0 +1,22 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import click + +from qm_try_analysis.analyze import analyze_qm_failures +from qm_try_analysis.fetch import fetch_qm_failures +from qm_try_analysis.report import report_qm_failures + + +@click.group(context_settings={"show_default": True}) +def cli(): + pass + + +cli.add_command(fetch_qm_failures, "fetch") +cli.add_command(analyze_qm_failures, "analyze") +cli.add_command(report_qm_failures, "report") + +if __name__ == "__main__": + cli() diff --git a/dom/quota/scripts/qm-try-analysis/qm_try_analysis/fetch.py b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/fetch.py new file mode 100644 index 0000000000..2512293c29 --- /dev/null +++ b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/fetch.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import pathlib + +import click + +from qm_try_analysis import telemetry, utils +from qm_try_analysis.logging import info + +""" +The analysis is based on the following query: +https://sql.telemetry.mozilla.org/queries/78691/source?p_day=28&p_month=03&p_year=2021 + +SELECT UNIX_MILLIS(timestamp) AS submit_timeabs, + session_start_time, + submission_date, + build_id, + client_id, + session_id, + event_timestamp, + CAST(mozfun.map.get_key(event_map_values, "seq") AS INT64) AS seq, + mozfun.map.get_key(event_map_values, "context") AS context, + mozfun.map.get_key(event_map_values, "source_file") AS source_file, + mozfun.map.get_key(event_map_values, "source_line") AS source_line, + mozfun.map.get_key(event_map_values, "severity") AS severity, + mozfun.map.get_key(event_map_values, "result") AS result, +FROM telemetry.events +WHERE submission_date >= CAST('{{ year }}-{{ month }}-{{ day }}' AS DATE) + AND event_category='dom.quota.try' + AND build_id >= '{{ build }}' + AND UNIX_MILLIS(timestamp) > {{ last }} +ORDER BY submit_timeabs +LIMIT 600000 + +We fetch events in chronological order, as we want to keep track of where we already +arrived with our analysis. To accomplish this we write our runs into qmexecutions.json. + +[ + { + "workdir": ".", + "daysback": 1, + "numrows": 17377, + "lasteventtime": 1617303855145, + "rawfile": "./qmrows_until_1617303855145.json" + } +] + +lasteventtime is the highest value of event_timeabs we found in our data. + +analyze_qm_failures instead needs the rows to be ordered by +client_id, session_id, thread_id, submit_timeabs, seq +Thus we sort the rows accordingly before writing them. +""" + + +@click.command() +@click.option( + "-k", + "--key", + required=True, + help="Your personal telemetry API key.", +) +@click.option( + "-b", + "--minbuild", + default="20210329000000", + help="The lowest build id we will fetch data for. This should have the following format: `yyyymmddhhmmss`.", +) +@click.option("-d", "--days", type=int, default=7, help="Number of days to go back.") +@click.option( + "-l", + "--lasteventtime", + type=int, + default=0, + help="Fetch only events after this number of Unix milliseconds.", +) +@click.option( + "-w", + "--workdir", + type=click.Path(file_okay=False, writable=True, path_type=pathlib.Path), + default="output", + help="Working directory", +) +def fetch_qm_failures(key, minbuild, days, lasteventtime, workdir): + """ + Invokes the query 78691 and stores the result in a JSON file. + """ + # Creeate output dir if it does not exist + workdir.mkdir(exist_ok=True) + + start = utils.dateback(days) + year, month, day = start.year, start.month, start.day + + run = {} + lastrun = utils.getLastRunFromExecutionFile(workdir) + if "lasteventtime" in lastrun: + lasteventtime = lastrun["lasteventtime"] + + run["workdir"] = workdir.as_posix() + run["daysback"] = days + run["minbuild"] = minbuild + + p_params = f"p_year={year:04d}&p_month={month:02d}&p_day={day:02d}&p_build={minbuild}&p_last={lasteventtime}" + + # Read string at the start of the file for more information on query 78691 + result = telemetry.query(key, 78691, p_params) + rows = result["query_result"]["data"]["rows"] + run["numrows"] = len(rows) + + if run["numrows"] > 0: + lasteventtime = telemetry.getLastEventTimeAbs(rows) + run["lasteventtime"] = lasteventtime + rows.sort( + key=lambda row: "{}.{}.{}.{}.{:06d}".format( + row["client_id"], + row["session_id"], + row["seq"] >> 32, # thread_id + row["submit_timeabs"], + row["seq"] & 0x00000000FFFFFFFF, # seq, + ), + reverse=False, + ) + outfile = f"{workdir}/qmrows_until_{lasteventtime}.json" + utils.writeJSONFile(outfile, rows) + run["rawfile"] = outfile + else: + info("No results found, maybe next time.") + run["lasteventtime"] = lasteventtime + + utils.addNewRunToExecutionFile(workdir, run) + + +if __name__ == "__main__": + fetch_qm_failures() diff --git a/dom/quota/scripts/fetch_fn_names.sh b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/fetch_fn_names.sh index 6d3a3c4d23..bd619186cd 100755 --- a/dom/quota/scripts/fetch_fn_names.sh +++ b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/fetch_fn_names.sh @@ -5,13 +5,13 @@ # This script assumes to have rust-code-analysis-cli in the path. HG_URL=$1 -TEMPDIR=/tmp/fetch_fn_names_$BASHPID +TEMPDIR=/tmp/fetch_fn_names_$$ TEMPSRC=$TEMPDIR/src mkdir $TEMPDIR echo "" > $TEMPDIR/empty.json HG_URL=`echo $HG_URL | sed 's/annotate/raw-file/g'` wget -q -O "$TEMPSRC" $HG_URL -rust-code-analysis-cli -m -O json -o "$TEMPDIR" -p "$TEMPSRC" +rust-code-analysis-cli -m -O json -p "$TEMPSRC" CONTENT=`cat $TEMPDIR/*.json` rm -rf $TEMPDIR echo $CONTENT diff --git a/dom/quota/scripts/fn_anchors.py b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/fn_anchors.py index eeaf43764c..13e3802399 100644 --- a/dom/quota/scripts/fn_anchors.py +++ b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/fn_anchors.py @@ -4,16 +4,24 @@ import json import subprocess +from os import path + +from qm_try_analysis.logging import info, warning cached_functions = {} def getMetricsJson(src_url): if src_url.startswith("http"): - print("Fetching source for function extraction: {}".format(src_url)) - metrics = subprocess.check_output(["./fetch_fn_names.sh", src_url]) + info(f"Fetching source for function extraction: {src_url}") + metrics = subprocess.check_output( + [ + path.join(path.dirname(path.realpath(__file__)), "fetch_fn_names.sh"), + src_url, + ] + ) else: - print("Skip fetching source: {}".format(src_url)) + warning(f"Skip fetching source: {src_url}") metrics = "" try: diff --git a/dom/quota/scripts/qm-try-analysis/qm_try_analysis/logging.py b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/logging.py new file mode 100644 index 0000000000..c96679f96c --- /dev/null +++ b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/logging.py @@ -0,0 +1,21 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import click + + +def debug(message): + click.echo(click.style("Debug", fg="cyan") + f": {message}") + + +def info(message): + click.echo(click.style("Info", fg="white") + f": {message}") + + +def warning(message): + click.echo(click.style("Warning", fg="yellow") + f": {message}") + + +def error(message): + click.echo(click.style("Error", fg="red") + f": {message}") diff --git a/dom/quota/scripts/qm-try-analysis/qm_try_analysis/report.py b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/report.py new file mode 100644 index 0000000000..0ec5428679 --- /dev/null +++ b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/report.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import hashlib +import json +import re +import sys +import webbrowser +from typing import Union + +import bugzilla +import click +from click.utils import echo + +from qm_try_analysis import stackanalysis, utils +from qm_try_analysis.logging import error, info, warning + +# Flag for toggling development mod +DEV = True + +# Constants for Bugzilla URLs +if DEV: + BUGZILLA_BASE_URL = "https://bugzilla-dev.allizom.org/" +else: + BUGZILLA_BASE_URL = "https://bugzilla.mozilla.org/" + +BUGZILLA_API_URL = BUGZILLA_BASE_URL + "rest/" +BUGZILLA_ATTACHMENT_URL = BUGZILLA_BASE_URL + "attachment.cgi?id=" +BUGZILLA_BUG_URL = BUGZILLA_BASE_URL + "show_bug.cgi?id=" + +# Constants for static bugs +QM_TRY_FAILURES_BUG = 1702411 +WARNING_STACKS_BUG = 1711703 + +# Regex pattern for parsing anchor strings +ANCHOR_REGEX_PATTERN = re.compile(r"([^:]+):([^:]+)?:*([^:]+)?") + + +@click.command() +@click.option( + "-k", + "--key", + help="Your personal Bugzilla API key", + required=True, +) +@click.option( + "--stacksfile", + type=click.File("rb"), + help="The output file of the previous analysis run. You only have to specify this, if the previous run does not include this info.", +) +@click.option( + "--open-modified/--no-open-modified", + default=True, + help="Whether to open modified bugs in your default browser after updating them.", +) +@click.option( + "-w", + "--workdir", + type=click.Path(file_okay=False, exists=True, writable=True), + default="output", + help="Working directory", +) +def report_qm_failures(key, stacksfile, open_modified, workdir): + """ + Report QM failures to Bugzilla based on stack analysis. + """ + run = utils.getLastRunFromExecutionFile(workdir) + + # Check for valid execution file from the previous run + if not {"errorfile", "warnfile"} <= run.keys(): + error("No analyzable execution from the previous run of analyze found.") + echo("Did you remember to run `poetry run qm-try-analysis analyze`?") + sys.exit(2) + + # Handle missing stacksfile + if not stacksfile: + if "stacksfile" not in run: + error( + "The previous analyze run did not contain the location of the stacksfile." + ) + echo('Please provide the file location using the "--stacksfile" option.') + sys.exit(2) + stacksfile = open(run["stacksfile"], "rb") + + # Create Bugzilla client + bugzilla_client = bugzilla.Bugzilla(url=BUGZILLA_API_URL, api_key=key) + + # Initialize report data + report = run.get("report", {}) + run["report"] = report + attachment_id = report.get("stacksfile_attachment", None) + reported = report.get("reported", []) + report["reported"] = reported + + def post_comment(bug_id, comment): + """ + Post a comment to a Bugzilla bug. + """ + data = {"id": bug_id, "comment": comment, "is_markdown": True} + res = bugzilla_client._post(f"bug/{bug_id}/comment", json.dumps(data)) + return res["id"] + + # Handle missing attachment ID + if not attachment_id: + attachment = bugzilla.DotDict() + attachment.file_name = f"qmstacks_until_{run['lasteventtime']}.txt" + attachment.summary = attachment.file_name + attachment.content_type = "text/plain" + attachment.data = stacksfile.read().decode() + res = bugzilla_client.post_attachment(QM_TRY_FAILURES_BUG, attachment) + attachment_id = next(iter(res["attachments"].values()))["id"] + report["stacksfile_attachment"] = attachment_id + utils.updateLastRunToExecutionFile(workdir, run) + + info( + f'Created attachment for "{attachment.file_name}": {BUGZILLA_ATTACHMENT_URL + str(attachment_id)}.' + ) + + def generate_comment(stacks): + """ + Generate a comment for Bugzilla based on error stacks. + """ + comment = f"Taken from Attachment {attachment_id}\n\n" + comment += stackanalysis.printStacks(stacks) + return comment + + # Handle missing warnings comment + if "warnings_comment" not in report: + warning_stacks = utils.readJSONFile(run["warnfile"]) + warning_stacks = filter(lambda stack: stack["hit_count"] >= 100, warning_stacks) + + comment = generate_comment(warning_stacks) + comment_id = post_comment(WARNING_STACKS_BUG, comment) + + report["warnings_comment"] = comment_id + utils.updateLastRunToExecutionFile(workdir, run) + + info("Created comment for warning stacks.") + + error_stacks = utils.readJSONFile(run["errorfile"]) + + def reduce(search_results, by: str) -> Union[int, None]: + """ + Reduce bug search results automatically or based on user input. + """ + anchor = by + + search_results = remove_duplicates(search_results, bugzilla_client) + + if not search_results: + return + + if len(search_results) == 1: + return search_results[0]["id"] + + echo(f'Multiple bugs found for anchor "{anchor}":') + + for i, result in enumerate(search_results, start=1): + echo( + f"{i}.{' [closed]' if result['resolution'] != '' else ''} {BUGZILLA_BUG_URL + str(result['id'])} - {result['summary']}" + ) + + choice = click.prompt( + "Enter the number of the bug you want to use", + type=click.Choice( + [str(i) for i in range(1, len(search_results) + 1)] + ["skip"] + ), + default="skip", + show_default=True, + confirmation_prompt="Please confirm the selected choice", + ) + + if choice == "skip": + return + + return search_results[int(choice) - 1]["id"] + + anchors = stackanalysis.groupStacksForAnchors(error_stacks) + + for anchor in anchors: + if hash_str(anchor) in reported: + info(f'Skipping anchor "{anchor}" since it has already been reported.') + continue + + if not (match := ANCHOR_REGEX_PATTERN.match(anchor)): + warning(f'"{anchor}" did not match the regex pattern.') + + if "Unknown" in match.group(2): + warning(f'Skipping "{anchor}" since it is not a valid anchor.') + continue + + search_string = " ".join(filter(None, match.groups())) + search_results = bugzilla_client.search_bugs( + [{"product": "Core", "summary": search_string}] + )["bugs"] + + if bug_id := reduce(search_results, by=anchor): + info(f'Found bug {BUGZILLA_BUG_URL + str(bug_id)} for anchor "{anchor}".') + else: + warning(f'No bug found for anchor "{anchor}".') + + if not click.confirm("Would you like to create one?"): + continue + + bug = bugzilla.DotDict() + bug.product = "Core" + bug.component = "Storage: Quota Manager" + bug.summary = f"[QM_TRY] Failures in {anchor}" + bug.description = f"This bug keeps track of the semi-automatic monitoring of QM_TRY failures in `{anchor}`" + bug["type"] = "defect" + bug.blocks = QM_TRY_FAILURES_BUG + bug.version = "unspecified" + + bug_id = bugzilla_client.post_bug(bug)["id"] + + info(f'Created bug {BUGZILLA_BUG_URL + str(bug_id)} for anchor "{anchor}".') + + comment = generate_comment(anchors[anchor]["stacks"]) + comment_id = post_comment(bug_id, comment) + + reported.append(hash_str(anchor)) + utils.updateLastRunToExecutionFile(workdir, run) + + if open_modified: + comment_seq_number = bugzilla_client.get_comment(comment_id)["comments"][ + str(comment_id) + ]["count"] + webbrowser.open( + BUGZILLA_BUG_URL + str(bug_id) + "#c" + str(comment_seq_number) + ) + + +def hash_str(s): + """ + Hash a string using MD5. + """ + encoded_str = s.encode("utf-8") + return int(hashlib.md5(encoded_str).hexdigest(), 16) + + +def remove_duplicates(search_results, bugzilla_client): + """ + Remove duplicate bugs in search results. + """ + resolved_bugs = set(bug["id"] for bug in search_results if not bug.get("dupe_of")) + + def resolve_if_dupe(bug): + if not (dupe_of := bug.get("dupe_of")): + return bug + + if dupe_of in resolved_bugs: + return None + + remote = resolve_if_dupe(bugzilla_client.get_bug(dupe_of)) + if remote: + resolved_bugs.add(remote["id"]) + + return remote + + return [non_dupe for bug in search_results if (non_dupe := resolve_if_dupe(bug))] + + +if __name__ == "__main__": + report_qm_failures() diff --git a/dom/quota/scripts/stackanalysis.py b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/stackanalysis.py index f0363c5e1f..f0363c5e1f 100644 --- a/dom/quota/scripts/stackanalysis.py +++ b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/stackanalysis.py diff --git a/dom/quota/scripts/telemetry.py b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/telemetry.py index a62abd62b1..26debd0546 100644 --- a/dom/quota/scripts/telemetry.py +++ b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/telemetry.py @@ -6,20 +6,22 @@ import time import requests +from qm_try_analysis.logging import info + +TELEMETRY_BASE_URL = "https://sql.telemetry.mozilla.org/api/" + def query(key, query, p_params): headers = {"Authorization": "Key {}".format(key)} - start_url = "https://sql.telemetry.mozilla.org/api/" "queries/{}/refresh?{}".format( - query, p_params - ) - print(start_url) + start_url = TELEMETRY_BASE_URL + f"queries/{query}/refresh?{p_params}" + info(f"Starting job using url {start_url}") resp = requests.post(url=start_url, headers=headers) job = resp.json()["job"] - jid = job["id"] - print("Started job {}".format(jid)) + job_id = job["id"] + info(f"Started job {job_id}") - poll_url = "https://sql.telemetry.mozilla.org/api/" "jobs/{}".format(jid) - print(poll_url) + poll_url = TELEMETRY_BASE_URL + f"jobs/{job_id}" + info(f"Polling query status from {poll_url}") poll = True status = 0 qresultid = 0 @@ -34,15 +36,13 @@ def query(key, query, p_params): else: time.sleep(0.2) print(".") - print("Finished with status {}".format(status)) + info(f"Finished with status {status}") if status == 3: - fetch_url = ( - "https://sql.telemetry.mozilla.org/api/" - "queries/78691/results/{}.json".format(qresultid) - ) - print(fetch_url) - resp = requests.get(url=fetch_url, headers=headers) + results_url = TELEMETRY_BASE_URL + f"queries/78691/results/{qresultid}.json" + + info(f"Querying result from {results_url}") + resp = requests.get(url=results_url, headers=headers) return resp.json() return {"query_result": {"data": {"rows": {}}}} diff --git a/dom/quota/scripts/utils.py b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/utils.py index d5322728a5..485dbf66f5 100644 --- a/dom/quota/scripts/utils.py +++ b/dom/quota/scripts/qm-try-analysis/qm_try_analysis/utils.py @@ -7,6 +7,8 @@ import json import requests +from qm_try_analysis.logging import error, info, warning + def readJSONFile(FileName): f = open(FileName, "r") @@ -37,7 +39,7 @@ def fetchBuildRevisions(buildids): buildhub_url = "https://buildhub.moz.tools/api/search" delids = {} for bid in buildids: - print("Fetching revision for build {}.".format(bid)) + info(f"Fetching revision for build {bid}.") body = {"size": 1, "query": {"term": {"build.id": bid}}} resp = requests.post(url=buildhub_url, json=body) hits = resp.json()["hits"]["hits"] @@ -48,7 +50,7 @@ def fetchBuildRevisions(buildids): + hits[0]["_source"]["source"]["revision"] ) else: - print("No revision for build.id {}".format(bid)) + warning(f"No revision for build.id {bid}") delids[bid] = "x" for bid in delids: buildids.pop(bid) @@ -67,7 +69,7 @@ def writeExecutionFile(workdir, executions): try: writeJSONFile(exefile, executions) except OSError: - print("Error writing execution record.") + error("Error writing execution record.") def getLastRunFromExecutionFile(workdir): diff --git a/dom/quota/scripts/qm-try-analysis/tests/test_reporting.py b/dom/quota/scripts/qm-try-analysis/tests/test_reporting.py new file mode 100644 index 0000000000..f3be6d9cf0 --- /dev/null +++ b/dom/quota/scripts/qm-try-analysis/tests/test_reporting.py @@ -0,0 +1,96 @@ +import copy + +from qm_try_analysis.report import remove_duplicates + + +class MockClient(object): + def __init__(self, search_results, remote_bugs): + self.search_results = copy.deepcopy(search_results) + self.remote_bugs = copy.deepcopy(remote_bugs) + + def get_bug(self, bug_id): + for item in self.search_results: + if bug_id == item["id"]: + return copy.deepcopy(item) + for item in self.remote_bugs: + if bug_id == item["id"]: + return copy.deepcopy(item) + return {"id": bug_id, "dupe_of": None} + + +def test_duplicate_bug_removal(): + test_cases = [ + { + "search_results": [ + {"id": "k1", "dupe_of": "k4"}, + {"id": "k3", "dupe_of": "k5"}, + {"id": "k2", "dupe_of": "k6"}, + ], + "remote_bugs": [ + {"id": "k4", "dupe_of": "k2"}, + {"id": "k5", "dupe_of": None}, + {"id": "k6", "dupe_of": "k3"}, + ], + "expected": [{"id": "k5", "dupe_of": None}], + }, + { + "search_results": [ + {"id": "k2", "dupe_of": "k6"}, + {"id": "k3", "dupe_of": "k5"}, + {"id": "k1", "dupe_of": "k4"}, + ], + "remote_bugs": [ + {"id": "k4", "dupe_of": "k2"}, + {"id": "k5", "dupe_of": None}, + {"id": "k6", "dupe_of": "k3"}, + ], + "expected": [{"id": "k5", "dupe_of": None}], + }, + { + "search_results": [ + {"id": "k1", "dupe_of": "k3"}, + {"id": "k2", "dupe_of": "k4"}, + ], + "remote_bugs": [ + {"id": "k3", "dupe_of": "k2"}, + {"id": "k4", "dupe_of": None}, + ], + "expected": [{"id": "k4", "dupe_of": None}], + }, + { + "search_results": [ + {"id": "k1", "dupe_of": "k3"}, + {"id": "k2", "dupe_of": None}, + ], + "remote_bugs": [ + {"id": "k3", "dupe_of": "k4"}, + {"id": "k4", "dupe_of": "k2"}, + ], + "expected": [{"id": "k2", "dupe_of": None}], + }, + { + "search_results": [ + {"id": "k1", "dupe_of": "k3"}, + {"id": "k2", "dupe_of": None}, + ], + "remote_bugs": [{"id": "k3", "dupe_of": "k2"}], + "expected": [{"id": "k2", "dupe_of": None}], + }, + { + "search_results": [ + {"id": "k1", "dupe_of": None}, + {"id": "k2", "dupe_of": "k3"}, + ], + "remote_bugs": [{"id": "k3", "dupe_of": "k1"}], + "expected": [{"id": "k1", "dupe_of": None}], + }, + ] + + for test_case in test_cases: + search_results = test_case["search_results"] + remote_bugs = test_case["remote_bugs"] + expected = test_case["expected"] + + mock_client = MockClient(search_results, remote_bugs) + + assert remove_duplicates(search_results, mock_client) == expected diff --git a/dom/quota/test/marionette/manifest.toml b/dom/quota/test/marionette/manifest.toml index de02cdd541..f80095c590 100644 --- a/dom/quota/test/marionette/manifest.toml +++ b/dom/quota/test/marionette/manifest.toml @@ -1,5 +1,4 @@ [DEFAULT] support-files = ["quota_test_case.py"] -["dummy.py"] -skip-if = ["true"] +["test_private_repository_cleanup.py"] diff --git a/dom/quota/test/marionette/quota_test_case.py b/dom/quota/test/marionette/quota_test_case.py index c31edcaaf7..7d161cf4ff 100644 --- a/dom/quota/test/marionette/quota_test_case.py +++ b/dom/quota/test/marionette/quota_test_case.py @@ -10,6 +10,51 @@ from marionette_harness import MarionetteTestCase class QuotaTestCase(MarionetteTestCase): + def executeAsyncScript(self, script, script_args): + res = self.marionette.execute_async_script( + """ + const resolve = arguments[arguments.length - 1]; + + class RequestError extends Error { + constructor(resultCode, resultName) { + super(`Request failed (code: ${resultCode}, name: ${resultName})`); + this.name = "RequestError"; + this.resultCode = resultCode; + this.resultName = resultName; + } + } + + async function requestFinished(request) { + await new Promise(function (resolve) { + request.callback = function () { + resolve(); + }; + }); + + if (request.resultCode !== Cr.NS_OK) { + throw new RequestError(request.resultCode, request.resultName); + } + + return request.result; + } + """ + + script + + """ + main() + .then(function(result) { + resolve(result); + }) + .catch(function() { + resolve(null); + });; + """, + script_args=script_args, + new_sandbox=False, + ) + + assert res is not None + return res + def ensureInvariantHolds(self, op): maxWaitTime = 60 Wait(self.marionette, timeout=maxWaitTime).until( @@ -24,27 +69,23 @@ class QuotaTestCase(MarionetteTestCase): return None def getFullOriginMetadata(self, persistenceType, origin): - with self.marionette.using_context("chrome"): - res = self.marionette.execute_async_script( + with self.marionette.using_context(self.marionette.CONTEXT_CHROME): + res = self.executeAsyncScript( """ - const [persistenceType, origin, resolve] = arguments; + const [persistenceType, origin] = arguments; - const principal = Services.scriptSecurityManager. - createContentPrincipalFromOrigin(origin); - - const request = Services.qms.getFullOriginMetadata( - persistenceType, principal); - - request.callback = function() { - if (request.resultCode != Cr.NS_OK) { - resolve(null); - } else { - resolve(request.result); - } - } + async function main() { + const principal = Services.scriptSecurityManager. + createContentPrincipalFromOrigin(origin); + + const request = Services.qms.getFullOriginMetadata( + persistenceType, principal); + const metadata = await requestFinished(request); + + return metadata; + } """, script_args=(persistenceType, origin), - new_sandbox=False, ) assert res is not None @@ -57,41 +98,91 @@ class QuotaTestCase(MarionetteTestCase): sanitizedStorageOrigin = storageOrigin.replace(":", "+").replace("/", "+") return os.path.join( - profilePath, "storage", persistenceType, sanitizedStorageOrigin, client + self.getRepositoryPath(persistenceType), sanitizedStorageOrigin, client ) + def getRepositoryPath(self, persistenceType): + profilePath = self.marionette.instance.profile.profile + assert profilePath is not None + + return os.path.join(profilePath, "storage", persistenceType) + + def initStorage(self): + with self.marionette.using_context(self.marionette.CONTEXT_CHROME): + return self.executeAsyncScript( + """ + async function main() { + let req = Services.qms.init(); + await requestFinished(req); + + return true; + } + """, + script_args=(), + ) + + def initTemporaryOrigin(self, persistenceType, origin): + with self.marionette.using_context(self.marionette.CONTEXT_CHROME): + return self.executeAsyncScript( + """ + const [persistenceType, origin] = arguments; + async function main() { + const principal = Services.scriptSecurityManager. + createContentPrincipalFromOrigin(origin); + + let req = Services.qms.initializeTemporaryOrigin(persistenceType, principal); + await requestFinished(req) + + return true; + } + """, + script_args=( + persistenceType, + origin, + ), + ) + + def initTemporaryStorage(self): + with self.marionette.using_context(self.marionette.CONTEXT_CHROME): + return self.executeAsyncScript( + """ + async function main() { + const req = Services.qms.initTemporaryStorage(); + await requestFinished(req); + + return true; + } + """, + script_args=(), + ) + def resetStoragesForPrincipal(self, origin, persistenceType, client): # This method is used to force sqlite to write journal file contents to # main sqlite database file - script = """ - const [resolve] = arguments - - let origin = '%s'; - let persistenceType = '%s'; - let client = '%s'; - let principal = Services.scriptSecurityManager. - createContentPrincipalFromOrigin(origin); - - let req = Services.qms.resetStoragesForPrincipal(principal, persistenceType, client); - req.callback = () => { - if (req.resultCode == Cr.NS_OK) { - resolve(true); - } else { - resolve(false); + with self.marionette.using_context(self.marionette.CONTEXT_CHROME): + res = self.executeAsyncScript( + """ + const [origin, persistenceType, client] = arguments; + + async function main() { + const principal = Services.scriptSecurityManager. + createContentPrincipalFromOrigin(origin); + + const request = Services.qms.resetStoragesForPrincipal(principal, persistenceType, client); + await requestFinished(request); + + return true; } - } - """ % ( - origin, - persistenceType, - client, - ) + """, + script_args=(origin, persistenceType, client), + ) - with self.marionette.using_context(self.marionette.CONTEXT_CHROME): - return self.marionette.execute_async_script(script) + assert res is not None + return res @contextmanager - def using_new_window(self, path, private=False): + def using_new_window(self, path, private=False, skipCleanup=False): """ This helper method is created to ensure that a temporary window required inside the test scope is lifetime'd properly @@ -109,5 +200,6 @@ class QuotaTestCase(MarionetteTestCase): yield (origin, "private" if private else "default") finally: - self.marionette.close() - self.marionette.switch_to_window(oldWindow) + if not skipCleanup: + self.marionette.close() + self.marionette.switch_to_window(oldWindow) diff --git a/dom/quota/test/marionette/test_private_repository_cleanup.py b/dom/quota/test/marionette/test_private_repository_cleanup.py new file mode 100644 index 0000000000..0d4c488757 --- /dev/null +++ b/dom/quota/test/marionette/test_private_repository_cleanup.py @@ -0,0 +1,94 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys +from pathlib import Path + +sys.path.append(os.fspath(Path(__file__).parents[0])) +from quota_test_case import QuotaTestCase + +QM_TESTING_PREF = "dom.quotaManager.testing" +AUTOSTART_PBM_PREF = "browser.privatebrowsing.autostart" + +""" + This test ensures that private repository gets properly purged in all below scenarios: + 1. at PBM session end + 2. on firefox start when last session was abnormally terminated (crash) + 3. on firefox shutdown (when PBM autostart is enabled i.e. browser.privatebrowsing.autostart set to true) +""" + + +class PrivateRepositoryCleanup(QuotaTestCase): + def setUp(self, autostartPBM=False): + super(PrivateRepositoryCleanup, self).setUp() + + self.marionette.set_pref(AUTOSTART_PBM_PREF, autostartPBM) + self.marionette.set_pref(QM_TESTING_PREF, True) + + self.marionette.restart(in_app=True) + + assert self.initStorage() + assert self.initTemporaryStorage() + + def tearDown(self): + self.marionette.clear_pref(AUTOSTART_PBM_PREF) + self.marionette.clear_pref(QM_TESTING_PREF) + + self.marionette.restart(in_app=True) + super(PrivateRepositoryCleanup, self).tearDown() + + def doStorageWork(self): + origin = self.marionette.absolute_url("")[:-1] + "^privateBrowsingId=1" + assert self.initTemporaryOrigin("private", origin) + + self.ensureInvariantHolds(lambda _: os.path.exists(self.getPrivateRepository())) + + def verifyCleanup(self): + self.ensureInvariantHolds( + lambda _: not os.path.exists(self.getPrivateRepository()) + ) + + def getPrivateRepository(self): + return self.getRepositoryPath("private") + + +class PBM(PrivateRepositoryCleanup): + def test_ensure_cleanup(self): + with self.using_new_window("", private=True): + self.doStorageWork() + + # private window must have been close by now + self.verifyCleanup() + + def test_ensure_cleanup_after_crash(self): + with self.using_new_window("", private=True, skipCleanup=True): + self.doStorageWork() + + # verify cleanup was performed after crashing and restarting + # firefox with in_app=False + self.marionette.restart(in_app=False) + self.verifyCleanup() + + +class PBMAutoStart(PrivateRepositoryCleanup): + def setUp(self): + super(PBMAutoStart, self).setUp(True) + + def test_ensure_cleanup(self): + self.doStorageWork() + + # verify cleanup was performed at the shutdown + self.marionette.quit(in_app=True) + self.verifyCleanup() + + self.marionette.start_session() + + def test_ensure_cleanup_after_crash(self): + self.doStorageWork() + + # verify cleanup was performed after crashing and restarting + # firefox with in_app=False + self.marionette.restart(in_app=False) + self.verifyCleanup() diff --git a/dom/reporting/ReportDeliver.cpp b/dom/reporting/ReportDeliver.cpp index 08a31e57ee..b973868b6d 100644 --- a/dom/reporting/ReportDeliver.cpp +++ b/dom/reporting/ReportDeliver.cpp @@ -213,7 +213,7 @@ void SendReports(nsTArray<ReportDeliver::ReportData>& aReports, RefPtr<Request> request = new Request(globalObject, std::move(internalRequest), nullptr); - RequestOrUSVString fetchInput; + RequestOrUTF8String fetchInput; fetchInput.SetAsRequest() = request; RootedDictionary<RequestInit> requestInit(RootingCx()); diff --git a/dom/script/ScriptLoadContext.h b/dom/script/ScriptLoadContext.h index 74f7c6fe4f..d32553c4d4 100644 --- a/dom/script/ScriptLoadContext.h +++ b/dom/script/ScriptLoadContext.h @@ -147,7 +147,7 @@ class ScriptLoadContext : public JS::loader::LoadContextBase, static void PrioritizeAsPreload(nsIChannel* aChannel); - bool IsPreload() const; + bool IsPreload() const override; bool CompileStarted() const; diff --git a/dom/script/ScriptLoader.cpp b/dom/script/ScriptLoader.cpp index 4fa08e074d..5297a11f4a 100644 --- a/dom/script/ScriptLoader.cpp +++ b/dom/script/ScriptLoader.cpp @@ -1097,19 +1097,6 @@ bool ScriptLoader::ProcessExternalScript(nsIScriptElement* aElement, return false; } - if (request && request->IsModuleRequest() && - mModuleLoader->HasImportMapRegistered() && - request->mState > ScriptLoadRequest::State::Compiling) { - // We don't preload module scripts after seeing an import map but a script - // can dynamically insert an import map after preloading has happened. - // - // In the case of an import map is inserted after preloading has happened, - // We also check if the request has started loading imports, if not then we - // can reuse the preloaded request. - request->Cancel(); - request = nullptr; - } - if (request) { // Use the preload request. @@ -1399,6 +1386,16 @@ bool ScriptLoader::ProcessInlineScript(nsIScriptElement* aElement, return false; } + // Remove any module preloads. Module specifier resolution is invalidated by + // adding an import map, and incorrect dependencies may have been loaded. + mPreloads.RemoveElementsBy([](const PreloadInfo& info) { + if (info.mRequest->IsModuleRequest()) { + info.mRequest->Cancel(); + return true; + } + return false; + }); + // TODO: Bug 1781758: Move RegisterImportMap into EvaluateScriptElement. // // https://html.spec.whatwg.org/multipage/scripting.html#execute-the-script-element @@ -3297,21 +3294,7 @@ nsresult ScriptLoader::OnStreamComplete( // hash in case we are going to save the bytecode of this script in the // cache. if (aRequest->IsSource()) { - uint32_t sriLength = 0; - rv = SaveSRIHash(aRequest, aSRIDataVerifier, &sriLength); - JS::TranscodeBuffer& bytecode = aRequest->SRIAndBytecode(); - MOZ_ASSERT_IF(NS_SUCCEEDED(rv), bytecode.length() == sriLength); - - // TODO: (Bug 1800896) This code should be moved into SaveSRIHash, and the - // SRI out-param can be removed. - aRequest->SetSRILength(sriLength); - if (aRequest->GetSRILength() != sriLength) { - // The bytecode is aligned in the bytecode buffer, and space might be - // reserved for padding after the SRI hash. - if (!bytecode.resize(aRequest->GetSRILength())) { - return NS_ERROR_OUT_OF_MEMORY; - } - } + rv = SaveSRIHash(aRequest, aSRIDataVerifier); } if (NS_SUCCEEDED(rv)) { @@ -3373,14 +3356,13 @@ nsresult ScriptLoader::VerifySRI(ScriptLoadRequest* aRequest, return rv; } -nsresult ScriptLoader::SaveSRIHash(ScriptLoadRequest* aRequest, - SRICheckDataVerifier* aSRIDataVerifier, - uint32_t* sriLength) const { +nsresult ScriptLoader::SaveSRIHash( + ScriptLoadRequest* aRequest, SRICheckDataVerifier* aSRIDataVerifier) const { MOZ_ASSERT(aRequest->IsSource()); JS::TranscodeBuffer& bytecode = aRequest->SRIAndBytecode(); MOZ_ASSERT(bytecode.empty()); - uint32_t len; + uint32_t len = 0; // If the integrity metadata does not correspond to a valid hash function, // IsComplete would be false. @@ -3418,7 +3400,16 @@ nsresult ScriptLoader::SaveSRIHash(ScriptLoadRequest* aRequest, SRICheckDataVerifier::DataSummaryLength(len, bytecode.begin(), &srilen))); MOZ_ASSERT(srilen == len); - *sriLength = len; + MOZ_ASSERT(bytecode.length() == len); + aRequest->SetSRILength(len); + + if (aRequest->GetSRILength() != len) { + // The bytecode is aligned in the bytecode buffer, and space might be + // reserved for padding after the SRI hash. + if (!bytecode.resize(aRequest->GetSRILength())) { + return NS_ERROR_OUT_OF_MEMORY; + } + } return NS_OK; } diff --git a/dom/script/ScriptLoader.h b/dom/script/ScriptLoader.h index bdfd64b024..2fa5721d8c 100644 --- a/dom/script/ScriptLoader.h +++ b/dom/script/ScriptLoader.h @@ -566,8 +566,7 @@ class ScriptLoader final : public JS::loader::ScriptLoaderInterface { SRICheckDataVerifier* aSRIDataVerifier) const; nsresult SaveSRIHash(ScriptLoadRequest* aRequest, - SRICheckDataVerifier* aSRIDataVerifier, - uint32_t* sriLength) const; + SRICheckDataVerifier* aSRIDataVerifier) const; void ReportErrorToConsole(ScriptLoadRequest* aRequest, nsresult aResult) const override; diff --git a/dom/security/ReferrerInfo.cpp b/dom/security/ReferrerInfo.cpp index 565d4d3284..856aa90f12 100644 --- a/dom/security/ReferrerInfo.cpp +++ b/dom/security/ReferrerInfo.cpp @@ -1078,11 +1078,9 @@ ReferrerInfo::Equals(nsIReferrerInfo* aOther, bool* aResult) { } NS_IMETHODIMP -ReferrerInfo::GetComputedReferrerSpec(nsAString& aComputedReferrerSpec) { +ReferrerInfo::GetComputedReferrerSpec(nsACString& aComputedReferrerSpec) { aComputedReferrerSpec.Assign( - mComputedReferrer.isSome() - ? NS_ConvertUTF8toUTF16(mComputedReferrer.value()) - : EmptyString()); + mComputedReferrer.isSome() ? mComputedReferrer.value() : EmptyCString()); return NS_OK; } diff --git a/dom/security/ReferrerInfo.h b/dom/security/ReferrerInfo.h index b62afbb934..bfe254c1a9 100644 --- a/dom/security/ReferrerInfo.h +++ b/dom/security/ReferrerInfo.h @@ -14,7 +14,7 @@ #include "mozilla/HashFunctions.h" #include "mozilla/dom/ReferrerPolicyBinding.h" -#define REFERRERINFOF_CONTRACTID "@mozilla.org/referrer-info;1" +#define REFERRERINFO_CONTRACTID "@mozilla.org/referrer-info;1" // 041a129f-10ce-4bda-a60d-e027a26d5ed0 #define REFERRERINFO_CID \ { \ diff --git a/dom/security/fuzztest/csp_fuzzer.cpp b/dom/security/fuzztest/csp_fuzzer.cpp index 24f938cb1f..14e2ca05ca 100644 --- a/dom/security/fuzztest/csp_fuzzer.cpp +++ b/dom/security/fuzztest/csp_fuzzer.cpp @@ -27,7 +27,7 @@ static int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { if (ret != NS_OK) return 0; ret = - csp->SetRequestContextWithPrincipal(selfURIPrincipal, selfURI, u""_ns, 0); + csp->SetRequestContextWithPrincipal(selfURIPrincipal, selfURI, ""_ns, 0); if (ret != NS_OK) return 0; NS_ConvertASCIItoUTF16 policy(reinterpret_cast<const char*>(data), size); diff --git a/dom/security/nsCSPContext.cpp b/dom/security/nsCSPContext.cpp index ed44304484..de67e2bf1c 100644 --- a/dom/security/nsCSPContext.cpp +++ b/dom/security/nsCSPContext.cpp @@ -450,15 +450,14 @@ nsCSPContext::AppendPolicy(const nsAString& aPolicyString, bool aReportOnly, if (policy) { if (policy->hasDirective( nsIContentSecurityPolicy::UPGRADE_IF_INSECURE_DIRECTIVE)) { - nsAutoCString selfURIspec, referrer; + nsAutoCString selfURIspec; if (mSelfURI) { mSelfURI->GetAsciiSpec(selfURIspec); } - CopyUTF16toUTF8(mReferrer, referrer); CSPCONTEXTLOG( ("nsCSPContext::AppendPolicy added UPGRADE_IF_INSECURE_DIRECTIVE " "self-uri=%s referrer=%s", - selfURIspec.get(), referrer.get())); + selfURIspec.get(), mReferrer.get())); } mPolicies.AppendElement(policy); @@ -787,7 +786,7 @@ nsCSPContext::SetRequestContextWithDocument(Document* aDocument) { NS_IMETHODIMP nsCSPContext::SetRequestContextWithPrincipal(nsIPrincipal* aRequestPrincipal, nsIURI* aSelfURI, - const nsAString& aReferrer, + const nsACString& aReferrer, uint64_t aInnerWindowId) { NS_ENSURE_ARG(aRequestPrincipal); @@ -812,9 +811,8 @@ nsIPrincipal* nsCSPContext::GetRequestPrincipal() { return mLoadingPrincipal; } nsIURI* nsCSPContext::GetSelfURI() { return mSelfURI; } NS_IMETHODIMP -nsCSPContext::GetReferrer(nsAString& outReferrer) { - outReferrer.Truncate(); - outReferrer.Append(mReferrer); +nsCSPContext::GetReferrer(nsACString& outReferrer) { + outReferrer.Assign(mReferrer); return NS_OK; } @@ -987,7 +985,7 @@ nsresult nsCSPContext::GatherSecurityPolicyViolationEventData( CopyUTF8toUTF16(reportDocumentURI, aViolationEventInit.mDocumentURI); // referrer - aViolationEventInit.mReferrer = mReferrer; + CopyUTF8toUTF16(mReferrer, aViolationEventInit.mReferrer); // blocked-uri if (aBlockedURI) { diff --git a/dom/security/nsCSPContext.h b/dom/security/nsCSPContext.h index ae47e34cb3..e4fe5af315 100644 --- a/dom/security/nsCSPContext.h +++ b/dom/security/nsCSPContext.h @@ -175,7 +175,7 @@ class nsCSPContext : public nsIContentSecurityPolicy { uint32_t aViolatedPolicyIndex, uint32_t aLineNumber, uint32_t aColumnNumber); - nsString mReferrer; + nsCString mReferrer; uint64_t mInnerWindowID; // used for web console logging bool mSkipAllowInlineStyleCheck; // used to allow Devtools to edit styles // When deserializing an nsCSPContext instance, we initially just keep the diff --git a/dom/security/nsCSPParser.cpp b/dom/security/nsCSPParser.cpp index 2559367831..07812470a3 100644 --- a/dom/security/nsCSPParser.cpp +++ b/dom/security/nsCSPParser.cpp @@ -936,14 +936,6 @@ nsCSPDirective* nsCSPParser::directiveName() { // directive = *WSP [ directive-name [ WSP directive-value ] ] void nsCSPParser::directive() { - // Set the directiveName to mCurToken - // Remember, the directive name is stored at index 0 - mCurToken = mCurDir[0]; - - CSPPARSERLOG(("nsCSPParser::directive, mCurToken: %s, mCurValue: %s", - NS_ConvertUTF16toUTF8(mCurToken).get(), - NS_ConvertUTF16toUTF8(mCurValue).get())); - // Make sure that the directive-srcs-array contains at least // one directive. if (mCurDir.Length() == 0) { @@ -953,6 +945,14 @@ void nsCSPParser::directive() { return; } + // Set the directiveName to mCurToken + // Remember, the directive name is stored at index 0 + mCurToken = mCurDir[0]; + + CSPPARSERLOG(("nsCSPParser::directive, mCurToken: %s, mCurValue: %s", + NS_ConvertUTF16toUTF8(mCurToken).get(), + NS_ConvertUTF16toUTF8(mCurValue).get())); + if (CSP_IsEmptyDirective(mCurValue, mCurToken)) { return; } @@ -1029,20 +1029,32 @@ void nsCSPParser::directive() { srcs.InsertElementAt(0, keyword); } + MaybeWarnAboutIgnoredSources(srcs); + MaybeWarnAboutUnsafeInline(*cspDir); + MaybeWarnAboutUnsafeEval(*cspDir); + + // Add the newly created srcs to the directive and add the directive to the + // policy + cspDir->addSrcs(srcs); + mPolicy->addDirective(cspDir); +} + +void nsCSPParser::MaybeWarnAboutIgnoredSources( + const nsTArray<nsCSPBaseSrc*>& aSrcs) { // If policy contains 'strict-dynamic' warn about ignored sources. if (mStrictDynamic && !CSP_IsDirective(mCurDir[0], nsIContentSecurityPolicy::DEFAULT_SRC_DIRECTIVE)) { - for (uint32_t i = 0; i < srcs.Length(); i++) { + for (uint32_t i = 0; i < aSrcs.Length(); i++) { nsAutoString srcStr; - srcs[i]->toString(srcStr); + aSrcs[i]->toString(srcStr); // Hashes and nonces continue to apply with 'strict-dynamic', as well as // 'unsafe-eval', 'wasm-unsafe-eval' and 'unsafe-hashes'. - if (!srcs[i]->isKeyword(CSP_STRICT_DYNAMIC) && - !srcs[i]->isKeyword(CSP_UNSAFE_EVAL) && - !srcs[i]->isKeyword(CSP_WASM_UNSAFE_EVAL) && - !srcs[i]->isKeyword(CSP_UNSAFE_HASHES) && !srcs[i]->isNonce() && - !srcs[i]->isHash()) { + if (!aSrcs[i]->isKeyword(CSP_STRICT_DYNAMIC) && + !aSrcs[i]->isKeyword(CSP_UNSAFE_EVAL) && + !aSrcs[i]->isKeyword(CSP_WASM_UNSAFE_EVAL) && + !aSrcs[i]->isKeyword(CSP_UNSAFE_HASHES) && !aSrcs[i]->isNonce() && + !aSrcs[i]->isHash()) { AutoTArray<nsString, 2> params = {srcStr, mCurDir[0]}; logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringScriptSrcForStrictDynamic", params); @@ -1057,37 +1069,37 @@ void nsCSPParser::directive() { "strictDynamicButNoHashOrNonce", params); } } +} +void nsCSPParser::MaybeWarnAboutUnsafeInline(const nsCSPDirective& aDirective) { // From https://w3c.github.io/webappsec-csp/#allow-all-inline // follows that when either a hash or nonce is specified, 'unsafe-inline' // should not apply. if (mHasHashOrNonce && mUnsafeInlineKeywordSrc && - (cspDir->isDefaultDirective() || - cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) || - cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) || - cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE) || - cspDir->equals(nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE) || - cspDir->equals(nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE) || - cspDir->equals(nsIContentSecurityPolicy::STYLE_SRC_ATTR_DIRECTIVE))) { + (aDirective.isDefaultDirective() || + aDirective.equals(nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) || + aDirective.equals(nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) || + aDirective.equals(nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE) || + aDirective.equals(nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE) || + aDirective.equals(nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE) || + aDirective.equals(nsIContentSecurityPolicy::STYLE_SRC_ATTR_DIRECTIVE))) { // Log to the console that unsafe-inline will be ignored. AutoTArray<nsString, 2> params = {u"'unsafe-inline'"_ns, mCurDir[0]}; logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringSrcWithinNonceOrHashDirective", params); } +} +void nsCSPParser::MaybeWarnAboutUnsafeEval(const nsCSPDirective& aDirective) { if (mHasAnyUnsafeEval && - (cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) || - cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE))) { + (aDirective.equals(nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) || + aDirective.equals( + nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE))) { // Log to the console that (wasm-)unsafe-eval will be ignored. AutoTArray<nsString, 1> params = {mCurDir[0]}; logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringUnsafeEval", params); } - - // Add the newly created srcs to the directive and add the directive to the - // policy - cspDir->addSrcs(srcs); - mPolicy->addDirective(cspDir); } // policy = [ directive *( ";" [ directive ] ) ] diff --git a/dom/security/nsCSPParser.h b/dom/security/nsCSPParser.h index 21679d86a0..28c24440d0 100644 --- a/dom/security/nsCSPParser.h +++ b/dom/security/nsCSPParser.h @@ -139,6 +139,10 @@ class nsCSPParser { void logWarningErrorToConsole(uint32_t aSeverityFlag, const char* aProperty, const nsTArray<nsString>& aParams); + void MaybeWarnAboutIgnoredSources(const nsTArray<nsCSPBaseSrc*>& aSrcs); + void MaybeWarnAboutUnsafeInline(const nsCSPDirective& aDirective); + void MaybeWarnAboutUnsafeEval(const nsCSPDirective& aDirective); + /** * When parsing the policy, the parser internally uses the following helper * variables/members which are used/reset during parsing. The following diff --git a/dom/security/nsContentSecurityUtils.cpp b/dom/security/nsContentSecurityUtils.cpp index d2c1b257bc..7bcbbdd002 100644 --- a/dom/security/nsContentSecurityUtils.cpp +++ b/dom/security/nsContentSecurityUtils.cpp @@ -1065,7 +1065,7 @@ nsresult CheckCSPFrameAncestorPolicy(nsIChannel* aChannel, csp->SuppressParserLogMessages(); nsCOMPtr<nsIURI> selfURI; - nsAutoString referrerSpec; + nsAutoCString referrerSpec; if (httpChannel) { aChannel->GetURI(getter_AddRefs(selfURI)); nsCOMPtr<nsIReferrerInfo> referrerInfo = httpChannel->GetReferrerInfo(); @@ -1367,6 +1367,7 @@ void nsContentSecurityUtils::AssertAboutPageHasCSP(Document* aDocument) { StringBeginsWith(aboutSpec, "about:preferences"_ns) || StringBeginsWith(aboutSpec, "about:settings"_ns) || StringBeginsWith(aboutSpec, "about:downloads"_ns) || + StringBeginsWith(aboutSpec, "about:fingerprinting"_ns) || StringBeginsWith(aboutSpec, "about:asrouter"_ns) || StringBeginsWith(aboutSpec, "about:newtab"_ns) || StringBeginsWith(aboutSpec, "about:logins"_ns) || diff --git a/dom/security/nsHTTPSOnlyUtils.cpp b/dom/security/nsHTTPSOnlyUtils.cpp index 535efaba4e..31c7408a37 100644 --- a/dom/security/nsHTTPSOnlyUtils.cpp +++ b/dom/security/nsHTTPSOnlyUtils.cpp @@ -398,8 +398,7 @@ bool nsHTTPSOnlyUtils::ShouldUpgradeHttpsFirstRequest(nsIURI* aURI, // 4. Don't upgrade if upgraded previously or exempt from upgrades uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus(); - if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST || - httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) { + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) { return false; } @@ -619,6 +618,18 @@ nsHTTPSOnlyUtils::PotentiallyDowngradeHttpsFirstRequest( duration); } } + + nsresult channelStatus; + channel->GetStatus(&channelStatus); + if (channelStatus == NS_ERROR_NET_TIMEOUT_EXTERNAL) { + if (loadInfo->GetWasSchemelessInput() && + !nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(isPrivateWin)) { + mozilla::glean::httpsfirst::downgraded_on_timer_schemeless + .AddToNumerator(); + } else { + mozilla::glean::httpsfirst::downgraded_on_timer.AddToNumerator(); + } + } } } @@ -891,6 +902,13 @@ bool nsHTTPSOnlyUtils::IsEqualURIExceptSchemeAndRef(nsIURI* aHTTPSSchemeURI, return uriEquals; } + +/* static */ +uint32_t nsHTTPSOnlyUtils::GetStatusForSubresourceLoad( + uint32_t aHttpsOnlyStatus) { + return aHttpsOnlyStatus & ~nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST; +} + ///////////////////////////////////////////////////////////////////// // Implementation of TestHTTPAnswerRunnable @@ -992,19 +1010,6 @@ TestHTTPAnswerRunnable::OnStartRequest(nsIRequest* aRequest) { nsresult httpsOnlyChannelStatus; httpsOnlyChannel->GetStatus(&httpsOnlyChannelStatus); if (httpsOnlyChannelStatus == NS_OK) { - bool isPrivateWin = - loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; - if (!nsHTTPSOnlyUtils::IsHttpsOnlyModeEnabled(isPrivateWin)) { - // Record HTTPS-First Telemetry - if (loadInfo->GetWasSchemelessInput() && - !nsHTTPSOnlyUtils::IsHttpsFirstModeEnabled(isPrivateWin)) { - mozilla::glean::httpsfirst::downgraded_on_timer_schemeless - .AddToNumerator(); - } else { - mozilla::glean::httpsfirst::downgraded_on_timer.AddToNumerator(); - } - } - httpsOnlyChannel->Cancel(NS_ERROR_NET_TIMEOUT_EXTERNAL); } } diff --git a/dom/security/nsHTTPSOnlyUtils.h b/dom/security/nsHTTPSOnlyUtils.h index 7e36bfadbd..775fbf39e0 100644 --- a/dom/security/nsHTTPSOnlyUtils.h +++ b/dom/security/nsHTTPSOnlyUtils.h @@ -164,6 +164,18 @@ class nsHTTPSOnlyUtils { nsIURI* aOtherURI, nsILoadInfo* aLoadInfo); + /** + * Determines which HTTPS-Only status flags should get propagated to + * sub-resources or sub-documents. As sub-resources and sub-documents are + * exempt when the top-level document is exempt, we need to copy the "exempt" + * flag. The HTTPS-First "upgraded" flag should not be copied to prevent a + * unwanted downgrade (Bug 1885949). + * @param aHttpsOnlyStatus The HTTPS-Only status of the top-level document. + * @return The HTTPS-Only status that the sub-resource/document should + * receive. + */ + static uint32_t GetStatusForSubresourceLoad(uint32_t aHttpsOnlyStatus); + private: /** * Checks if it can be ruled out that the error has something diff --git a/dom/security/test/general/file_block_script_wrong_mime_sw.js b/dom/security/test/general/file_block_script_wrong_mime_sw.js new file mode 100644 index 0000000000..4d8d667af4 --- /dev/null +++ b/dom/security/test/general/file_block_script_wrong_mime_sw.js @@ -0,0 +1,51 @@ +/** + * Service Worker that runs in 2 modes: 1) direct pass-through via + * fetch(event.request) and 2) indirect pass-through via + * fetch(event.request.url). + * + * Because this is updating a pre-existing mochitest that didn't use a SW and + * used a single test document, we use a SW idiom where the SW claims the + * existing window client. And because we operate in two modes and we + * parameterize via URL, we also ensure that we skipWaiting. + **/ + +/* eslint-env serviceworker */ + +// We are parameterized by "mode". +const params = new URLSearchParams(location.search); +const fetchMode = params.get("fetchMode"); + +// When activating on initial install, claim the existing window client. +// For synchronziation, also message the controlled document to report our mode. +self.addEventListener("activate", event => { + event.waitUntil( + (async () => { + await clients.claim(); + const allClients = await clients.matchAll(); + for (const client of allClients) { + client.postMessage({ + fetchMode, + }); + } + })() + ); +}); + +// When updating the SW to change our mode of operation, skipWaiting so we +// advance directly to activating without waiting for the test window client +// to stop being controlled by our previous configuration. +self.addEventListener("install", () => { + self.skipWaiting(); +}); + +self.addEventListener("fetch", event => { + switch (fetchMode) { + case "direct": + event.respondWith(fetch(event.request)); + break; + + case "indirect": + event.respondWith(fetch(event.request.url)); + break; + } +}); diff --git a/dom/security/test/general/mochitest.toml b/dom/security/test/general/mochitest.toml index c46b5ecf57..22024fcc67 100644 --- a/dom/security/test/general/mochitest.toml +++ b/dom/security/test/general/mochitest.toml @@ -8,6 +8,7 @@ support-files = [ "file_block_toplevel_data_navigation2.html", "file_block_toplevel_data_navigation3.html", "file_block_toplevel_data_redirect.sjs", + "file_block_script_wrong_mime_sw.js", "file_block_subresource_redir_to_data.sjs", "file_same_site_cookies_subrequest.sjs", "file_same_site_cookies_toplevel_nav.sjs", diff --git a/dom/security/test/general/test_block_script_wrong_mime.html b/dom/security/test/general/test_block_script_wrong_mime.html index 7122363dfc..896823a417 100644 --- a/dom/security/test/general/test_block_script_wrong_mime.html +++ b/dom/security/test/general/test_block_script_wrong_mime.html @@ -29,7 +29,7 @@ function testScript([mime, shouldLoad]) { let script = document.createElement("script"); script.onload = () => { document.body.removeChild(script); - ok(shouldLoad, `script with mime '${mime}' should load`); + ok(shouldLoad, `script with mime '${mime}' should ${shouldLoad ? "" : "NOT "}load`); resolve(); }; script.onerror = () => { @@ -47,7 +47,7 @@ function testWorker([mime, shouldLoad]) { return new Promise((resolve) => { let worker = new Worker("file_block_script_wrong_mime_server.sjs?type=worker&mime="+mime); worker.onmessage = (event) => { - ok(shouldLoad, `worker with mime '${mime}' should load`) + ok(shouldLoad, `worker with mime '${mime}' should ${shouldLoad ? "" : "NOT "}load`); is(event.data, "worker-loaded", "worker should send correct message"); resolve(); }; @@ -65,7 +65,7 @@ function testWorkerImportScripts([mime, shouldLoad]) { return new Promise((resolve) => { let worker = new Worker("file_block_script_wrong_mime_server.sjs?type=worker-import&mime="+mime); worker.onmessage = (event) => { - ok(shouldLoad, `worker/importScripts with mime '${mime}' should load`) + ok(shouldLoad, `worker/importScripts with mime '${mime}' should ${shouldLoad ? "" : "NOT "}load`); is(event.data, "worker-loaded", "worker should send correct message"); resolve(); }; @@ -73,20 +73,103 @@ function testWorkerImportScripts([mime, shouldLoad]) { ok(!shouldLoad, `worker/importScripts with wrong mime '${mime}' should be blocked`); error.preventDefault(); resolve(); + // The worker doesn't self-terminate via close, so let's do it. + worker.terminate(); } worker.postMessage("dummy"); }); } -SimpleTest.waitForExplicitFinish(); -Promise.all(MIMETypes.map(testScript)).then(() => { - return Promise.all(MIMETypes.map(testWorker)); -}).then(() => { - return Promise.all(MIMETypes.map(testWorkerImportScripts)); -}).then(() => { - return SpecialPowers.popPrefEnv(); -}).then(SimpleTest.finish); +async function runMimeTypePermutations() { + info("### Running document script MIME checks."); + for (const mimeType of MIMETypes) { + await testScript(mimeType); + } + info("### Running worker top-level script MIME checks."); + for (const mimeType of MIMETypes) { + await testWorker(mimeType); + } + + info("### Running worker importScripts MIME checks."); + for (const mimeType of MIMETypes) { + await testWorkerImportScripts(mimeType); + } +} + +let gRegistration; + +/** + * Register and wait for the helper ServiceWorker to be active in the given + * mode. + */ +async function useServiceWorker({ fetchMode }) { + info(`### Registering ServiceWorker with mode '${fetchMode}'`); + const activePromise = new Promise((resolve, reject) => { + navigator.serviceWorker.addEventListener( + "message", + event => { + if (event.data.fetchMode === fetchMode) { + resolve(); + } else { + reject(`wrong fetchMode: ${fetchMode}`); + } + is(fetchMode, event.data.fetchMode, "right fetch mode"); + }, + { once: true }); + }); + + const reg = gRegistration = await navigator.serviceWorker.register( + `file_block_script_wrong_mime_sw.js?fetchMode=${fetchMode}`); + info("register resolved. " + + `installing: ${!!reg.installing} ` + + `waiting: ${!!reg.waiting} ` + + `active: ${!!reg.active}`); + + await activePromise; +} + +/** + * Unregister the ServiceWorker, with the caveat that the ServiceWorker will + * still be controlling us until this window goes away. + */ +async function cleanupServiceWorkerWithCaveat() { + await gRegistration.unregister(); +} + +/** + * Top-level test that runs the MIME type checks in different ServiceWorker/ + * network configurations. + * + * We use the ServiceWorker mechanism that allows ServiceWorkers to claim + * existing scope-matching clients in order to make this window controlled and + * then run the tests. When changing the SW behavior the SW also needs to + * skipWaiting in order to advance to active. + */ +async function runNetworkPermutations() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + info("## Run tests without a ServiceWorker involved."); + await runMimeTypePermutations(); + + info("## Run tests with a pass-through fetch(event.request) handler."); + await useServiceWorker({ fetchMode: "direct" }); + await runMimeTypePermutations(); + + info("## Run tests with a naive URL propagating fetch(event.request.url) handler."); + await useServiceWorker({ fetchMode: "indirect" }); + await runMimeTypePermutations(); + + await cleanupServiceWorkerWithCaveat(); +} + +add_task(runNetworkPermutations); </script> </body> </html> diff --git a/dom/security/test/gtest/TestCSPParser.cpp b/dom/security/test/gtest/TestCSPParser.cpp index b8a4e986b6..19ba0548de 100644 --- a/dom/security/test/gtest/TestCSPParser.cpp +++ b/dom/security/test/gtest/TestCSPParser.cpp @@ -93,8 +93,7 @@ nsresult runTest( // for testing the parser we only need to set a principal which is needed // to translate the keyword 'self' into an actual URI. - rv = - csp->SetRequestContextWithPrincipal(selfURIPrincipal, selfURI, u""_ns, 0); + rv = csp->SetRequestContextWithPrincipal(selfURIPrincipal, selfURI, ""_ns, 0); NS_ENSURE_SUCCESS(rv, rv); // append a policy diff --git a/dom/security/test/https-first/browser.toml b/dom/security/test/https-first/browser.toml index 0c63b8317d..49e2d522f4 100644 --- a/dom/security/test/https-first/browser.toml +++ b/dom/security/test/https-first/browser.toml @@ -7,7 +7,7 @@ support-files = ["file_beforeunload_permit_http.html"] support-files = [ "file_mixed_content_auto_upgrade.html", "pass.png", - "test.ogv", + "test.webm", "test.wav", ] @@ -40,6 +40,12 @@ support-files = [ ["browser_navigation.js"] support-files = ["file_navigation.html"] +["browser_subdocument_downgrade.js"] +support-files = [ + "file_empty.html", + "file_subdocument_downgrade.sjs", +] + ["browser_schemeless.js"] ["browser_slow_download.js"] diff --git a/dom/security/test/https-first/browser_beforeunload_permit_http.js b/dom/security/test/https-first/browser_beforeunload_permit_http.js index 660c1a352d..281def37e9 100644 --- a/dom/security/test/https-first/browser_beforeunload_permit_http.js +++ b/dom/security/test/https-first/browser_beforeunload_permit_http.js @@ -162,7 +162,7 @@ async function loadPageAndReload(testCase) { } ); is(true, hasInteractedWith, "Simulated successfully user interaction"); - BrowserReloadWithFlags(testCase.reloadFlag); + BrowserCommands.reloadWithFlags(testCase.reloadFlag); await BrowserTestUtils.browserLoaded(browser); is(true, true, `reload with flag ${testCase.name} was successful`); } diff --git a/dom/security/test/https-first/browser_subdocument_downgrade.js b/dom/security/test/https-first/browser_subdocument_downgrade.js new file mode 100644 index 0000000000..4cb5b4ed2e --- /dev/null +++ b/dom/security/test/https-first/browser_subdocument_downgrade.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const EMPTY_URL = + "http://example.com/browser/dom/security/test/https-first/file_empty.html"; +const SUBDOCUMENT_URL = + "https://example.com/browser/dom/security/test/https-first/file_subdocument_downgrade.sjs"; + +add_task(async function test_subdocument_downgrade() { + await SpecialPowers.pushPrefEnv({ + set: [ + // We want to test HTTPS-First + ["dom.security.https_first", true], + // Makes it easier to detect the error + ["security.mixed_content.block_active_content", false], + ], + }); + + // Open a empty document with origin http://example.com, which gets upgraded + // to https://example.com by HTTPS-First and thus is marked as + // HTTPS_ONLY_UPGRADED_HTTPS_FIRST. + await BrowserTestUtils.withNewTab(EMPTY_URL, async browser => { + await SpecialPowers.spawn( + browser, + [SUBDOCUMENT_URL], + async SUBDOCUMENT_URL => { + function isCrossOriginIframe(iframe) { + try { + return !iframe.contentDocument; + } catch (e) { + return true; + } + } + const subdocument = content.document.createElement("iframe"); + // We open https://example.com/.../file_subdocument_downgrade.sjs in a + // iframe, which sends a invalid response if the scheme is https. Thus + // we should get an error. But if we accidentally copy the + // HTTPS_ONLY_UPGRADED_HTTPS_FIRST flag from the parent into the iframe + // loadinfo, HTTPS-First will try to downgrade the iframe. We test that + // this doesn't happen. + subdocument.src = SUBDOCUMENT_URL; + const loadPromise = new Promise(resolve => { + subdocument.addEventListener("load", () => { + ok( + // If the iframe got downgraded, it should now have the origin + // http://example.com, which we can detect as being cross-origin. + !isCrossOriginIframe(subdocument), + "Subdocument should not be downgraded" + ); + resolve(); + }); + }); + content.document.body.appendChild(subdocument); + await loadPromise; + } + ); + }); +}); diff --git a/dom/security/test/https-first/file_empty.html b/dom/security/test/https-first/file_empty.html new file mode 100644 index 0000000000..39d495653e --- /dev/null +++ b/dom/security/test/https-first/file_empty.html @@ -0,0 +1 @@ +<!doctype html><html><body></body></html> diff --git a/dom/security/test/https-first/file_mixed_content_auto_upgrade.html b/dom/security/test/https-first/file_mixed_content_auto_upgrade.html index 7dda8909a5..5a8bef6bb0 100644 --- a/dom/security/test/https-first/file_mixed_content_auto_upgrade.html +++ b/dom/security/test/https-first/file_mixed_content_auto_upgrade.html @@ -6,7 +6,7 @@ <body> <!--upgradeable resources---> <img src="http://example.com/browser/dom/security/test/https-first/pass.png"> - <video src="http://example.com/browser/dom/security/test/https-first/test.ogv"> + <video src="http://example.com/browser/dom/security/test/https-first/test.webm"> <audio src="http://example.com/browser/dom/security/test/https-first/test.wav"> </body> </html> diff --git a/dom/security/test/https-first/file_multiple_redirection.sjs b/dom/security/test/https-first/file_multiple_redirection.sjs index 49098ccdb7..e34a360fa6 100644 --- a/dom/security/test/https-first/file_multiple_redirection.sjs +++ b/dom/security/test/https-first/file_multiple_redirection.sjs @@ -5,6 +5,8 @@ const REDIRECT_URI = "https://example.com/tests/dom/security/test/https-first/file_multiple_redirection.sjs?redirect"; const REDIRECT_URI_HTTP = "http://example.com/tests/dom/security/test/https-first/file_multiple_redirection.sjs?verify"; +const OTHERHOST_REDIRECT_URI_HTTP = + "http://example.org/tests/dom/security/test/https-first/file_multiple_redirection.sjs?verify"; const REDIRECT_URI_HTTPS = "https://example.com/tests/dom/security/test/https-first/file_multiple_redirection.sjs?verify"; @@ -44,6 +46,11 @@ function sendRedirection(query, response) { if (query.includes("test3")) { response.setHeader("Strict-Transport-Security", "max-age=60"); response.setHeader("Location", REDIRECT_URI_HTTP, false); + return; + } + // send a redirection to a different http uri + if (query.includes("test4")) { + response.setHeader("Location", OTHERHOST_REDIRECT_URI_HTTP, false); } } @@ -53,6 +60,11 @@ function handleRequest(request, response) { // if the query contains a test query start first test if (query.startsWith("test")) { + // all of these should be upgraded + if (request.scheme !== "https") { + response.setStatusLine(request.httpVersion, 500, "OK"); + response.write("Request should have been HTTPS."); + } // send a 302 redirection response.setStatusLine(request.httpVersion, 302, "Found"); response.setHeader("Location", REDIRECT_URI + query, false); @@ -60,6 +72,10 @@ function handleRequest(request, response) { } // Send a redirection if (query.includes("redirect")) { + if (request.scheme !== "https") { + response.setStatusLine(request.httpVersion, 500, "OK"); + response.write("Request should have been HTTPS."); + } response.setStatusLine(request.httpVersion, 302, "Found"); sendRedirection(query, response); return; @@ -83,5 +99,5 @@ function handleRequest(request, response) { // We should never get here, but just in case ... response.setStatusLine(request.httpVersion, 500, "OK"); - response.write("unexepcted query"); + response.write("unexpected query"); } diff --git a/dom/security/test/https-first/file_subdocument_downgrade.sjs b/dom/security/test/https-first/file_subdocument_downgrade.sjs new file mode 100644 index 0000000000..53ced94ba8 --- /dev/null +++ b/dom/security/test/https-first/file_subdocument_downgrade.sjs @@ -0,0 +1,8 @@ +function handleRequest(request, response) { + if (request.scheme === "https") { + response.setStatusLine("1.1", 429, "Too Many Requests"); + } else { + response.setHeader("Content-Type", "text/html", false); + response.write("<!doctype html><html><body></body></html>"); + } +} diff --git a/dom/security/test/https-first/test.ogv b/dom/security/test/https-first/test.ogv Binary files differdeleted file mode 100644 index 0f83996e5d..0000000000 --- a/dom/security/test/https-first/test.ogv +++ /dev/null diff --git a/dom/security/test/https-first/test.webm b/dom/security/test/https-first/test.webm Binary files differnew file mode 100644 index 0000000000..221877e303 --- /dev/null +++ b/dom/security/test/https-first/test.webm diff --git a/dom/security/test/https-first/test_multiple_redirection.html b/dom/security/test/https-first/test_multiple_redirection.html index d631f140e6..678a8133a8 100644 --- a/dom/security/test/https-first/test_multiple_redirection.html +++ b/dom/security/test/https-first/test_multiple_redirection.html @@ -37,6 +37,12 @@ Test multiple redirects using https-first and ensure the entire redirect chain i {name: "test last redirect HSTS", result: "scheme-https", query: "test3"}, // reset: reset hsts header for example.com {name: "reset HSTS header", result: "scheme-https", query: "reset"}, + // test 4: http://example.com/...test4 -upgrade-> httpS://example.com/...test4 + // https://example.com/...test4 -redir-> https://example.com/.../REDIRECT + // https://example.com/.../redirect -redir-> http://example.ORG/.../verify + // http://example.org/.../verify -upgrade-> httpS://example.ORG/.../verify + // Everything should be upgraded and accessed only via HTTPS! + {name: "test last redirect other HTTP origin gets upgraded", result: "scheme-https", query: "test4" }, ] let currentTest = 0; let testWin; @@ -48,7 +54,7 @@ Test multiple redirects using https-first and ensure the entire redirect chain i let test = testCase[currentTest]; is(event.data.result, test.result, - "same-origin redirect results in " + test.name + "redirect results in " + test.name ); testWin.close(); if (++currentTest < testCase.length) { diff --git a/dom/security/test/mixedcontentblocker/browser.toml b/dom/security/test/mixedcontentblocker/browser.toml index 5b0b85cb0b..402e8b91b1 100644 --- a/dom/security/test/mixedcontentblocker/browser.toml +++ b/dom/security/test/mixedcontentblocker/browser.toml @@ -15,7 +15,7 @@ support-files = [ support-files = [ "file_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.html", "pass.png", - "test.ogv", + "test.webm", "test.wav", ] diff --git a/dom/security/test/mixedcontentblocker/browser_mixed_content_auth_download.js b/dom/security/test/mixedcontentblocker/browser_mixed_content_auth_download.js index 25fee8de3c..57842eb623 100644 --- a/dom/security/test/mixedcontentblocker/browser_mixed_content_auth_download.js +++ b/dom/security/test/mixedcontentblocker/browser_mixed_content_auth_download.js @@ -12,10 +12,6 @@ const { PromptTestUtils } = ChromeUtils.importESModule( "resource://testing-common/PromptTestUtils.sys.mjs" ); -let authPromptModalType = Services.prefs.getIntPref( - "prompts.modalType.httpAuth" -); - const downloadMonitoringView = { _listeners: [], onDownloadAdded(download) { @@ -107,7 +103,7 @@ async function runTest(url, link, checkFunction, description) { // Wait for the auth prompt, enter the login details and close the prompt await PromptTestUtils.handleNextPrompt( gBrowser.selectedBrowser, - { modalType: authPromptModalType, promptType: "promptUserAndPass" }, + { modalType: Ci.nsIPrompt.MODAL_TYPE_TAB, promptType: "promptUserAndPass" }, { buttonNumClick: 0, loginInput: "user", passwordInput: "pass" } ); await checkPromise; diff --git a/dom/security/test/mixedcontentblocker/file_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.html b/dom/security/test/mixedcontentblocker/file_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.html index 80e97443ed..62e705227f 100644 --- a/dom/security/test/mixedcontentblocker/file_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.html +++ b/dom/security/test/mixedcontentblocker/file_csp_block_all_mixedcontent_and_mixed_content_display_upgrade.html @@ -8,7 +8,7 @@ <body> <!--upgradeable resources---> <img id="some-img" src="http://test1.example.com/browser/dom/security/test/mixedcontentblocker/pass.png" width="100px"> - <video id="some-video" src="http://test1.example.com/browser/dom/security/test/mixedcontentblocker/test.ogv" width="100px"> + <video id="some-video" src="http://test1.example.com/browser/dom/security/test/mixedcontentblocker/test.webm" width="100px"> <audio id="some-audio" src="http://test1.example.com/browser/dom/security/test/mixedcontentblocker/test.wav" width="100px"> </body> </html> diff --git a/dom/security/test/mixedcontentblocker/file_server.sjs b/dom/security/test/mixedcontentblocker/file_server.sjs index 4f86c282ee..90034dad3f 100644 --- a/dom/security/test/mixedcontentblocker/file_server.sjs +++ b/dom/security/test/mixedcontentblocker/file_server.sjs @@ -96,8 +96,8 @@ function handleRequest(request, response) { break; case "media": - response.setHeader("Content-Type", "video/ogg", false); - response.write(loadContentFromFile("tests/dom/media/test/320x240.ogv")); + response.setHeader("Content-Type", "video/webm", false); + response.write(loadContentFromFile("tests/dom/media/test/vp9.webm")); break; case "iframe": diff --git a/dom/security/test/mixedcontentblocker/mochitest.toml b/dom/security/test/mixedcontentblocker/mochitest.toml index 17d8cb4608..cf1d4827a0 100644 --- a/dom/security/test/mixedcontentblocker/mochitest.toml +++ b/dom/security/test/mixedcontentblocker/mochitest.toml @@ -16,7 +16,9 @@ support-files = [ "file_main_bug803225.html", "file_main_bug803225_websocket_wsh.py", "file_server.sjs", - "!/dom/media/test/320x240.ogv", + "!/dom/media/test/vp9.webm", + "test.webm", + "test.wav", "!/image/test/mochitest/blue.png", "file_redirect.html", "file_redirect_handler.sjs", diff --git a/dom/security/test/mixedcontentblocker/test.ogv b/dom/security/test/mixedcontentblocker/test.ogv Binary files differdeleted file mode 100644 index 0f83996e5d..0000000000 --- a/dom/security/test/mixedcontentblocker/test.ogv +++ /dev/null diff --git a/dom/security/test/mixedcontentblocker/test.webm b/dom/security/test/mixedcontentblocker/test.webm Binary files differnew file mode 100644 index 0000000000..221877e303 --- /dev/null +++ b/dom/security/test/mixedcontentblocker/test.webm diff --git a/dom/security/test/referrer-policy/browser.toml b/dom/security/test/referrer-policy/browser.toml index a77046c85b..ba571fec81 100644 --- a/dom/security/test/referrer-policy/browser.toml +++ b/dom/security/test/referrer-policy/browser.toml @@ -1,9 +1,10 @@ [DEFAULT] support-files = ["referrer_page.sjs"] -["browser_session_history.js"] -support-files = ["file_session_history.sjs"] - ["browser_referrer_disallow_cross_site_relaxing.js"] +skip-if = ["asan"] # too slow ["browser_referrer_telemetry.js"] + +["browser_session_history.js"] +support-files = ["file_session_history.sjs"] diff --git a/dom/security/test/referrer-policy/browser_referrer_disallow_cross_site_relaxing.js b/dom/security/test/referrer-policy/browser_referrer_disallow_cross_site_relaxing.js index 7f8df7b34b..84e79af3ef 100644 --- a/dom/security/test/referrer-policy/browser_referrer_disallow_cross_site_relaxing.js +++ b/dom/security/test/referrer-policy/browser_referrer_disallow_cross_site_relaxing.js @@ -191,6 +191,8 @@ add_setup(async function () { set: [ // Disable mixed content blocking to be able to test downgrade scenario. ["security.mixed_content.block_active_content", false], + // Disable https-first since we are testing http and https referrers + ["dom.security.https_first", false], ], }); }); diff --git a/dom/security/trusted-types/TrustedTypePolicy.cpp b/dom/security/trusted-types/TrustedTypePolicy.cpp index 3c4e758ed0..62c58ae1f6 100644 --- a/dom/security/trusted-types/TrustedTypePolicy.cpp +++ b/dom/security/trusted-types/TrustedTypePolicy.cpp @@ -6,38 +6,98 @@ #include "mozilla/dom/TrustedTypePolicy.h" -#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/dom/DOMString.h" #include "mozilla/dom/TrustedTypePolicyFactory.h" #include "mozilla/dom/TrustedTypesBinding.h" +#include <utility> + namespace mozilla::dom { -NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TrustedTypePolicy, mParentObject) +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TrustedTypePolicy, mParentObject, + mOptions.mCreateHTMLCallback, + mOptions.mCreateScriptCallback, + mOptions.mCreateScriptURLCallback) + +TrustedTypePolicy::TrustedTypePolicy(TrustedTypePolicyFactory* aParentObject, + const nsAString& aName, Options&& aOptions) + : mParentObject{aParentObject}, + mName{aName}, + mOptions{std::move(aOptions)} {} JSObject* TrustedTypePolicy::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { return TrustedTypePolicy_Binding::Wrap(aCx, this, aGivenProto); } -UniquePtr<TrustedHTML> TrustedTypePolicy::CreateHTML( - JSContext* aJSContext, const nsAString& aInput, - const Sequence<JS::Value>& aArguments) const { - // TODO: implement the spec. - return MakeUnique<TrustedHTML>(); +void TrustedTypePolicy::GetName(DOMString& aResult) const { + aResult.SetKnownLiveString(mName); } -UniquePtr<TrustedScript> TrustedTypePolicy::CreateScript( - JSContext* aJSContext, const nsAString& aInput, - const Sequence<JS::Value>& aArguments) const { - // TODO: implement the spec. - return MakeUnique<TrustedScript>(); +#define IMPL_CREATE_TRUSTED_TYPE(_trustedTypeSuffix) \ + UniquePtr<Trusted##_trustedTypeSuffix> \ + TrustedTypePolicy::Create##_trustedTypeSuffix( \ + JSContext* aJSContext, const nsAString& aInput, \ + const Sequence<JS::Value>& aArguments, ErrorResult& aErrorResult) \ + const { \ + /* Invoking the callback could delete the policy and hence the callback. \ + * Hence keep a strong reference to it on the stack. \ + */ \ + RefPtr<Create##_trustedTypeSuffix##Callback> callbackObject = \ + mOptions.mCreate##_trustedTypeSuffix##Callback; \ + \ + return CreateTrustedType<Trusted##_trustedTypeSuffix>( \ + callbackObject, aInput, aArguments, aErrorResult); \ + } + +IMPL_CREATE_TRUSTED_TYPE(HTML) +IMPL_CREATE_TRUSTED_TYPE(Script) +IMPL_CREATE_TRUSTED_TYPE(ScriptURL) + +template <typename T, typename CallbackObject> +UniquePtr<T> TrustedTypePolicy::CreateTrustedType( + const RefPtr<CallbackObject>& aCallbackObject, const nsAString& aValue, + const Sequence<JS::Value>& aArguments, ErrorResult& aErrorResult) const { + nsString policyValue; + DetermineTrustedPolicyValue(aCallbackObject, aValue, aArguments, true, + aErrorResult, policyValue); + + if (aErrorResult.Failed()) { + return nullptr; + } + + UniquePtr<T> trustedObject = MakeUnique<T>(std::move(policyValue)); + + // TODO: add special handling for `TrustedScript` when default policy support + // is added. + + return trustedObject; } -UniquePtr<TrustedScriptURL> TrustedTypePolicy::CreateScriptURL( - JSContext* aJSContext, const nsAString& aInput, - const Sequence<JS::Value>& aArguments) const { - // TODO: implement the spec. - return MakeUnique<TrustedScriptURL>(); +template <typename CallbackObject> +void TrustedTypePolicy::DetermineTrustedPolicyValue( + const RefPtr<CallbackObject>& aCallbackObject, const nsAString& aValue, + const Sequence<JS::Value>& aArguments, bool aThrowIfMissing, + ErrorResult& aErrorResult, nsAString& aResult) const { + if (!aCallbackObject) { + // The spec lacks a definition for stringifying null, see + // <https://github.com/w3c/trusted-types/issues/469>. + aResult = EmptyString(); + + if (aThrowIfMissing) { + aErrorResult.ThrowTypeError("Function missing."); + } + + return; + } + + nsString callbackResult; + aCallbackObject->Call(aValue, aArguments, callbackResult, aErrorResult, + nullptr, CallbackObject::eRethrowExceptions); + + if (!aErrorResult.Failed()) { + aResult = std::move(callbackResult); + } } } // namespace mozilla::dom diff --git a/dom/security/trusted-types/TrustedTypePolicy.h b/dom/security/trusted-types/TrustedTypePolicy.h index 22d99947b3..d677088285 100644 --- a/dom/security/trusted-types/TrustedTypePolicy.h +++ b/dom/security/trusted-types/TrustedTypePolicy.h @@ -9,19 +9,20 @@ #include "js/TypeDecls.h" #include "js/Value.h" +#include "mozilla/Attributes.h" #include "mozilla/RefPtr.h" #include "mozilla/UniquePtr.h" #include "mozilla/dom/BindingDeclarations.h" -#include "mozilla/dom/DOMString.h" #include "mozilla/dom/TrustedHTML.h" #include "mozilla/dom/TrustedScript.h" #include "mozilla/dom/TrustedScriptURL.h" #include "nsISupportsImpl.h" -#include "nsStringFwd.h" +#include "nsString.h" #include "nsWrapperCache.h" namespace mozilla::dom { +class DOMString; class TrustedTypePolicyFactory; // https://w3c.github.io/trusted-types/dist/spec/#trusted-type-policy @@ -30,8 +31,14 @@ class TrustedTypePolicy : public nsWrapperCache { NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(TrustedTypePolicy) NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(TrustedTypePolicy) - explicit TrustedTypePolicy(TrustedTypePolicyFactory* aParentObject) - : mParentObject{aParentObject} {} + struct Options { + RefPtr<CreateHTMLCallback> mCreateHTMLCallback; + RefPtr<CreateScriptCallback> mCreateScriptCallback; + RefPtr<CreateScriptURLCallback> mCreateScriptURLCallback; + }; + + TrustedTypePolicy(TrustedTypePolicyFactory* aParentObject, + const nsAString& aName, Options&& aOptions); // Required for Web IDL binding. TrustedTypePolicyFactory* GetParentObject() const { return mParentObject; } @@ -41,30 +48,46 @@ class TrustedTypePolicy : public nsWrapperCache { JS::Handle<JSObject*> aGivenProto) override; // https://w3c.github.io/trusted-types/dist/spec/#trustedtypepolicy-name - void GetName(DOMString& aResult) const { - // TODO: impl. - } + void GetName(DOMString& aResult) const; // https://w3c.github.io/trusted-types/dist/spec/#dom-trustedtypepolicy-createhtml - UniquePtr<TrustedHTML> CreateHTML( + MOZ_CAN_RUN_SCRIPT UniquePtr<TrustedHTML> CreateHTML( JSContext* aJSContext, const nsAString& aInput, - const Sequence<JS::Value>& aArguments) const; + const Sequence<JS::Value>& aArguments, ErrorResult& aErrorResult) const; // https://w3c.github.io/trusted-types/dist/spec/#dom-trustedtypepolicy-createscript - UniquePtr<TrustedScript> CreateScript( + MOZ_CAN_RUN_SCRIPT UniquePtr<TrustedScript> CreateScript( JSContext* aJSContext, const nsAString& aInput, - const Sequence<JS::Value>& aArguments) const; + const Sequence<JS::Value>& aArguments, ErrorResult& aErrorResult) const; // https://w3c.github.io/trusted-types/dist/spec/#dom-trustedtypepolicy-createscripturl - UniquePtr<TrustedScriptURL> CreateScriptURL( + MOZ_CAN_RUN_SCRIPT UniquePtr<TrustedScriptURL> CreateScriptURL( JSContext* aJSContext, const nsAString& aInput, - const Sequence<JS::Value>& aArguments) const; + const Sequence<JS::Value>& aArguments, ErrorResult& aErrorResult) const; private: // Required because this class is ref-counted. virtual ~TrustedTypePolicy() = default; + // https://w3c.github.io/trusted-types/dist/spec/#abstract-opdef-create-a-trusted-type + template <typename T, typename CallbackObject> + MOZ_CAN_RUN_SCRIPT UniquePtr<T> CreateTrustedType( + const RefPtr<CallbackObject>& aCallbackObject, const nsAString& aValue, + const Sequence<JS::Value>& aArguments, ErrorResult& aErrorResult) const; + + // https://w3c.github.io/trusted-types/dist/spec/#abstract-opdef-get-trusted-type-policy-value + // + // @param aResult may become void. + template <typename CallbackObject> + MOZ_CAN_RUN_SCRIPT void DetermineTrustedPolicyValue( + const RefPtr<CallbackObject>& aCallbackObject, const nsAString& aValue, + const Sequence<JS::Value>& aArguments, bool aThrowIfMissing, + ErrorResult& aErrorResult, nsAString& aResult) const; RefPtr<TrustedTypePolicyFactory> mParentObject; + + const nsString mName; + + Options mOptions; }; } // namespace mozilla::dom diff --git a/dom/security/trusted-types/TrustedTypePolicyFactory.cpp b/dom/security/trusted-types/TrustedTypePolicyFactory.cpp index 448c51eb3b..c2544124e3 100644 --- a/dom/security/trusted-types/TrustedTypePolicyFactory.cpp +++ b/dom/security/trusted-types/TrustedTypePolicyFactory.cpp @@ -22,10 +22,50 @@ JSObject* TrustedTypePolicyFactory::WrapObject( already_AddRefed<TrustedTypePolicy> TrustedTypePolicyFactory::CreatePolicy( const nsAString& aPolicyName, const TrustedTypePolicyOptions& aPolicyOptions) { - // TODO: implement the spec. - return MakeRefPtr<TrustedTypePolicy>(this).forget(); + // TODO: add CSP support. + + // TODO: add default policy support; this requires accessing the default + // policy on the C++ side, hence already now ref-counting policy + // objects. + + TrustedTypePolicy::Options options; + + if (aPolicyOptions.mCreateHTML.WasPassed()) { + options.mCreateHTMLCallback = &aPolicyOptions.mCreateHTML.Value(); + } + + if (aPolicyOptions.mCreateScript.WasPassed()) { + options.mCreateScriptCallback = &aPolicyOptions.mCreateScript.Value(); + } + + if (aPolicyOptions.mCreateScriptURL.WasPassed()) { + options.mCreateScriptURLCallback = &aPolicyOptions.mCreateScriptURL.Value(); + } + + RefPtr<TrustedTypePolicy> policy = + MakeRefPtr<TrustedTypePolicy>(this, aPolicyName, std::move(options)); + + mCreatedPolicyNames.AppendElement(aPolicyName); + + return policy.forget(); } +#define IS_TRUSTED_TYPE_IMPL(_trustedTypeSuffix) \ + bool TrustedTypePolicyFactory::Is##_trustedTypeSuffix( \ + JSContext*, const JS::Handle<JS::Value>& aValue) const { \ + /** \ + * No need to check the internal slot. \ + * Ensured by the corresponding test: \ + * <https://searchfox.org/mozilla-central/rev/b60cb73160843adb5a5a3ec8058e75a69b46acf7/testing/web-platform/tests/trusted-types/TrustedTypePolicyFactory-isXXX.html> \ + */ \ + return aValue.isObject() && \ + IS_INSTANCE_OF(Trusted##_trustedTypeSuffix, &aValue.toObject()); \ + } + +IS_TRUSTED_TYPE_IMPL(HTML); +IS_TRUSTED_TYPE_IMPL(Script); +IS_TRUSTED_TYPE_IMPL(ScriptURL); + UniquePtr<TrustedHTML> TrustedTypePolicyFactory::EmptyHTML() { // Preserving the wrapper ensures: // ``` @@ -37,14 +77,14 @@ UniquePtr<TrustedHTML> TrustedTypePolicyFactory::EmptyHTML() { // multiple emptyHML objects. Both, the JS- and the C++-objects. dom::PreserveWrapper(this); - return MakeUnique<TrustedHTML>(); + return MakeUnique<TrustedHTML>(EmptyString()); } UniquePtr<TrustedScript> TrustedTypePolicyFactory::EmptyScript() { // See the explanation in `EmptyHTML()`. dom::PreserveWrapper(this); - return MakeUnique<TrustedScript>(); + return MakeUnique<TrustedScript>(EmptyString()); } } // namespace mozilla::dom diff --git a/dom/security/trusted-types/TrustedTypePolicyFactory.h b/dom/security/trusted-types/TrustedTypePolicyFactory.h index fea5312cf8..61dae94ed9 100644 --- a/dom/security/trusted-types/TrustedTypePolicyFactory.h +++ b/dom/security/trusted-types/TrustedTypePolicyFactory.h @@ -8,13 +8,16 @@ #define DOM_SECURITY_TRUSTED_TYPES_TRUSTEDTYPEPOLICYFACTORY_H_ #include "js/TypeDecls.h" +#include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/TrustedHTML.h" #include "mozilla/dom/TrustedScript.h" +#include "mozilla/dom/TrustedScriptURL.h" #include "mozilla/RefPtr.h" #include "mozilla/UniquePtr.h" #include "nsIGlobalObject.h" #include "nsISupportsImpl.h" #include "nsStringFwd.h" +#include "nsTArray.h" #include "nsWrapperCache.h" template <typename T> @@ -48,25 +51,15 @@ class TrustedTypePolicyFactory : public nsWrapperCache { const TrustedTypePolicyOptions& aPolicyOptions); // https://w3c.github.io/trusted-types/dist/spec/#dom-trustedtypepolicyfactory-ishtml - bool IsHTML(JSContext* aJSContext, - const JS::Handle<JS::Value>& aValue) const { - // TODO: impl. - return false; - } + bool IsHTML(JSContext* aJSContext, const JS::Handle<JS::Value>& aValue) const; // https://w3c.github.io/trusted-types/dist/spec/#dom-trustedtypepolicyfactory-isscript bool IsScript(JSContext* aJSContext, - const JS::Handle<JS::Value>& aValue) const { - // TODO: impl. - return false; - } + const JS::Handle<JS::Value>& aValue) const; // https://w3c.github.io/trusted-types/dist/spec/#dom-trustedtypepolicyfactory-isscripturl bool IsScriptURL(JSContext* aJSContext, - const JS::Handle<JS::Value>& aValue) const { - // TODO: impl. - return false; - } + const JS::Handle<JS::Value>& aValue) const; // https://w3c.github.io/trusted-types/dist/spec/#dom-trustedtypepolicyfactory-emptyhtml UniquePtr<TrustedHTML> EmptyHTML(); @@ -98,6 +91,8 @@ class TrustedTypePolicyFactory : public nsWrapperCache { virtual ~TrustedTypePolicyFactory() = default; RefPtr<nsIGlobalObject> mGlobalObject; + + nsTArray<nsString> mCreatedPolicyNames; }; } // namespace mozilla::dom diff --git a/dom/security/trusted-types/TrustedTypeUtils.h b/dom/security/trusted-types/TrustedTypeUtils.h index 90ffc50c38..508c26c3c2 100644 --- a/dom/security/trusted-types/TrustedTypeUtils.h +++ b/dom/security/trusted-types/TrustedTypeUtils.h @@ -10,20 +10,26 @@ #include "mozilla/dom/DOMString.h" #include "mozilla/dom/NonRefcountedDOMObject.h" #include "mozilla/dom/TrustedTypesBinding.h" -#include "nsStringFwd.h" +#include "nsString.h" #define DECL_TRUSTED_TYPE_CLASS(_class) \ class _class : public mozilla::dom::NonRefcountedDOMObject { \ public: \ + explicit _class(const nsAString& aData) : mData{aData} {} \ + \ /* Required for Web IDL binding. */ \ bool WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, \ JS::MutableHandle<JSObject*> aObject); \ \ - void Stringify(nsAString& aResult) const { /* TODO: impl. */ \ + void Stringify(DOMString& aResult) const { \ + aResult.SetKnownLiveString(mData); \ } \ \ - void ToJSON(DOMString& aResult) const { /* TODO: impl. */ \ + void ToJSON(DOMString& aResult) const { \ + aResult.SetKnownLiveString(mData); \ } \ + \ + const nsString mData; \ }; #define IMPL_TRUSTED_TYPE_CLASS(_class) \ diff --git a/dom/serializers/nsDocumentEncoder.cpp b/dom/serializers/nsDocumentEncoder.cpp index 14120bae64..46ea15fecc 100644 --- a/dom/serializers/nsDocumentEncoder.cpp +++ b/dom/serializers/nsDocumentEncoder.cpp @@ -1270,8 +1270,14 @@ nsresult nsDocumentEncoder::RangeSerializer::SerializeRangeToString( const nsRange* aRange) { if (!aRange || aRange->Collapsed()) return NS_OK; + // Consider a case where the boundary of the selection is ShadowRoot (ie, the + // first child of ShadowRoot is selected, so ShadowRoot is the container hence + // the boundary), allowing GetClosestCommonInclusiveAncestor to cross the + // boundary can return the host element as the container. + // SerializeRangeContextStart doesn't support this case. mClosestCommonInclusiveAncestorOfRange = - aRange->GetClosestCommonInclusiveAncestor(); + aRange->GetClosestCommonInclusiveAncestor( + AllowRangeCrossShadowBoundary::No); if (!mClosestCommonInclusiveAncestorOfRange) { return NS_OK; diff --git a/dom/serviceworkers/ServiceWorkerPrivate.cpp b/dom/serviceworkers/ServiceWorkerPrivate.cpp index 864a598006..664874d991 100644 --- a/dom/serviceworkers/ServiceWorkerPrivate.cpp +++ b/dom/serviceworkers/ServiceWorkerPrivate.cpp @@ -324,7 +324,7 @@ Result<IPCInternalRequest, nsresult> GetIPCInternalRequest( RequestCredentials requestCredentials = InternalRequest::MapChannelToRequestCredentials(underlyingChannel); - nsAutoString referrer; + nsAutoCString referrer; ReferrerPolicy referrerPolicy = ReferrerPolicy::_empty; ReferrerPolicy environmentReferrerPolicy = ReferrerPolicy::_empty; @@ -544,10 +544,11 @@ nsresult ServiceWorkerPrivate::Initialize() { nsAutoString scheme; nsAutoString pkBaseDomain; int32_t unused; + bool unused2; if (OriginAttributes::ParsePartitionKey( principal->OriginAttributesRef().mPartitionKey, scheme, - pkBaseDomain, unused)) { + pkBaseDomain, unused, unused2)) { nsCOMPtr<nsIURI> firstPartyURI; rv = NS_NewURI(getter_AddRefs(firstPartyURI), scheme + u"://"_ns + pkBaseDomain); @@ -592,7 +593,8 @@ nsresult ServiceWorkerPrivate::Initialize() { } } } else { - net::CookieJarSettings::Cast(cookieJarSettings)->SetPartitionKey(uri); + net::CookieJarSettings::Cast(cookieJarSettings) + ->SetPartitionKey(uri, false); // The service worker is for a first-party context, we can use the uri of // the service worker as the first-party domain to get the fingerprinting diff --git a/dom/serviceworkers/ServiceWorkerScriptCache.cpp b/dom/serviceworkers/ServiceWorkerScriptCache.cpp index f1569ffe5f..00b69ebf45 100644 --- a/dom/serviceworkers/ServiceWorkerScriptCache.cpp +++ b/dom/serviceworkers/ServiceWorkerScriptCache.cpp @@ -114,7 +114,7 @@ class CompareNetwork final : public nsIStreamLoaderObserver, MOZ_ASSERT(NS_IsMainThread()); } - nsresult Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, + nsresult Initialize(nsIPrincipal* aPrincipal, const nsACString& aURL, Cache* const aCache); void Abort(); @@ -123,7 +123,7 @@ class CompareNetwork final : public nsIStreamLoaderObserver, void CacheFinish(nsresult aRv); - const nsString& URL() const { + const nsCString& URL() const { MOZ_ASSERT(NS_IsMainThread()); return mURL; } @@ -164,7 +164,7 @@ class CompareNetwork final : public nsIStreamLoaderObserver, nsCOMPtr<nsIChannel> mChannel; nsString mBuffer; - nsString mURL; + nsCString mURL; ChannelInfo mChannelInfo; RefPtr<InternalHeaders> mInternalHeaders; UniquePtr<PrincipalInfo> mPrincipalInfo; @@ -204,7 +204,7 @@ class CompareCache final : public PromiseNativeHandler, MOZ_ASSERT(NS_IsMainThread()); } - nsresult Initialize(Cache* const aCache, const nsAString& aURL); + nsresult Initialize(Cache* const aCache, const nsACString& aURL); void Finish(nsresult aStatus, bool aInCache); @@ -231,7 +231,7 @@ class CompareCache final : public PromiseNativeHandler, RefPtr<CompareNetwork> mCN; nsCOMPtr<nsIInputStreamPump> mPump; - nsString mURL; + nsCString mURL; nsString mBuffer; enum { @@ -262,7 +262,7 @@ class CompareManager final : public PromiseNativeHandler { MOZ_ASSERT(aRegistration); } - nsresult Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, + nsresult Initialize(nsIPrincipal* aPrincipal, const nsACString& aURL, const nsAString& aCacheName); void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, @@ -326,7 +326,7 @@ class CompareManager final : public PromiseNativeHandler { void Cleanup(); - nsresult FetchScript(const nsAString& aURL, bool aIsMainScript, + nsresult FetchScript(const nsACString& aURL, bool aIsMainScript, Cache* const aCache = nullptr) { MOZ_ASSERT(NS_IsMainThread()); @@ -364,7 +364,7 @@ class CompareManager final : public PromiseNativeHandler { return; } - Optional<RequestOrUSVString> request; + Optional<RequestOrUTF8String> request; CacheQueryOptions options; ErrorResult error; RefPtr<Promise> promise = mOldCache->Keys(aCx, request, options, error); @@ -407,7 +407,7 @@ class CompareManager final : public PromiseNativeHandler { mState = WaitingForScriptOrComparisonResult; bool hasMainScript = false; - AutoTArray<nsString, 8> urlList; + AutoTArray<nsCString, 8> urlList; // Extract the list of URLs in the old cache. for (uint32_t i = 0; i < len; ++i) { @@ -423,14 +423,14 @@ class CompareManager final : public PromiseNativeHandler { return; }; - nsString url; + nsCString url; request->GetUrl(url); if (!hasMainScript && url == mURL) { hasMainScript = true; } - urlList.AppendElement(url); + urlList.AppendElement(std::move(url)); } // If the main script is missing, then something has gone wrong. We @@ -565,8 +565,8 @@ class CompareManager final : public PromiseNativeHandler { RefPtr<Response> response = new Response(aCache->GetGlobalObject(), std::move(ir), nullptr); - RequestOrUSVString request; - request.SetAsUSVString().ShareOrDependUpon(aCN->URL()); + RequestOrUTF8String request; + request.SetAsUTF8String().ShareOrDependUpon(aCN->URL()); // For now we have to wait until the Put Promise is fulfilled before we can // continue since Cache does not yet support starting a read that is being @@ -592,7 +592,7 @@ class CompareManager final : public PromiseNativeHandler { nsTArray<RefPtr<CompareNetwork>> mCNList; - nsString mURL; + nsCString mURL; RefPtr<nsIPrincipal> mPrincipal; // Used for the old cache where saves the old source scripts. @@ -622,7 +622,7 @@ class CompareManager final : public PromiseNativeHandler { NS_IMPL_ISUPPORTS0(CompareManager) nsresult CompareNetwork::Initialize(nsIPrincipal* aPrincipal, - const nsAString& aURL, + const nsACString& aURL, Cache* const aCache) { MOZ_ASSERT(aPrincipal); MOZ_ASSERT(NS_IsMainThread()); @@ -634,7 +634,7 @@ nsresult CompareNetwork::Initialize(nsIPrincipal* aPrincipal, } mURL = aURL; - mURLList.AppendElement(NS_ConvertUTF16toUTF8(mURL)); + mURLList.AppendElement(mURL); nsCOMPtr<nsILoadGroup> loadGroup; rv = NS_NewLoadGroup(getter_AddRefs(loadGroup), aPrincipal); @@ -678,7 +678,8 @@ nsresult CompareNetwork::Initialize(nsIPrincipal* aPrincipal, net::CookieJarSettings::Cast(cookieJarSettings) ->SetPartitionKey(aPrincipal->OriginAttributesRef().mPartitionKey); } else { - net::CookieJarSettings::Cast(cookieJarSettings)->SetPartitionKey(uri); + net::CookieJarSettings::Cast(cookieJarSettings) + ->SetPartitionKey(uri, false); } // Note that because there is no "serviceworker" RequestContext type, we can @@ -1044,7 +1045,7 @@ CompareNetwork::OnStreamComplete(nsIStreamLoader* aLoader, ServiceWorkerManager::LocalizeAndReportToAllClients( mRegistration->Scope(), "ServiceWorkerRegisterNetworkError", nsTArray<nsString>{NS_ConvertUTF8toUTF16(mRegistration->Scope()), - statusAsText, mURL}); + statusAsText, NS_ConvertUTF8toUTF16(mURL)}); rv = NS_ERROR_FAILURE; return NS_OK; @@ -1077,7 +1078,8 @@ CompareNetwork::OnStreamComplete(nsIStreamLoader* aLoader, ServiceWorkerManager::LocalizeAndReportToAllClients( mRegistration->Scope(), "ServiceWorkerRegisterMimeTypeError2", nsTArray<nsString>{NS_ConvertUTF8toUTF16(mRegistration->Scope()), - NS_ConvertUTF8toUTF16(mimeType), mURL}); + NS_ConvertUTF8toUTF16(mimeType), + NS_ConvertUTF8toUTF16(mURL)}); rv = NS_ERROR_DOM_SECURITY_ERR; return rv; } @@ -1114,7 +1116,7 @@ CompareNetwork::OnStreamComplete(nsIStreamLoader* aLoader, return NS_OK; } -nsresult CompareCache::Initialize(Cache* const aCache, const nsAString& aURL) { +nsresult CompareCache::Initialize(Cache* const aCache, const nsACString& aURL) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aCache); MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForInitialization); @@ -1124,8 +1126,8 @@ nsresult CompareCache::Initialize(Cache* const aCache, const nsAString& aURL) { AutoJSAPI jsapi; jsapi.Init(); - RequestOrUSVString request; - request.SetAsUSVString().ShareOrDependUpon(aURL); + RequestOrUTF8String request; + request.SetAsUTF8String().ShareOrDependUpon(aURL); ErrorResult error; CacheQueryOptions params; RefPtr<Promise> promise = aCache->Match(jsapi.cx(), request, params, error); @@ -1293,7 +1295,7 @@ void CompareCache::ManageValueResult(JSContext* aCx, } nsresult CompareManager::Initialize(nsIPrincipal* aPrincipal, - const nsAString& aURL, + const nsACString& aURL, const nsAString& aCacheName) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); @@ -1488,7 +1490,7 @@ nsresult GenerateCacheName(nsAString& aName) { nsresult Compare(ServiceWorkerRegistrationInfo* aRegistration, nsIPrincipal* aPrincipal, const nsAString& aCacheName, - const nsAString& aURL, CompareCallback* aCallback) { + const nsACString& aURL, CompareCallback* aCallback) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aRegistration); MOZ_ASSERT(aPrincipal); diff --git a/dom/serviceworkers/ServiceWorkerScriptCache.h b/dom/serviceworkers/ServiceWorkerScriptCache.h index 5d71840b46..8c71c42b1a 100644 --- a/dom/serviceworkers/ServiceWorkerScriptCache.h +++ b/dom/serviceworkers/ServiceWorkerScriptCache.h @@ -45,7 +45,7 @@ class CompareCallback { nsresult Compare(ServiceWorkerRegistrationInfo* aRegistration, nsIPrincipal* aPrincipal, const nsAString& aCacheName, - const nsAString& aURL, CompareCallback* aCallback); + const nsACString& aURL, CompareCallback* aCallback); } // namespace serviceWorkerScriptCache diff --git a/dom/serviceworkers/ServiceWorkerUpdateJob.cpp b/dom/serviceworkers/ServiceWorkerUpdateJob.cpp index a6726fbf47..25f86e164d 100644 --- a/dom/serviceworkers/ServiceWorkerUpdateJob.cpp +++ b/dom/serviceworkers/ServiceWorkerUpdateJob.cpp @@ -287,8 +287,7 @@ void ServiceWorkerUpdateJob::Update() { RefPtr<CompareCallback> callback = new CompareCallback(this); nsresult rv = serviceWorkerScriptCache::Compare( - mRegistration, mPrincipal, cacheName, NS_ConvertUTF8toUTF16(mScriptSpec), - callback); + mRegistration, mPrincipal, cacheName, mScriptSpec, callback); if (NS_WARN_IF(NS_FAILED(rv))) { FailUpdateJob(rv); return; diff --git a/dom/smil/SMILAnimationController.cpp b/dom/smil/SMILAnimationController.cpp index 99abc07b58..be180e96f6 100644 --- a/dom/smil/SMILAnimationController.cpp +++ b/dom/smil/SMILAnimationController.cpp @@ -264,11 +264,8 @@ void SMILAnimationController::DoSample(bool aSkipUnchangedContainers) { return; } - bool isStyleFlushNeeded = mResampleNeeded; mResampleNeeded = false; - nsCOMPtr<Document> document(mDocument); // keeps 'this' alive too - // Set running sample flag -- do this before flushing styles so that when we // flush styles we don't end up requesting extra samples AutoRestore<bool> autoRestoreRunningSample(mRunningSample); @@ -327,8 +324,7 @@ void SMILAnimationController::DoSample(bool aSkipUnchangedContainers) { for (SVGAnimationElement* animElem : mAnimationElementTable.Keys()) { SampleTimedElement(animElem, &activeContainers); - AddAnimationToCompositorTable(animElem, currentCompositorTable.get(), - isStyleFlushNeeded); + AddAnimationToCompositorTable(animElem, currentCompositorTable.get()); animElems.AppendElement(animElem); } activeContainers.Clear(); @@ -375,15 +371,6 @@ void SMILAnimationController::DoSample(bool aSkipUnchangedContainers) { return; } - if (isStyleFlushNeeded) { - document->FlushPendingNotifications(FlushType::Style); - } - - // WARNING: - // WARNING: the above flush may have destroyed the pres shell and/or - // WARNING: frames and other layout related objects. - // WARNING: - // STEP 5: Compose currently-animated attributes. // XXXdholbert: This step traverses our animation targets in an effectively // random order. For animation from/to 'inherit' values to work correctly @@ -533,8 +520,7 @@ void SMILAnimationController::SampleTimedElement( /*static*/ void SMILAnimationController::AddAnimationToCompositorTable( - SVGAnimationElement* aElement, SMILCompositorTable* aCompositorTable, - bool& aStyleFlushNeeded) { + SVGAnimationElement* aElement, SMILCompositorTable* aCompositorTable) { // Add a compositor to the hash table if there's not already one there SMILTargetIdentifier key; if (!GetTargetIdentifierForAnimation(aElement, key)) @@ -550,7 +536,6 @@ void SMILAnimationController::AddAnimationToCompositorTable( // Look up the compositor for our target, & add our animation function // to its list of animation functions. SMILCompositor* result = aCompositorTable->PutEntry(key); - aStyleFlushNeeded |= func.ValueNeedsReparsingEverySample(); result->AddAnimationFunction(&func); } else if (func.HasChanged()) { @@ -560,7 +545,6 @@ void SMILAnimationController::AddAnimationToCompositorTable( // it's got HasChanged() == true), so we need to make sure to recompose // its target. SMILCompositor* result = aCompositorTable->PutEntry(key); - aStyleFlushNeeded |= func.ValueNeedsReparsingEverySample(); result->ToggleForceCompositing(); // We've now made sure that |func|'s inactivity will be reflected as of @@ -614,23 +598,20 @@ bool SMILAnimationController::GetTargetIdentifierForAnimation( return true; } -bool SMILAnimationController::PreTraverse() { - return PreTraverseInSubtree(nullptr); -} +void SMILAnimationController::PreTraverse() { PreTraverseInSubtree(nullptr); } -bool SMILAnimationController::PreTraverseInSubtree(Element* aRoot) { +void SMILAnimationController::PreTraverseInSubtree(Element* aRoot) { MOZ_ASSERT(NS_IsMainThread()); if (!mMightHavePendingStyleUpdates) { - return false; + return; } nsPresContext* context = mDocument->GetPresContext(); if (!context) { - return false; + return; } - bool foundElementsNeedingRestyle = false; for (SVGAnimationElement* animElement : mAnimationElementTable.Keys()) { SMILTargetIdentifier key; if (!GetTargetIdentifierForAnimation(animElement, key)) { @@ -647,8 +628,6 @@ bool SMILAnimationController::PreTraverseInSubtree(Element* aRoot) { context->RestyleManager()->PostRestyleEventForAnimations( key.mElement, PseudoStyleType::NotPseudo, RestyleHint::RESTYLE_SMIL); - - foundElementsNeedingRestyle = true; } // Only clear the mMightHavePendingStyleUpdates flag if we definitely posted @@ -656,8 +635,6 @@ bool SMILAnimationController::PreTraverseInSubtree(Element* aRoot) { if (!aRoot) { mMightHavePendingStyleUpdates = false; } - - return foundElementsNeedingRestyle; } //---------------------------------------------------------------------- diff --git a/dom/smil/SMILAnimationController.h b/dom/smil/SMILAnimationController.h index 04f2e34cf3..498ab06170 100644 --- a/dom/smil/SMILAnimationController.h +++ b/dom/smil/SMILAnimationController.h @@ -107,8 +107,8 @@ class SMILAnimationController final : public SMILTimeContainer, return mMightHavePendingStyleUpdates; } - bool PreTraverse(); - bool PreTraverseInSubtree(mozilla::dom::Element* aRoot); + void PreTraverse(); + void PreTraverseInSubtree(mozilla::dom::Element* aRoot); protected: ~SMILAnimationController(); @@ -144,7 +144,7 @@ class SMILAnimationController final : public SMILTimeContainer, static void AddAnimationToCompositorTable( mozilla::dom::SVGAnimationElement* aElement, - SMILCompositorTable* aCompositorTable, bool& aStyleFlushNeeded); + SMILCompositorTable* aCompositorTable); static bool GetTargetIdentifierForAnimation( mozilla::dom::SVGAnimationElement* aAnimElem, diff --git a/dom/storage/LocalStorageCache.cpp b/dom/storage/LocalStorageCache.cpp index 4b55482441..16ebe8ca3b 100644 --- a/dom/storage/LocalStorageCache.cpp +++ b/dom/storage/LocalStorageCache.cpp @@ -37,8 +37,11 @@ inline uint32_t GetDataSetIndex(bool aPrivateBrowsing, } inline uint32_t GetDataSetIndex(const LocalStorage* aStorage) { + // A session only mode doesn't exist anymore, so having a separate data set + // for it here is basically useless. This code is only kept until we remove + // the old / legacy LocalStorage implementation. return GetDataSetIndex(aStorage->IsPrivateBrowsing(), - aStorage->IsSessionScopedOrLess()); + aStorage->IsPrivateBrowsingOrLess()); } } // namespace @@ -167,8 +170,8 @@ void LocalStorageCache::NotifyObservers(const LocalStorage* aStorage, } inline bool LocalStorageCache::Persist(const LocalStorage* aStorage) const { - return mPersistent && - (aStorage->IsPrivateBrowsing() || !aStorage->IsSessionScopedOrLess()); + return mPersistent && (aStorage->IsPrivateBrowsing() || + !aStorage->IsPrivateBrowsingOrLess()); } const nsCString LocalStorageCache::Origin() const { diff --git a/dom/storage/Storage.cpp b/dom/storage/Storage.cpp index cfc609969c..3602dcb9b1 100644 --- a/dom/storage/Storage.cpp +++ b/dom/storage/Storage.cpp @@ -38,18 +38,18 @@ Storage::Storage(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal, mPrincipal(aPrincipal), mStoragePrincipal(aStoragePrincipal), mPrivateBrowsing(false), - mSessionScopedOrLess(false) { + mPrivateBrowsingOrLess(false) { MOZ_ASSERT(aPrincipal); if (mPrincipal->IsSystemPrincipal()) { mPrivateBrowsing = false; - mSessionScopedOrLess = false; + mPrivateBrowsingOrLess = false; } else if (mWindow) { uint32_t rejectedReason = 0; StorageAccess access = StorageAllowedForWindow(mWindow, &rejectedReason); mPrivateBrowsing = access == StorageAccess::ePrivateBrowsing; - mSessionScopedOrLess = access <= StorageAccess::eSessionScoped; + mPrivateBrowsingOrLess = access <= StorageAccess::ePrivateBrowsing; } } diff --git a/dom/storage/Storage.h b/dom/storage/Storage.h index 81546a0b0e..cea49f3114 100644 --- a/dom/storage/Storage.h +++ b/dom/storage/Storage.h @@ -52,7 +52,7 @@ class Storage : public nsISupports, public nsWrapperCache { bool IsPrivateBrowsing() const { return mPrivateBrowsing; } - bool IsSessionScopedOrLess() const { return mSessionScopedOrLess; } + bool IsPrivateBrowsingOrLess() const { return mPrivateBrowsingOrLess; } // WebIDL JSObject* WrapObject(JSContext* aCx, @@ -98,11 +98,6 @@ class Storage : public nsISupports, public nsWrapperCache { virtual void Clear(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) = 0; - // The attribute in the WebIDL interface has rather confusing name. So we - // shouldn't use this method internally. IsSessionScopedOrLess should be used - // directly. - bool IsSessionOnly() const { return IsSessionScopedOrLess(); } - ////////////////////////////////////////////////////////////////////////////// // Testing Methods: // @@ -172,7 +167,7 @@ class Storage : public nsISupports, public nsWrapperCache { // Whether storage is set to persist data only per session, may change // dynamically and is set by CanUseStorage function that is called // before any operation on the storage. - bool mSessionScopedOrLess : 1; + bool mPrivateBrowsingOrLess : 1; }; } // namespace mozilla::dom diff --git a/dom/streams/TransformStream.cpp b/dom/streams/TransformStream.cpp index e517ecda89..3b78cabeea 100644 --- a/dom/streams/TransformStream.cpp +++ b/dom/streams/TransformStream.cpp @@ -20,14 +20,6 @@ #include "mozilla/dom/TransformerBinding.h" #include "nsWrapperCache.h" -// XXX: GCC somehow does not allow attributes before lambda return types, while -// clang requires so. See also bug 1627007. -#ifdef __clang__ -# define MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA MOZ_CAN_RUN_SCRIPT_BOUNDARY -#else -# define MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA -#endif - namespace mozilla::dom { using namespace streams_abstract; diff --git a/dom/streams/UnderlyingSinkCallbackHelpers.cpp b/dom/streams/UnderlyingSinkCallbackHelpers.cpp index 91562a2db3..6277fd26e0 100644 --- a/dom/streams/UnderlyingSinkCallbackHelpers.cpp +++ b/dom/streams/UnderlyingSinkCallbackHelpers.cpp @@ -111,6 +111,24 @@ already_AddRefed<Promise> UnderlyingSinkAlgorithms::AbortCallback( } // https://streams.spec.whatwg.org/#writable-set-up +// This one is not covered by the above section as the spec expects any spec +// implementation to explicitly return a promise for this callback. It's still +// useful for Gecko as error handling is very frequently done with +// ErrorResult instead of a rejected promise. See also +// https://github.com/whatwg/streams/issues/1253. +already_AddRefed<Promise> UnderlyingSinkAlgorithmsWrapper::WriteCallback( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + WritableStreamDefaultController& aController, ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = xpc::CurrentNativeGlobal(aCx); + return PromisifyAlgorithm( + global, + [&](ErrorResult& aRv) { + return WriteCallbackImpl(aCx, aChunk, aController, aRv); + }, + aRv); +} + +// https://streams.spec.whatwg.org/#writable-set-up // Step 2.1: Let closeAlgorithmWrapper be an algorithm that runs these steps: already_AddRefed<Promise> UnderlyingSinkAlgorithmsWrapper::CloseCallback( JSContext* aCx, ErrorResult& aRv) { @@ -182,19 +200,20 @@ WritableStreamToOutput::OnOutputStreamReady(nsIAsyncOutputStream* aStream) { return NS_OK; } -already_AddRefed<Promise> WritableStreamToOutput::WriteCallback( +already_AddRefed<Promise> WritableStreamToOutput::WriteCallbackImpl( JSContext* aCx, JS::Handle<JS::Value> aChunk, - WritableStreamDefaultController& aController, ErrorResult& aError) { + WritableStreamDefaultController& aController, ErrorResult& aRv) { ArrayBufferViewOrArrayBuffer data; if (!data.Init(aCx, aChunk)) { - aError.StealExceptionFromJSContext(aCx); + aRv.MightThrowJSException(); + aRv.StealExceptionFromJSContext(aCx); return nullptr; } // buffer/bufferView MOZ_ASSERT(data.IsArrayBuffer() || data.IsArrayBufferView()); - RefPtr<Promise> promise = Promise::Create(mParent, aError); - if (NS_WARN_IF(aError.Failed())) { + RefPtr<Promise> promise = Promise::Create(mParent, aRv); + if (NS_WARN_IF(aRv.Failed())) { return nullptr; } diff --git a/dom/streams/UnderlyingSinkCallbackHelpers.h b/dom/streams/UnderlyingSinkCallbackHelpers.h index c99c8709ce..0717176aca 100644 --- a/dom/streams/UnderlyingSinkCallbackHelpers.h +++ b/dom/streams/UnderlyingSinkCallbackHelpers.h @@ -135,6 +135,10 @@ class UnderlyingSinkAlgorithmsWrapper : public UnderlyingSinkAlgorithmsBase { aRetVal.setUndefined(); } + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> WriteCallback( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + WritableStreamDefaultController& aController, ErrorResult& aRv) final; + MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CloseCallback( JSContext* aCx, ErrorResult& aRv) final; @@ -142,6 +146,10 @@ class UnderlyingSinkAlgorithmsWrapper : public UnderlyingSinkAlgorithmsBase { JSContext* aCx, const Optional<JS::Handle<JS::Value>>& aReason, ErrorResult& aRv) final; + virtual already_AddRefed<Promise> WriteCallbackImpl( + JSContext* aCx, JS::Handle<JS::Value> aChunk, + WritableStreamDefaultController& aController, ErrorResult& aRv) = 0; + virtual already_AddRefed<Promise> CloseCallbackImpl(JSContext* aCx, ErrorResult& aRv) { // (closeAlgorithm is optional, give null by default) @@ -169,7 +177,7 @@ class WritableStreamToOutput final : public UnderlyingSinkAlgorithmsWrapper, // Streams algorithms - already_AddRefed<Promise> WriteCallback( + already_AddRefed<Promise> WriteCallbackImpl( JSContext* aCx, JS::Handle<JS::Value> aChunk, WritableStreamDefaultController& aController, ErrorResult& aRv) override; diff --git a/dom/svg/SVGAElement.cpp b/dom/svg/SVGAElement.cpp index d27e8735ad..1533cceb34 100644 --- a/dom/svg/SVGAElement.cpp +++ b/dom/svg/SVGAElement.cpp @@ -257,4 +257,14 @@ SVGElement::StringAttributesInfo SVGAElement::GetStringInfo() { ArrayLength(sStringInfo)); } +void SVGAElement::DidAnimateAttribute(int32_t aNameSpaceID, + nsAtom* aAttribute) { + if ((aNameSpaceID == kNameSpaceID_None || + aNameSpaceID == kNameSpaceID_XLink) && + aAttribute == nsGkAtoms::href) { + Link::ResetLinkState(true, Link::ElementHasHref()); + } + SVGAElementBase::DidAnimateAttribute(aNameSpaceID, aAttribute); +} + } // namespace mozilla::dom diff --git a/dom/svg/SVGAElement.h b/dom/svg/SVGAElement.h index 166a146436..bbb552fce6 100644 --- a/dom/svg/SVGAElement.h +++ b/dom/svg/SVGAElement.h @@ -24,8 +24,7 @@ namespace dom { using SVGAElementBase = SVGGraphicsElement; -class SVGAElement final : public SVGAElementBase, - public Link { +class SVGAElement final : public SVGAElementBase, public Link { protected: using Element::GetText; @@ -93,6 +92,7 @@ class SVGAElement final : public SVGAElementBase, virtual ~SVGAElement() = default; StringAttributesInfo GetStringInfo() override; + void DidAnimateAttribute(int32_t aNameSpaceID, nsAtom* aAttribute) override; enum { HREF, XLINK_HREF, TARGET }; SVGAnimatedString mStringAttributes[3]; diff --git a/dom/svg/SVGContentUtils.cpp b/dom/svg/SVGContentUtils.cpp index 0913ae48c5..72534c12e8 100644 --- a/dom/svg/SVGContentUtils.cpp +++ b/dom/svg/SVGContentUtils.cpp @@ -493,6 +493,14 @@ static gfx::Matrix GetCTMInternal(SVGElement* aElement, bool aScreenCTM, return ret; }; + auto postTranslateFrameOffset = [](nsIFrame* aFrame, nsIFrame* aAncestorFrame, + gfx::Matrix& aMatrix) { + auto point = aFrame->GetOffsetTo(aAncestorFrame); + aMatrix = + aMatrix.PostTranslate(nsPresContext::AppUnitsToFloatCSSPixels(point.x), + nsPresContext::AppUnitsToFloatCSSPixels(point.y)); + }; + gfxMatrix matrix = getLocalTransformHelper(aElement, aHaveRecursed); SVGElement* element = aElement; @@ -530,39 +538,62 @@ static gfx::Matrix GetCTMInternal(SVGElement* aElement, bool aScreenCTM, matrix = getLocalTransformHelper(aElement, true); } - if (auto* f = element->GetPrimaryFrame()) { - if (f->IsSVGOuterSVGFrame()) { - nsMargin bp = f->GetUsedBorderAndPadding(); - matrix.PostTranslate( - NSAppUnitsToFloatPixels(bp.left, AppUnitsPerCSSPixel()), - NSAppUnitsToFloatPixels(bp.top, AppUnitsPerCSSPixel())); - } + gfx::Matrix tm = gfx::ToMatrix(matrix); + nsIFrame* frame = element->GetPrimaryFrame(); + if (!frame) { + return tm; + } + if (frame->IsSVGOuterSVGFrame()) { + nsMargin bp = frame->GetUsedBorderAndPadding(); + int32_t appUnitsPerCSSPixel = AppUnitsPerCSSPixel(); + tm.PostTranslate(NSAppUnitsToFloatPixels(bp.left, appUnitsPerCSSPixel), + NSAppUnitsToFloatPixels(bp.top, appUnitsPerCSSPixel)); } if (!ancestor || !ancestor->IsElement()) { - return gfx::ToMatrix(matrix); + return tm; } if (auto* ancestorSVG = SVGElement::FromNode(ancestor)) { - return gfx::ToMatrix(matrix) * GetCTMInternal(ancestorSVG, true, true); - } - - // XXX this does not take into account CSS transform, or that the non-SVG - // content that we've hit may itself be inside an SVG foreignObject higher up - Document* currentDoc = aElement->GetComposedDoc(); - float x = 0.0f, y = 0.0f; - if (currentDoc && element->IsSVGElement(nsGkAtoms::svg)) { - PresShell* presShell = currentDoc->GetPresShell(); - if (presShell) { - nsIFrame* frame = element->GetPrimaryFrame(); - nsIFrame* ancestorFrame = presShell->GetRootFrame(); - if (frame && ancestorFrame) { - nsPoint point = frame->GetOffsetTo(ancestorFrame); - x = nsPresContext::AppUnitsToFloatCSSPixels(point.x); - y = nsPresContext::AppUnitsToFloatCSSPixels(point.y); - } - } + return tm * GetCTMInternal(ancestorSVG, true, true); } - return ToMatrix(matrix).PostTranslate(x, y); + nsIFrame* parentFrame = frame->GetParent(); + if (!parentFrame) { + return tm; + } + postTranslateFrameOffset(frame, parentFrame, tm); + + nsIContent* nearestSVGAncestor = ancestor; + while (nearestSVGAncestor && !nearestSVGAncestor->IsSVGElement()) { + nearestSVGAncestor = nearestSVGAncestor->GetFlattenedTreeParent(); + } + + nsIFrame* ancestorFrame; + if (nearestSVGAncestor) { + ancestorFrame = nearestSVGAncestor->GetPrimaryFrame(); + } else { + Document* currentDoc = aElement->GetComposedDoc(); + PresShell* presShell = currentDoc ? currentDoc->GetPresShell() : nullptr; + ancestorFrame = presShell ? presShell->GetRootFrame() : nullptr; + } + if (!ancestorFrame) { + return tm; + } + auto transformToAncestor = nsLayoutUtils::GetTransformToAncestor( + RelativeTo{parentFrame, ViewportType::Layout}, + RelativeTo{ancestorFrame, ViewportType::Layout}, nsIFrame::IN_CSS_UNITS); + gfx::Matrix result2d; + if (transformToAncestor.CanDraw2D(&result2d)) { + tm = tm * result2d; + } else { + // The transform from our outer SVG matrix to the root is a 3D + // transform. We can't really process that so give up and just + // return the overall translation from the outer SVG to the root. + postTranslateFrameOffset(parentFrame, ancestorFrame, tm); + } + return nearestSVGAncestor + ? tm * GetCTMInternal(static_cast<SVGElement*>(nearestSVGAncestor), + true, true) + : tm; } gfx::Matrix SVGContentUtils::GetCTM(SVGElement* aElement, bool aScreenCTM) { diff --git a/dom/svg/SVGElement.cpp b/dom/svg/SVGElement.cpp index 57c1cc19cb..213bc6be0e 100644 --- a/dom/svg/SVGElement.cpp +++ b/dom/svg/SVGElement.cpp @@ -2080,7 +2080,7 @@ void SVGElement::DidChangeStringList(bool aIsConditionalProcessingAttribute, void SVGElement::DidAnimateAttribute(int32_t aNameSpaceID, nsAtom* aAttribute) { if (auto* frame = GetPrimaryFrame()) { frame->AttributeChanged(aNameSpaceID, aAttribute, - MutationEvent_Binding::SMIL); + MutationEvent_Binding::MODIFICATION); SVGObserverUtils::InvalidateRenderingObservers(frame); return; } diff --git a/dom/svg/SVGElement.h b/dom/svg/SVGElement.h index 08f70ab482..d9ad8b79ca 100644 --- a/dom/svg/SVGElement.h +++ b/dom/svg/SVGElement.h @@ -538,9 +538,9 @@ class SVGElement : public SVGElementBase // nsIContent static SVGEnumMapping sSVGUnitTypesMap[]; - private: - void DidAnimateAttribute(int32_t aNameSpaceID, nsAtom* aAttribute); + virtual void DidAnimateAttribute(int32_t aNameSpaceID, nsAtom* aAttribute); + private: void UnsetAttrInternal(int32_t aNameSpaceID, nsAtom* aName, bool aNotify); SVGAnimatedClass mClassAttribute; diff --git a/dom/svg/SVGFEImageElement.cpp b/dom/svg/SVGFEImageElement.cpp index f9c5f96758..826ba82228 100644 --- a/dom/svg/SVGFEImageElement.cpp +++ b/dom/svg/SVGFEImageElement.cpp @@ -371,4 +371,21 @@ void SVGFEImageElement::Notify(imgIRequest* aRequest, int32_t aType, } } +void SVGFEImageElement::DidAnimateAttribute(int32_t aNameSpaceID, + nsAtom* aAttribute) { + if ((aNameSpaceID == kNameSpaceID_None || + aNameSpaceID == kNameSpaceID_XLink) && + aAttribute == nsGkAtoms::href) { + bool hrefIsSet = + mStringAttributes[SVGFEImageElement::HREF].IsExplicitlySet() || + mStringAttributes[SVGFEImageElement::XLINK_HREF].IsExplicitlySet(); + if (hrefIsSet) { + LoadSVGImage(true, true); + } else { + CancelImageRequests(true); + } + } + SVGFEImageElementBase::DidAnimateAttribute(aNameSpaceID, aAttribute); +} + } // namespace mozilla::dom diff --git a/dom/svg/SVGFEImageElement.h b/dom/svg/SVGFEImageElement.h index 41b28b83a9..4df686d2d5 100644 --- a/dom/svg/SVGFEImageElement.h +++ b/dom/svg/SVGFEImageElement.h @@ -94,6 +94,7 @@ class SVGFEImageElement final : public SVGFEImageElementBase, } private: + void DidAnimateAttribute(int32_t aNameSpaceID, nsAtom* aAttribute) override; nsresult LoadSVGImage(bool aForce, bool aNotify); bool ShouldLoadImage() const; diff --git a/dom/svg/SVGGeometryElement.cpp b/dom/svg/SVGGeometryElement.cpp index d012a911b8..de5756ed7d 100644 --- a/dom/svg/SVGGeometryElement.cpp +++ b/dom/svg/SVGGeometryElement.cpp @@ -187,7 +187,7 @@ bool SVGGeometryElement::IsPointInFill(const DOMPointInit& aPoint) { bool SVGGeometryElement::IsPointInStroke(const DOMPointInit& aPoint) { // stroke-* attributes and the d attribute are presentation attributes, so we // flush the layout before building the path. - if (nsCOMPtr<Document> doc = GetComposedDoc()) { + if (auto* doc = GetComposedDoc()) { doc->FlushPendingNotifications(FlushType::Layout); } diff --git a/dom/svg/SVGGraphicsElement.cpp b/dom/svg/SVGGraphicsElement.cpp index 139b97a76a..29b6b76728 100644 --- a/dom/svg/SVGGraphicsElement.cpp +++ b/dom/svg/SVGGraphicsElement.cpp @@ -125,8 +125,7 @@ already_AddRefed<SVGRect> SVGGraphicsElement::GetBBox( } already_AddRefed<SVGMatrix> SVGGraphicsElement::GetCTM() { - Document* currentDoc = GetComposedDoc(); - if (currentDoc) { + if (auto* currentDoc = GetComposedDoc()) { // Flush all pending notifications so that our frames are up to date currentDoc->FlushPendingNotifications(FlushType::Layout); } @@ -137,8 +136,7 @@ already_AddRefed<SVGMatrix> SVGGraphicsElement::GetCTM() { } already_AddRefed<SVGMatrix> SVGGraphicsElement::GetScreenCTM() { - Document* currentDoc = GetComposedDoc(); - if (currentDoc) { + if (auto* currentDoc = GetComposedDoc()) { // Flush all pending notifications so that our frames are up to date currentDoc->FlushPendingNotifications(FlushType::Layout); } diff --git a/dom/svg/SVGImageElement.cpp b/dom/svg/SVGImageElement.cpp index 264c03da09..4a1332fb00 100644 --- a/dom/svg/SVGImageElement.cpp +++ b/dom/svg/SVGImageElement.cpp @@ -310,4 +310,21 @@ SVGElement::StringAttributesInfo SVGImageElement::GetStringInfo() { ArrayLength(sStringInfo)); } +void SVGImageElement::DidAnimateAttribute(int32_t aNameSpaceID, + nsAtom* aAttribute) { + if ((aNameSpaceID == kNameSpaceID_None || + aNameSpaceID == kNameSpaceID_XLink) && + aAttribute == nsGkAtoms::href) { + bool hrefIsSet = + mStringAttributes[SVGImageElement::HREF].IsExplicitlySet() || + mStringAttributes[SVGImageElement::XLINK_HREF].IsExplicitlySet(); + if (hrefIsSet) { + LoadSVGImage(true, true); + } else { + CancelImageRequests(true); + } + } + SVGImageElementBase::DidAnimateAttribute(aNameSpaceID, aAttribute); +} + } // namespace mozilla::dom diff --git a/dom/svg/SVGImageElement.h b/dom/svg/SVGImageElement.h index 06770e2576..591179b3a9 100644 --- a/dom/svg/SVGImageElement.h +++ b/dom/svg/SVGImageElement.h @@ -102,6 +102,8 @@ class SVGImageElement final : public SVGImageElementBase, gfx::Rect GeometryBounds(const gfx::Matrix& aToBoundsSpace); protected: + void DidAnimateAttribute(int32_t aNameSpaceID, nsAtom* aAttribute) override; + nsresult LoadSVGImage(bool aForce, bool aNotify); bool ShouldLoadImage() const; diff --git a/dom/svg/SVGPathData.cpp b/dom/svg/SVGPathData.cpp index a1f5b2ac98..2b2eaca46b 100644 --- a/dom/svg/SVGPathData.cpp +++ b/dom/svg/SVGPathData.cpp @@ -31,36 +31,14 @@ static inline bool IsMoveto(uint16_t aSegType) { return aSegType == PATHSEG_MOVETO_ABS || aSegType == PATHSEG_MOVETO_REL; } -static inline bool IsMoveto(StylePathCommand::Tag aSegType) { - return aSegType == StylePathCommand::Tag::MoveTo; -} - static inline bool IsValidType(uint16_t aSegType) { return SVGPathSegUtils::IsValidType(aSegType); } -static inline bool IsValidType(StylePathCommand::Tag aSegType) { - return aSegType != StylePathCommand::Tag::Unknown; -} - static inline bool IsClosePath(uint16_t aSegType) { return aSegType == PATHSEG_CLOSEPATH; } -static inline bool IsClosePath(StylePathCommand::Tag aSegType) { - return aSegType == StylePathCommand::Tag::ClosePath; -} - -static inline bool IsCubicType(StylePathCommand::Tag aType) { - return aType == StylePathCommand::Tag::CurveTo || - aType == StylePathCommand::Tag::SmoothCurveTo; -} - -static inline bool IsQuadraticType(StylePathCommand::Tag aType) { - return aType == StylePathCommand::Tag::QuadBezierCurveTo || - aType == StylePathCommand::Tag::SmoothQuadBezierCurveTo; -} - nsresult SVGPathData::CopyFrom(const SVGPathData& rhs) { if (!mData.Assign(rhs.mData, fallible)) { return NS_ERROR_OUT_OF_MEMORY; @@ -200,13 +178,13 @@ bool SVGPathData::GetDistancesFromOriginToEndsOfVisibleSegments( } // We skip all moveto commands except for the initial moveto. - if (!cmd.IsMoveTo() || !firstMoveToIsChecked) { + if (!cmd.IsMove() || !firstMoveToIsChecked) { if (!aOutput->AppendElement(state.length, fallible)) { return false; } } - if (cmd.IsMoveTo() && !firstMoveToIsChecked) { + if (cmd.IsMove() && !firstMoveToIsChecked) { firstMoveToIsChecked = true; } } @@ -550,6 +528,8 @@ already_AddRefed<Path> SVGPathData::BuildPath(PathBuilder* aBuilder, return aBuilder->Finish(); } +#undef MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS_TO_DT + already_AddRefed<Path> SVGPathData::BuildPathForMeasuring() const { // Since the path that we return will not be used for painting it doesn't // matter what we pass to CreatePathBuilder as aFillRule. Hawever, we do want @@ -576,15 +556,33 @@ already_AddRefed<Path> SVGPathData::BuildPathForMeasuring( return BuildPath(aPath, builder, StyleStrokeLinecap::Butt, 0); } -// We could simplify this function because this is only used by CSS motion path -// and clip-path, which don't render the SVG Path. i.e. The returned path is -// used as a reference. -/* static */ -already_AddRefed<Path> SVGPathData::BuildPath( - Span<const StylePathCommand> aPath, PathBuilder* aBuilder, - StyleStrokeLinecap aStrokeLineCap, Float aStrokeWidth, const Point& aOffset, +static inline StyleCSSFloat GetRotate(const StyleCSSFloat& aAngle) { + return aAngle; +} + +static inline StyleCSSFloat GetRotate(const StyleAngle& aAngle) { + return aAngle.ToDegrees(); +} + +static inline StyleCSSFloat Resolve(const StyleCSSFloat& aValue, + CSSCoord aBasis) { + return aValue; +} + +static inline StyleCSSFloat Resolve(const LengthPercentage& aValue, + CSSCoord aBasis) { + return aValue.ResolveToCSSPixels(aBasis); +} + +template <typename Angle, typename LP> +static already_AddRefed<Path> BuildPathInternal( + Span<const StyleGenericShapeCommand<Angle, LP>> aPath, + PathBuilder* aBuilder, StyleStrokeLinecap aStrokeLineCap, + Float aStrokeWidth, const CSSSize& aPercentageBasis, const Point& aOffset, float aZoomFactor) { - if (aPath.IsEmpty() || !aPath[0].IsMoveTo()) { + using Command = StyleGenericShapeCommand<Angle, LP>; + + if (aPath.IsEmpty() || !aPath[0].IsMove()) { return nullptr; // paths without an initial moveto are invalid } @@ -592,14 +590,24 @@ already_AddRefed<Path> SVGPathData::BuildPath( bool subpathHasLength = false; // visual length bool subpathContainsNonMoveTo = false; - StylePathCommand::Tag segType = StylePathCommand::Tag::Unknown; - StylePathCommand::Tag prevSegType = StylePathCommand::Tag::Unknown; + const Command* seg = nullptr; + const Command* prevSeg = nullptr; Point pathStart(0.0, 0.0); // start point of [sub]path Point segStart(0.0, 0.0); Point segEnd; Point cp1, cp2; // previous bezier's control points Point tcp1, tcp2; // temporaries + auto maybeApproximateZeroLengthSubpathSquareCaps = + [&](const Command* aPrevSeg, const Command* aSeg) { + if (!subpathHasLength && hasLineCaps && aStrokeWidth > 0 && + subpathContainsNonMoveTo && aPrevSeg && aSeg && + (!aPrevSeg->IsMove() || aSeg->IsClose())) { + ApproximateZeroLengthSubpathSquareCaps(aBuilder, segStart, + aStrokeWidth); + } + }; + auto scale = [aOffset, aZoomFactor](const Point& p) { return Point(p.x * aZoomFactor, p.y * aZoomFactor) + aOffset; }; @@ -608,41 +616,39 @@ already_AddRefed<Path> SVGPathData::BuildPath( // then cp2 is its second control point. If the previous segment was a // quadratic curve, then cp1 is its (only) control point. - for (const StylePathCommand& cmd : aPath) { - segType = cmd.tag; - switch (segType) { - case StylePathCommand::Tag::ClosePath: + for (const auto& cmd : aPath) { + seg = &cmd; + switch (cmd.tag) { + case Command::Tag::Close: // set this early to allow drawing of square caps for "M{x},{y} Z": subpathContainsNonMoveTo = true; - MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS_TO_DT; + maybeApproximateZeroLengthSubpathSquareCaps(prevSeg, seg); segEnd = pathStart; aBuilder->Close(); break; - case StylePathCommand::Tag::MoveTo: { - MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS_TO_DT; - const Point& p = cmd.move_to.point.ConvertsToGfxPoint(); - pathStart = segEnd = - cmd.move_to.absolute == StyleIsAbsolute::Yes ? p : segStart + p; + case Command::Tag::Move: { + maybeApproximateZeroLengthSubpathSquareCaps(prevSeg, seg); + const Point& p = cmd.move.point.ToGfxPoint(aPercentageBasis); + pathStart = segEnd = cmd.move.by_to == StyleByTo::To ? p : segStart + p; aBuilder->MoveTo(scale(segEnd)); subpathHasLength = false; break; } - case StylePathCommand::Tag::LineTo: { - const Point& p = cmd.line_to.point.ConvertsToGfxPoint(); - segEnd = - cmd.line_to.absolute == StyleIsAbsolute::Yes ? p : segStart + p; + case Command::Tag::Line: { + const Point& p = cmd.line.point.ToGfxPoint(aPercentageBasis); + segEnd = cmd.line.by_to == StyleByTo::To ? p : segStart + p; if (segEnd != segStart) { subpathHasLength = true; aBuilder->LineTo(scale(segEnd)); } break; } - case StylePathCommand::Tag::CurveTo: - cp1 = cmd.curve_to.control1.ConvertsToGfxPoint(); - cp2 = cmd.curve_to.control2.ConvertsToGfxPoint(); - segEnd = cmd.curve_to.point.ConvertsToGfxPoint(); + case Command::Tag::CubicCurve: + cp1 = cmd.cubic_curve.control1.ToGfxPoint(aPercentageBasis); + cp2 = cmd.cubic_curve.control2.ToGfxPoint(aPercentageBasis); + segEnd = cmd.cubic_curve.point.ToGfxPoint(aPercentageBasis); - if (cmd.curve_to.absolute == StyleIsAbsolute::No) { + if (cmd.cubic_curve.by_to == StyleByTo::By) { cp1 += segStart; cp2 += segStart; segEnd += segStart; @@ -654,11 +660,11 @@ already_AddRefed<Path> SVGPathData::BuildPath( } break; - case StylePathCommand::Tag::QuadBezierCurveTo: - cp1 = cmd.quad_bezier_curve_to.control1.ConvertsToGfxPoint(); - segEnd = cmd.quad_bezier_curve_to.point.ConvertsToGfxPoint(); + case Command::Tag::QuadCurve: + cp1 = cmd.quad_curve.control1.ToGfxPoint(aPercentageBasis); + segEnd = cmd.quad_curve.point.ToGfxPoint(aPercentageBasis); - if (cmd.quad_bezier_curve_to.absolute == StyleIsAbsolute::No) { + if (cmd.quad_curve.by_to == StyleByTo::By) { cp1 += segStart; segEnd += segStart; // set before setting tcp2! } @@ -673,11 +679,11 @@ already_AddRefed<Path> SVGPathData::BuildPath( } break; - case StylePathCommand::Tag::EllipticalArc: { - const auto& arc = cmd.elliptical_arc; - Point radii(arc.rx, arc.ry); - segEnd = arc.point.ConvertsToGfxPoint(); - if (arc.absolute == StyleIsAbsolute::No) { + case Command::Tag::Arc: { + const auto& arc = cmd.arc; + const Point& radii = arc.radii.ToGfxPoint(aPercentageBasis); + segEnd = arc.point.ToGfxPoint(aPercentageBasis); + if (arc.by_to == StyleByTo::By) { segEnd += segStart; } if (segEnd != segStart) { @@ -685,8 +691,11 @@ already_AddRefed<Path> SVGPathData::BuildPath( if (radii.x == 0.0f || radii.y == 0.0f) { aBuilder->LineTo(scale(segEnd)); } else { - SVGArcConverter converter(segStart, segEnd, radii, arc.angle, - arc.large_arc_flag._0, arc.sweep_flag._0); + const bool arc_is_large = arc.arc_size == StyleArcSize::Large; + const bool arc_is_cw = arc.arc_sweep == StyleArcSweep::Cw; + SVGArcConverter converter(segStart, segEnd, radii, + GetRotate(arc.rotate), arc_is_large, + arc_is_cw); while (converter.GetNextSegment(&cp1, &cp2, &segEnd)) { aBuilder->BezierTo(scale(cp1), scale(cp2), scale(segEnd)); } @@ -694,11 +703,12 @@ already_AddRefed<Path> SVGPathData::BuildPath( } break; } - case StylePathCommand::Tag::HorizontalLineTo: - if (cmd.horizontal_line_to.absolute == StyleIsAbsolute::Yes) { - segEnd = Point(cmd.horizontal_line_to.x, segStart.y); + case Command::Tag::HLine: { + const float x = Resolve(cmd.h_line.x, aPercentageBasis.width); + if (cmd.h_line.by_to == StyleByTo::To) { + segEnd = Point(x, segStart.y); } else { - segEnd = segStart + Point(cmd.horizontal_line_to.x, 0.0f); + segEnd = segStart + Point(x, 0.0f); } if (segEnd != segStart) { @@ -706,12 +716,13 @@ already_AddRefed<Path> SVGPathData::BuildPath( aBuilder->LineTo(scale(segEnd)); } break; - - case StylePathCommand::Tag::VerticalLineTo: - if (cmd.vertical_line_to.absolute == StyleIsAbsolute::Yes) { - segEnd = Point(segStart.x, cmd.vertical_line_to.y); + } + case Command::Tag::VLine: { + const float y = Resolve(cmd.v_line.y, aPercentageBasis.height); + if (cmd.v_line.by_to == StyleByTo::To) { + segEnd = Point(segStart.x, y); } else { - segEnd = segStart + Point(0.0f, cmd.vertical_line_to.y); + segEnd = segStart + Point(0.0f, y); } if (segEnd != segStart) { @@ -719,13 +730,13 @@ already_AddRefed<Path> SVGPathData::BuildPath( aBuilder->LineTo(scale(segEnd)); } break; + } + case Command::Tag::SmoothCubic: + cp1 = prevSeg && prevSeg->IsCubicType() ? segStart * 2 - cp2 : segStart; + cp2 = cmd.smooth_cubic.control2.ToGfxPoint(aPercentageBasis); + segEnd = cmd.smooth_cubic.point.ToGfxPoint(aPercentageBasis); - case StylePathCommand::Tag::SmoothCurveTo: - cp1 = IsCubicType(prevSegType) ? segStart * 2 - cp2 : segStart; - cp2 = cmd.smooth_curve_to.control2.ConvertsToGfxPoint(); - segEnd = cmd.smooth_curve_to.point.ConvertsToGfxPoint(); - - if (cmd.smooth_curve_to.absolute == StyleIsAbsolute::No) { + if (cmd.smooth_cubic.by_to == StyleByTo::By) { cp2 += segStart; segEnd += segStart; } @@ -736,18 +747,15 @@ already_AddRefed<Path> SVGPathData::BuildPath( } break; - case StylePathCommand::Tag::SmoothQuadBezierCurveTo: { - cp1 = IsQuadraticType(prevSegType) ? segStart * 2 - cp1 : segStart; + case Command::Tag::SmoothQuad: { + cp1 = prevSeg && prevSeg->IsQuadraticType() ? segStart * 2 - cp1 + : segStart; // Convert quadratic curve to cubic curve: tcp1 = segStart + (cp1 - segStart) * 2 / 3; - const Point& p = - cmd.smooth_quad_bezier_curve_to.point.ConvertsToGfxPoint(); + const Point& p = cmd.smooth_quad.point.ToGfxPoint(aPercentageBasis); // set before setting tcp2! - segEnd = - cmd.smooth_quad_bezier_curve_to.absolute == StyleIsAbsolute::Yes - ? p - : segStart + p; + segEnd = cmd.smooth_quad.by_to == StyleByTo::To ? p : segStart + p; tcp2 = cp1 + (segEnd - cp1) / 3; if (segEnd != segStart || segEnd != cp1) { @@ -756,24 +764,38 @@ already_AddRefed<Path> SVGPathData::BuildPath( } break; } - case StylePathCommand::Tag::Unknown: - MOZ_ASSERT_UNREACHABLE("Unacceptable path segment type"); - return nullptr; } - subpathContainsNonMoveTo = !IsMoveto(segType); - prevSegType = segType; + subpathContainsNonMoveTo = !cmd.IsMove(); + prevSeg = seg; segStart = segEnd; } - MOZ_ASSERT(prevSegType == segType, - "prevSegType should be left at the final segType"); + MOZ_ASSERT(prevSeg == seg, "prevSegType should be left at the final segType"); - MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS_TO_DT; + maybeApproximateZeroLengthSubpathSquareCaps(prevSeg, seg); return aBuilder->Finish(); } +/* static */ +already_AddRefed<Path> SVGPathData::BuildPath( + Span<const StylePathCommand> aPath, PathBuilder* aBuilder, + StyleStrokeLinecap aStrokeLineCap, Float aStrokeWidth, + const CSSSize& aBasis, const gfx::Point& aOffset, float aZoomFactor) { + return BuildPathInternal(aPath, aBuilder, aStrokeLineCap, aStrokeWidth, + aBasis, aOffset, aZoomFactor); +} + +/* static */ +already_AddRefed<Path> SVGPathData::BuildPath( + Span<const StyleShapeCommand> aShape, PathBuilder* aBuilder, + StyleStrokeLinecap aStrokeLineCap, Float aStrokeWidth, + const CSSSize& aBasis, const gfx::Point& aOffset, float aZoomFactor) { + return BuildPathInternal(aShape, aBuilder, aStrokeLineCap, aStrokeWidth, + aBasis, aOffset, aZoomFactor); +} + static double AngleOfVector(const Point& aVector) { // C99 says about atan2 "A domain error may occur if both arguments are // zero" and "On a domain error, the function returns an implementation- @@ -1135,48 +1157,44 @@ void SVGPathData::GetMarkerPositioningData(Span<const StylePathCommand> aPath, uint32_t pathStartIndex = 0; // info on previous segment: - StylePathCommand::Tag prevSegType = StylePathCommand::Tag::Unknown; + const StylePathCommand* prevSeg = nullptr; Point prevSegEnd(0.0, 0.0); float prevSegEndAngle = 0.0f; Point prevCP; // if prev seg was a bezier, this was its last control point - StylePathCommand::Tag segType = StylePathCommand::Tag::Unknown; for (const StylePathCommand& cmd : aPath) { - segType = cmd.tag; Point& segStart = prevSegEnd; Point segEnd; float segStartAngle, segEndAngle; - switch (segType) // to find segStartAngle, segEnd and segEndAngle + switch (cmd.tag) // to find segStartAngle, segEnd and segEndAngle { - case StylePathCommand::Tag::ClosePath: + case StylePathCommand::Tag::Close: segEnd = pathStart; segStartAngle = segEndAngle = AngleOfVector(segEnd, segStart); break; - case StylePathCommand::Tag::MoveTo: { - const Point& p = cmd.move_to.point.ConvertsToGfxPoint(); - pathStart = segEnd = - cmd.move_to.absolute == StyleIsAbsolute::Yes ? p : segStart + p; + case StylePathCommand::Tag::Move: { + const Point& p = cmd.move.point.ToGfxPoint(); + pathStart = segEnd = cmd.move.by_to == StyleByTo::To ? p : segStart + p; pathStartIndex = aMarks->Length(); // If authors are going to specify multiple consecutive moveto commands // with markers, me might as well make the angle do something useful: segStartAngle = segEndAngle = AngleOfVector(segEnd, segStart); break; } - case StylePathCommand::Tag::LineTo: { - const Point& p = cmd.line_to.point.ConvertsToGfxPoint(); - segEnd = - cmd.line_to.absolute == StyleIsAbsolute::Yes ? p : segStart + p; + case StylePathCommand::Tag::Line: { + const Point& p = cmd.line.point.ToGfxPoint(); + segEnd = cmd.line.by_to == StyleByTo::To ? p : segStart + p; segStartAngle = segEndAngle = AngleOfVector(segEnd, segStart); break; } - case StylePathCommand::Tag::CurveTo: { - Point cp1 = cmd.curve_to.control1.ConvertsToGfxPoint(); - Point cp2 = cmd.curve_to.control2.ConvertsToGfxPoint(); - segEnd = cmd.curve_to.point.ConvertsToGfxPoint(); + case StylePathCommand::Tag::CubicCurve: { + Point cp1 = cmd.cubic_curve.control1.ToGfxPoint(); + Point cp2 = cmd.cubic_curve.control2.ToGfxPoint(); + segEnd = cmd.cubic_curve.point.ToGfxPoint(); - if (cmd.curve_to.absolute == StyleIsAbsolute::No) { + if (cmd.cubic_curve.by_to == StyleByTo::By) { cp1 += segStart; cp2 += segStart; segEnd += segStart; @@ -1189,11 +1207,11 @@ void SVGPathData::GetMarkerPositioningData(Span<const StylePathCommand> aPath, segEnd, cp2 == segEnd ? (cp1 == cp2 ? segStart : cp1) : cp2); break; } - case StylePathCommand::Tag::QuadBezierCurveTo: { - Point cp1 = cmd.quad_bezier_curve_to.control1.ConvertsToGfxPoint(); - segEnd = cmd.quad_bezier_curve_to.point.ConvertsToGfxPoint(); + case StylePathCommand::Tag::QuadCurve: { + Point cp1 = cmd.quad_curve.control1.ToGfxPoint(); + segEnd = cmd.quad_curve.point.ToGfxPoint(); - if (cmd.quad_bezier_curve_to.absolute == StyleIsAbsolute::No) { + if (cmd.quad_curve.by_to == StyleByTo::By) { cp1 += segStart; segEnd += segStart; // set before setting tcp2! } @@ -1203,16 +1221,15 @@ void SVGPathData::GetMarkerPositioningData(Span<const StylePathCommand> aPath, segEndAngle = AngleOfVector(segEnd, cp1 == segEnd ? segStart : cp1); break; } - case StylePathCommand::Tag::EllipticalArc: { - const auto& arc = cmd.elliptical_arc; - float rx = arc.rx; - float ry = arc.ry; - float angle = arc.angle; - bool largeArcFlag = arc.large_arc_flag._0; - bool sweepFlag = arc.sweep_flag._0; - Point radii(arc.rx, arc.ry); - segEnd = arc.point.ConvertsToGfxPoint(); - if (arc.absolute == StyleIsAbsolute::No) { + case StylePathCommand::Tag::Arc: { + const auto& arc = cmd.arc; + float rx = arc.radii.x; + float ry = arc.radii.y; + float angle = arc.rotate; + bool largeArcFlag = arc.arc_size == StyleArcSize::Large; + bool sweepFlag = arc.arc_sweep == StyleArcSweep::Cw; + segEnd = arc.point.ToGfxPoint(); + if (arc.by_to == StyleByTo::By) { segEnd += segStart; } @@ -1246,30 +1263,32 @@ void SVGPathData::GetMarkerPositioningData(Span<const StylePathCommand> aPath, largeArcFlag, sweepFlag, rx, ry); break; } - case StylePathCommand::Tag::HorizontalLineTo: { - if (cmd.horizontal_line_to.absolute == StyleIsAbsolute::Yes) { - segEnd = Point(cmd.horizontal_line_to.x, segStart.y); + case StylePathCommand::Tag::HLine: { + if (cmd.h_line.by_to == StyleByTo::To) { + segEnd = Point(cmd.h_line.x, segStart.y); } else { - segEnd = segStart + Point(cmd.horizontal_line_to.x, 0.0f); + segEnd = segStart + Point(cmd.h_line.x, 0.0f); } segStartAngle = segEndAngle = AngleOfVector(segEnd, segStart); break; } - case StylePathCommand::Tag::VerticalLineTo: { - if (cmd.vertical_line_to.absolute == StyleIsAbsolute::Yes) { - segEnd = Point(segStart.x, cmd.vertical_line_to.y); + case StylePathCommand::Tag::VLine: { + if (cmd.v_line.by_to == StyleByTo::To) { + segEnd = Point(segStart.x, cmd.v_line.y); } else { - segEnd = segStart + Point(0.0f, cmd.vertical_line_to.y); + segEnd = segStart + Point(0.0f, cmd.v_line.y); } segStartAngle = segEndAngle = AngleOfVector(segEnd, segStart); break; } - case StylePathCommand::Tag::SmoothCurveTo: { - Point cp1 = IsCubicType(prevSegType) ? segStart * 2 - prevCP : segStart; - Point cp2 = cmd.smooth_curve_to.control2.ConvertsToGfxPoint(); - segEnd = cmd.smooth_curve_to.point.ConvertsToGfxPoint(); - - if (cmd.smooth_curve_to.absolute == StyleIsAbsolute::No) { + case StylePathCommand::Tag::SmoothCubic: { + const Point& cp1 = prevSeg && prevSeg->IsCubicType() + ? segStart * 2 - prevCP + : segStart; + Point cp2 = cmd.smooth_cubic.control2.ToGfxPoint(); + segEnd = cmd.smooth_cubic.point.ToGfxPoint(); + + if (cmd.smooth_cubic.by_to == StyleByTo::By) { cp2 += segStart; segEnd += segStart; } @@ -1281,40 +1300,33 @@ void SVGPathData::GetMarkerPositioningData(Span<const StylePathCommand> aPath, segEnd, cp2 == segEnd ? (cp1 == cp2 ? segStart : cp1) : cp2); break; } - case StylePathCommand::Tag::SmoothQuadBezierCurveTo: { - Point cp1 = - IsQuadraticType(prevSegType) ? segStart * 2 - prevCP : segStart; - segEnd = - cmd.smooth_quad_bezier_curve_to.absolute == StyleIsAbsolute::Yes - ? cmd.smooth_quad_bezier_curve_to.point.ConvertsToGfxPoint() - : segStart + cmd.smooth_quad_bezier_curve_to.point - .ConvertsToGfxPoint(); + case StylePathCommand::Tag::SmoothQuad: { + const Point& cp1 = prevSeg && prevSeg->IsQuadraticType() + ? segStart * 2 - prevCP + : segStart; + segEnd = cmd.smooth_quad.by_to == StyleByTo::To + ? cmd.smooth_quad.point.ToGfxPoint() + : segStart + cmd.smooth_quad.point.ToGfxPoint(); prevCP = cp1; segStartAngle = AngleOfVector(cp1 == segStart ? segEnd : cp1, segStart); segEndAngle = AngleOfVector(segEnd, cp1 == segEnd ? segStart : cp1); break; } - case StylePathCommand::Tag::Unknown: - // Leave any existing marks in aMarks so we have a visual indication of - // when things went wrong. - MOZ_ASSERT_UNREACHABLE("Unknown segment type - path corruption?"); - return; } // Set the angle of the mark at the start of this segment: if (aMarks->Length()) { SVGMark& mark = aMarks->LastElement(); - if (!IsMoveto(segType) && IsMoveto(prevSegType)) { + if (!cmd.IsMove() && prevSeg && prevSeg->IsMove()) { // start of new subpath pathStartAngle = mark.angle = segStartAngle; - } else if (IsMoveto(segType) && !IsMoveto(prevSegType)) { + } else if (cmd.IsMove() && !(prevSeg && prevSeg->IsMove())) { // end of a subpath - if (prevSegType != StylePathCommand::Tag::ClosePath) { + if (!(prevSeg && prevSeg->IsClose())) { mark.angle = prevSegEndAngle; } - } else if (!(segType == StylePathCommand::Tag::ClosePath && - prevSegType == StylePathCommand::Tag::ClosePath)) { + } else if (!(cmd.IsClose() && prevSeg && prevSeg->IsClose())) { mark.angle = SVGContentUtils::AngleBisect(prevSegEndAngle, segStartAngle); } @@ -1327,19 +1339,18 @@ void SVGPathData::GetMarkerPositioningData(Span<const StylePathCommand> aPath, static_cast<float>(segEnd.y), 0.0f, SVGMark::eMid)); - if (segType == StylePathCommand::Tag::ClosePath && - prevSegType != StylePathCommand::Tag::ClosePath) { + if (cmd.IsClose() && !(prevSeg && prevSeg->IsClose())) { aMarks->LastElement().angle = aMarks->ElementAt(pathStartIndex).angle = SVGContentUtils::AngleBisect(segEndAngle, pathStartAngle); } - prevSegType = segType; + prevSeg = &cmd; prevSegEnd = segEnd; prevSegEndAngle = segEndAngle; } if (aMarks->Length()) { - if (prevSegType != StylePathCommand::Tag::ClosePath) { + if (!(prevSeg && prevSeg->IsClose())) { aMarks->LastElement().angle = prevSegEndAngle; } aMarks->LastElement().type = SVGMark::eEnd; diff --git a/dom/svg/SVGPathData.h b/dom/svg/SVGPathData.h index 4aa60de3cb..e06e669706 100644 --- a/dom/svg/SVGPathData.h +++ b/dom/svg/SVGPathData.h @@ -17,13 +17,13 @@ #include "mozilla/gfx/Types.h" #include "mozilla/MemoryReporting.h" #include "mozilla/RefPtr.h" +#include "mozilla/ServoStyleConsts.h" #include "nsTArray.h" #include <string.h> namespace mozilla { -struct StylePathCommand; struct SVGMark; enum class StyleStrokeLinecap : uint8_t; @@ -190,14 +190,22 @@ class SVGPathData { Span<const StylePathCommand> aPath); /** - * This function tries to build the path from an array of StylePathCommand, + * This function tries to build the path from an array of GenericShapeCommand, * which is generated by cbindgen from Rust (see ServoStyleConsts.h). * Basically, this is a variant of the above BuildPath() functions. + * Note: |StylePathCommand| doesn't accept percentage values, so its |aBasis| + * is empty by default. */ static already_AddRefed<Path> BuildPath( Span<const StylePathCommand> aPath, PathBuilder* aBuilder, StyleStrokeLinecap aStrokeLineCap, Float aStrokeWidth, - const gfx::Point& aOffset = gfx::Point(), float aZoomFactor = 1.0); + const CSSSize& aBasis = {}, const gfx::Point& aOffset = gfx::Point(), + float aZoomFactor = 1.0); + static already_AddRefed<Path> BuildPath( + Span<const StyleShapeCommand> aShape, PathBuilder* aBuilder, + StyleStrokeLinecap aStrokeLineCap, Float aStrokeWidth, + const CSSSize& aBasis, const gfx::Point& aOffset = gfx::Point(), + float aZoomFactor = 1.0); const_iterator begin() const { return mData.Elements(); } const_iterator end() const { return mData.Elements() + mData.Length(); } diff --git a/dom/svg/SVGPathElement.cpp b/dom/svg/SVGPathElement.cpp index 61d2dab070..e9f500bbfd 100644 --- a/dom/svg/SVGPathElement.cpp +++ b/dom/svg/SVGPathElement.cpp @@ -351,7 +351,7 @@ bool SVGPathElement::IsClosedLoop() const { const nsStyleSVGReset* styleSVGReset = s->StyleSVGReset(); if (styleSVGReset->mD.IsPath()) { isClosed = !styleSVGReset->mD.AsPath()._0.IsEmpty() && - styleSVGReset->mD.AsPath()._0.AsSpan().rbegin()->IsClosePath(); + styleSVGReset->mD.AsPath()._0.AsSpan().rbegin()->IsClose(); } }; diff --git a/dom/svg/SVGPathSegUtils.cpp b/dom/svg/SVGPathSegUtils.cpp index 149f76f250..dfe6b5eec9 100644 --- a/dom/svg/SVGPathSegUtils.cpp +++ b/dom/svg/SVGPathSegUtils.cpp @@ -417,14 +417,13 @@ void SVGPathSegUtils::TraversePathSegment(const float* aData, void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, SVGPathTraversalState& aState) { switch (aCommand.tag) { - case StylePathCommand::Tag::ClosePath: + case StylePathCommand::Tag::Close: TraverseClosePath(nullptr, aState); break; - case StylePathCommand::Tag::MoveTo: { - const Point& p = aCommand.move_to.point.ConvertsToGfxPoint(); + case StylePathCommand::Tag::Move: { + const Point& p = aCommand.move.point.ToGfxPoint(); aState.start = aState.pos = - aCommand.move_to.absolute == StyleIsAbsolute::Yes ? p - : aState.pos + p; + aCommand.move.by_to == StyleByTo::To ? p : aState.pos + p; if (aState.ShouldUpdateLengthAndControlPoints()) { // aState.length is unchanged, since move commands don't affect path= // length. @@ -432,10 +431,10 @@ void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, } break; } - case StylePathCommand::Tag::LineTo: { - Point to = aCommand.line_to.absolute == StyleIsAbsolute::Yes - ? aCommand.line_to.point.ConvertsToGfxPoint() - : aState.pos + aCommand.line_to.point.ConvertsToGfxPoint(); + case StylePathCommand::Tag::Line: { + Point to = aCommand.line.by_to == StyleByTo::To + ? aCommand.line.point.ToGfxPoint() + : aState.pos + aCommand.line.point.ToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { aState.length += CalcDistanceBetweenPoints(aState.pos, to); aState.cp1 = aState.cp2 = to; @@ -443,14 +442,14 @@ void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, aState.pos = to; break; } - case StylePathCommand::Tag::CurveTo: { - const bool isRelative = aCommand.curve_to.absolute == StyleIsAbsolute::No; + case StylePathCommand::Tag::CubicCurve: { + const bool isRelative = aCommand.cubic_curve.by_to == StyleByTo::By; Point to = isRelative - ? aState.pos + aCommand.curve_to.point.ConvertsToGfxPoint() - : aCommand.curve_to.point.ConvertsToGfxPoint(); + ? aState.pos + aCommand.cubic_curve.point.ToGfxPoint() + : aCommand.cubic_curve.point.ToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { - Point cp1 = aCommand.curve_to.control1.ConvertsToGfxPoint(); - Point cp2 = aCommand.curve_to.control2.ConvertsToGfxPoint(); + Point cp1 = aCommand.cubic_curve.control1.ToGfxPoint(); + Point cp2 = aCommand.cubic_curve.control2.ToGfxPoint(); if (isRelative) { cp1 += aState.pos; cp2 += aState.pos; @@ -463,20 +462,15 @@ void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, aState.pos = to; break; } - case StylePathCommand::Tag::QuadBezierCurveTo: { - const bool isRelative = - aCommand.quad_bezier_curve_to.absolute == StyleIsAbsolute::No; - Point to = - isRelative - ? aState.pos + - aCommand.quad_bezier_curve_to.point.ConvertsToGfxPoint() - : aCommand.quad_bezier_curve_to.point.ConvertsToGfxPoint(); + case StylePathCommand::Tag::QuadCurve: { + const bool isRelative = aCommand.quad_curve.by_to == StyleByTo::By; + Point to = isRelative + ? aState.pos + aCommand.quad_curve.point.ToGfxPoint() + : aCommand.quad_curve.point.ToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { - Point cp = - isRelative - ? aState.pos + aCommand.quad_bezier_curve_to.control1 - .ConvertsToGfxPoint() - : aCommand.quad_bezier_curve_to.control1.ConvertsToGfxPoint(); + Point cp = isRelative + ? aState.pos + aCommand.quad_curve.control1.ToGfxPoint() + : aCommand.quad_curve.control1.ToGfxPoint(); aState.length += (float)CalcLengthOfQuadraticBezier(aState.pos, cp, to); aState.cp1 = cp; aState.cp2 = to; @@ -484,21 +478,22 @@ void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, aState.pos = to; break; } - case StylePathCommand::Tag::EllipticalArc: { - Point to = - aCommand.elliptical_arc.absolute == StyleIsAbsolute::Yes - ? aCommand.elliptical_arc.point.ConvertsToGfxPoint() - : aState.pos + aCommand.elliptical_arc.point.ConvertsToGfxPoint(); + case StylePathCommand::Tag::Arc: { + const auto& arc = aCommand.arc; + Point to = arc.by_to == StyleByTo::To + ? arc.point.ToGfxPoint() + : aState.pos + arc.point.ToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { - const auto& arc = aCommand.elliptical_arc; float dist = 0; - Point radii(arc.rx, arc.ry); + Point radii = arc.radii.ToGfxPoint(); if (radii.x == 0.0f || radii.y == 0.0f) { dist = CalcDistanceBetweenPoints(aState.pos, to); } else { Point bez[4] = {aState.pos, Point(0, 0), Point(0, 0), Point(0, 0)}; - SVGArcConverter converter(aState.pos, to, radii, arc.angle, - arc.large_arc_flag._0, arc.sweep_flag._0); + const bool largeArcFlag = arc.arc_size == StyleArcSize::Large; + const bool sweepFlag = arc.arc_sweep == StyleArcSweep::Cw; + SVGArcConverter converter(aState.pos, to, radii, arc.rotate, + largeArcFlag, sweepFlag); while (converter.GetNextSegment(&bez[1], &bez[2], &bez[3])) { dist += CalcBezLengthHelper(bez, 4, 0, SplitCubicBezier); bez[0] = bez[3]; @@ -510,10 +505,10 @@ void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, aState.pos = to; break; } - case StylePathCommand::Tag::HorizontalLineTo: { - Point to(aCommand.horizontal_line_to.absolute == StyleIsAbsolute::Yes - ? aCommand.horizontal_line_to.x - : aState.pos.x + aCommand.horizontal_line_to.x, + case StylePathCommand::Tag::HLine: { + Point to(aCommand.h_line.by_to == StyleByTo::To + ? aCommand.h_line.x + : aState.pos.x + aCommand.h_line.x, aState.pos.y); if (aState.ShouldUpdateLengthAndControlPoints()) { aState.length += std::fabs(to.x - aState.pos.x); @@ -522,11 +517,10 @@ void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, aState.pos = to; break; } - case StylePathCommand::Tag::VerticalLineTo: { - Point to(aState.pos.x, - aCommand.vertical_line_to.absolute == StyleIsAbsolute::Yes - ? aCommand.vertical_line_to.y - : aState.pos.y + aCommand.vertical_line_to.y); + case StylePathCommand::Tag::VLine: { + Point to(aState.pos.x, aCommand.v_line.by_to == StyleByTo::To + ? aCommand.v_line.y + : aState.pos.y + aCommand.v_line.y); if (aState.ShouldUpdateLengthAndControlPoints()) { aState.length += std::fabs(to.y - aState.pos.y); aState.cp1 = aState.cp2 = to; @@ -534,20 +528,16 @@ void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, aState.pos = to; break; } - case StylePathCommand::Tag::SmoothCurveTo: { - const bool isRelative = - aCommand.smooth_curve_to.absolute == StyleIsAbsolute::No; - Point to = - isRelative - ? aState.pos + aCommand.smooth_curve_to.point.ConvertsToGfxPoint() - : aCommand.smooth_curve_to.point.ConvertsToGfxPoint(); + case StylePathCommand::Tag::SmoothCubic: { + const bool isRelative = aCommand.smooth_cubic.by_to == StyleByTo::By; + Point to = isRelative + ? aState.pos + aCommand.smooth_cubic.point.ToGfxPoint() + : aCommand.smooth_cubic.point.ToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp1 = aState.pos - (aState.cp2 - aState.pos); - Point cp2 = - isRelative - ? aState.pos + - aCommand.smooth_curve_to.control2.ConvertsToGfxPoint() - : aCommand.smooth_curve_to.control2.ConvertsToGfxPoint(); + Point cp2 = isRelative ? aState.pos + + aCommand.smooth_cubic.control2.ToGfxPoint() + : aCommand.smooth_cubic.control2.ToGfxPoint(); aState.length += (float)CalcLengthOfCubicBezier(aState.pos, cp1, cp2, to); aState.cp2 = cp2; @@ -556,12 +546,10 @@ void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, aState.pos = to; break; } - case StylePathCommand::Tag::SmoothQuadBezierCurveTo: { - Point to = - aCommand.smooth_quad_bezier_curve_to.absolute == StyleIsAbsolute::Yes - ? aCommand.smooth_quad_bezier_curve_to.point.ConvertsToGfxPoint() - : aState.pos + aCommand.smooth_quad_bezier_curve_to.point - .ConvertsToGfxPoint(); + case StylePathCommand::Tag::SmoothQuad: { + Point to = aCommand.smooth_quad.by_to == StyleByTo::To + ? aCommand.smooth_quad.point.ToGfxPoint() + : aState.pos + aCommand.smooth_quad.point.ToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp = aState.pos - (aState.cp1 - aState.pos); aState.length += (float)CalcLengthOfQuadraticBezier(aState.pos, cp, to); @@ -571,8 +559,6 @@ void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, aState.pos = to; break; } - case StylePathCommand::Tag::Unknown: - MOZ_ASSERT_UNREACHABLE("Unacceptable path segment type"); } } @@ -704,8 +690,8 @@ Maybe<gfx::Rect> SVGPathToAxisAlignedRect(Span<const StylePathCommand> aPath) { for (const StylePathCommand& cmd : aPath) { switch (cmd.tag) { - case StylePathCommand::Tag::MoveTo: { - Point to = cmd.move_to.point.ConvertsToGfxPoint(); + case StylePathCommand::Tag::Move: { + Point to = cmd.move.point.ToGfxPoint(); if (helper.idx != 0) { // This is overly strict since empty moveto sequences such as "M 10 12 // M 3 2 M 0 0" render nothing, but I expect it won't make us miss a @@ -731,7 +717,7 @@ Maybe<gfx::Rect> SVGPathToAxisAlignedRect(Span<const StylePathCommand> aPath) { return Nothing(); } - if (cmd.move_to.absolute == StyleIsAbsolute::No) { + if (cmd.move.by_to == StyleByTo::By) { to = segStart + to; } @@ -744,7 +730,7 @@ Maybe<gfx::Rect> SVGPathToAxisAlignedRect(Span<const StylePathCommand> aPath) { break; } - case StylePathCommand::Tag::ClosePath: { + case StylePathCommand::Tag::Close: { if (!helper.Edge(segStart, pathStart)) { return Nothing(); } @@ -754,9 +740,9 @@ Maybe<gfx::Rect> SVGPathToAxisAlignedRect(Span<const StylePathCommand> aPath) { pathStart = segStart; break; } - case StylePathCommand::Tag::LineTo: { - Point to = cmd.line_to.point.ConvertsToGfxPoint(); - if (cmd.line_to.absolute == StyleIsAbsolute::No) { + case StylePathCommand::Tag::Line: { + Point to = cmd.line.point.ToGfxPoint(); + if (cmd.line.by_to == StyleByTo::By) { to = segStart + to; } @@ -766,9 +752,9 @@ Maybe<gfx::Rect> SVGPathToAxisAlignedRect(Span<const StylePathCommand> aPath) { segStart = to; break; } - case StylePathCommand::Tag::HorizontalLineTo: { - Point to = gfx::Point(cmd.horizontal_line_to.x, segStart.y); - if (cmd.horizontal_line_to.absolute == StyleIsAbsolute::No) { + case StylePathCommand::Tag::HLine: { + Point to = gfx::Point(cmd.h_line.x, segStart.y); + if (cmd.h_line.by_to == StyleByTo::By) { to.x += segStart.x; } @@ -778,9 +764,9 @@ Maybe<gfx::Rect> SVGPathToAxisAlignedRect(Span<const StylePathCommand> aPath) { segStart = to; break; } - case StylePathCommand::Tag::VerticalLineTo: { - Point to = gfx::Point(segStart.x, cmd.vertical_line_to.y); - if (cmd.horizontal_line_to.absolute == StyleIsAbsolute::No) { + case StylePathCommand::Tag::VLine: { + Point to = gfx::Point(segStart.x, cmd.v_line.y); + if (cmd.h_line.by_to == StyleByTo::By) { to.y += segStart.y; } diff --git a/dom/svg/SVGPathSegUtils.h b/dom/svg/SVGPathSegUtils.h index f0877d9a59..40a0952bfb 100644 --- a/dom/svg/SVGPathSegUtils.h +++ b/dom/svg/SVGPathSegUtils.h @@ -14,8 +14,8 @@ #include "nsDebug.h" namespace mozilla { - -struct StylePathCommand; +template <typename Angle, typename LP> +struct StyleGenericShapeCommand; #define NS_SVG_PATH_SEG_MAX_ARGS 7 #define NS_SVG_PATH_SEG_FIRST_VALID_TYPE \ @@ -264,8 +264,9 @@ class SVGPathSegUtils { * Traverse the given path segment and update the SVGPathTraversalState * object. This is identical to the above one but accepts StylePathCommand. */ - static void TraversePathSegment(const StylePathCommand& aCommand, - SVGPathTraversalState& aState); + static void TraversePathSegment( + const StyleGenericShapeCommand<float, float>& aCommand, + SVGPathTraversalState& aState); }; /// Detect whether the path represents a rectangle (for both filling AND @@ -280,7 +281,8 @@ class SVGPathSegUtils { /// practice). /// /// We could implement something similar for polygons. -Maybe<gfx::Rect> SVGPathToAxisAlignedRect(Span<const StylePathCommand> aPath); +Maybe<gfx::Rect> SVGPathToAxisAlignedRect( + Span<const StyleGenericShapeCommand<float, float>> aPath); } // namespace mozilla diff --git a/dom/svg/SVGSVGElement.cpp b/dom/svg/SVGSVGElement.cpp index f7282569f9..04f5b8662e 100644 --- a/dom/svg/SVGSVGElement.cpp +++ b/dom/svg/SVGSVGElement.cpp @@ -193,23 +193,30 @@ float SVGSVGElement::GetCurrentTimeAsFloat() { } void SVGSVGElement::SetCurrentTime(float seconds) { - if (mTimedDocumentRoot) { - // Make sure the timegraph is up-to-date - FlushAnimations(); - double fMilliseconds = double(seconds) * PR_MSEC_PER_SEC; - // Round to nearest whole number before converting, to avoid precision - // errors - SMILTime lMilliseconds = SVGUtils::ClampToInt64(NS_round(fMilliseconds)); - mTimedDocumentRoot->SetCurrentTime(lMilliseconds); - AnimationNeedsResample(); - // Trigger synchronous sample now, to: - // - Make sure we get an up-to-date paint after this method - // - re-enable event firing (it got disabled during seeking, and it - // doesn't get re-enabled until the first sample after the seek -- so - // let's make that happen now.) - FlushAnimations(); + if (!mTimedDocumentRoot) { + // we're not the outermost <svg> or not bound to a tree, so silently fail + return; } - // else we're not the outermost <svg> or not bound to a tree, so silently fail + // Make sure the timegraph is up-to-date + if (auto* currentDoc = GetComposedDoc()) { + currentDoc->FlushPendingNotifications(FlushType::Style); + } + if (!mTimedDocumentRoot) { + return; + } + FlushAnimations(); + double fMilliseconds = double(seconds) * PR_MSEC_PER_SEC; + // Round to nearest whole number before converting, to avoid precision + // errors + SMILTime lMilliseconds = SVGUtils::ClampToInt64(NS_round(fMilliseconds)); + mTimedDocumentRoot->SetCurrentTime(lMilliseconds); + AnimationNeedsResample(); + // Trigger synchronous sample now, to: + // - Make sure we get an up-to-date paint after this method + // - re-enable event firing (it got disabled during seeking, and it + // doesn't get re-enabled until the first sample after the seek -- so + // let's make that happen now.) + FlushAnimations(); } void SVGSVGElement::DeselectAll() { diff --git a/dom/svg/SVGUseElement.cpp b/dom/svg/SVGUseElement.cpp index 2db8649ba1..0bd9ce63db 100644 --- a/dom/svg/SVGUseElement.cpp +++ b/dom/svg/SVGUseElement.cpp @@ -127,6 +127,11 @@ void SVGUseElement::ProcessAttributeChange(int32_t aNamespaceID, } } +void SVGUseElement::DidAnimateAttribute(int32_t aNameSpaceID, + nsAtom* aAttribute) { + ProcessAttributeChange(aNameSpaceID, aAttribute); +} + void SVGUseElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aAttribute, const nsAttrValue* aValue, const nsAttrValue* aOldValue, diff --git a/dom/svg/SVGUseElement.h b/dom/svg/SVGUseElement.h index 3bdf3fc5bb..47b9db3e0c 100644 --- a/dom/svg/SVGUseElement.h +++ b/dom/svg/SVGUseElement.h @@ -143,6 +143,7 @@ class SVGUseElement final : public SVGUseElementBase, SVGUseElement* mOwningUseElement; }; + void DidAnimateAttribute(int32_t aNameSpaceID, nsAtom* aAttribute) override; SVGUseFrame* GetFrame() const; LengthAttributesInfo GetLengthInfo() override; diff --git a/dom/svg/crashtests/1858792.html b/dom/svg/crashtests/1858792.html new file mode 100644 index 0000000000..3cbdac2382 --- /dev/null +++ b/dom/svg/crashtests/1858792.html @@ -0,0 +1,15 @@ +<script> +window.onload = () => { + a.setAttribute("aria-setsize", "512") +} +function func() { + a.innerHTML = "A" +} +</script> +<svg> +<foreignObject id="a"> +<iframe></iframe> +</foreignObject> +<animate repeatDur="0s" onend="func()"></animate> +<animate attributeName="x" by="6%"> +<use href="#a"> diff --git a/dom/svg/crashtests/crashtests.list b/dom/svg/crashtests/crashtests.list index 683fae76cd..bda385bb3e 100644 --- a/dom/svg/crashtests/crashtests.list +++ b/dom/svg/crashtests/crashtests.list @@ -98,4 +98,5 @@ load 1572904.html load 1683907.html pref(dom.svg.pathSeg.enabled,true) load 1715387.html load 1837487.html +load 1858792.html load 1861736.html diff --git a/dom/svg/test/getCTM-helper.svg b/dom/svg/test/getCTM-helper.svg index 835efc5067..5a21feb99f 100644 --- a/dom/svg/test/getCTM-helper.svg +++ b/dom/svg/test/getCTM-helper.svg @@ -1,6 +1,7 @@ <?xml version="1.0"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="100" height="100" viewBox="-11 -22 100 100"> + <g id="base"> <g transform="translate(3, 4)"> <svg x="1" y="2" width="888" height="999"> <g> @@ -44,4 +45,5 @@ </g> </svg> </g> + </g> </svg> diff --git a/dom/svg/test/test_getCTM.html b/dom/svg/test/test_getCTM.html index 57ea79c861..332d935811 100644 --- a/dom/svg/test/test_getCTM.html +++ b/dom/svg/test/test_getCTM.html @@ -28,36 +28,36 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=366697 SimpleTest.waitForExplicitFinish(); function runTest() { - var doc = $("svg").contentWindow.document; + let doc = $("svg").contentWindow.document; /* Minimal */ - var buggy = doc.getElementById("buggy"); + let buggy = doc.getElementById("buggy"); is(buggy.getCTM().e, 30, "buggy.getCTM().e"); is(buggy.getCTM().f, 40, "buggy.getCTM().f"); - var transrect1 = document.getElementById("transrect1"); + let transrect1 = document.getElementById("transrect1"); is(transrect1.getCTM().a, 2, "transrect1.getCTM().a"); is(transrect1.getCTM().d, 3, "transrect1.getCTM().d"); - var padsvg1 = document.getElementById("padsvg1"); - var ctm = padsvg1.getScreenCTM(); - var rect = padsvg1.getBoundingClientRect(); + let padsvg1 = document.getElementById("padsvg1"); + let ctm = padsvg1.getScreenCTM(); + let rect = padsvg1.getBoundingClientRect(); // Use isfuzzy to ignore some miniscule floating-point precision error on // certain platforms: isfuzzy(ctm.e - rect.x, 27, 0.0001, "padsvg1.getScreenCTM().e"); is(ctm.f - rect.y, 43, "padsvg1.getScreenCTM().f"); - var root = doc.documentElement; - var inner = doc.getElementById("inner"); - var g1 = doc.getElementById("g1"); - var outer = doc.getElementById("outer"); - var outer2 = doc.getElementById("outer2"); - var g2 = doc.getElementById("g2"); - var g3 = doc.getElementById("g3"); - var g4 = doc.getElementById("g4"); - var g5 = doc.getElementById("g5"); - var symbolRect = doc.getElementById("symbolRect"); - var fO = doc.getElementById("fO"); + let root = doc.documentElement; + let inner = doc.getElementById("inner"); + let g1 = doc.getElementById("g1"); + let outer = doc.getElementById("outer"); + let outer2 = doc.getElementById("outer2"); + let g2 = doc.getElementById("g2"); + let g3 = doc.getElementById("g3"); + let g4 = doc.getElementById("g4"); + let g5 = doc.getElementById("g5"); + let symbolRect = doc.getElementById("symbolRect"); + let fO = doc.getElementById("fO"); /* Tests the consistency with nearestViewportElement (code is from test_viewport.html) */ // root.nearestViewportElement == null @@ -102,8 +102,8 @@ function runTest() { is((function() { try { return outer.getScreenCTM().e; } catch (e) { return e; } })(), 46, "outer.getScreenCTM().e"); is((function() { try { return outer.getScreenCTM().f; } catch (e) { return e; } })(), 69, "outer.getScreenCTM().f"); // outer.farthestViewportElement == null (but actually == root) - is((function() { try { return outer2.getScreenCTM().e; } catch (e) { return e; } })(), -19, "outer2.getScreenCTM().e"); - is((function() { try { return outer2.getScreenCTM().f; } catch (e) { return e; } })(), -8, "outer2.getScreenCTM().f"); + is((function() { try { return outer2.getScreenCTM().e; } catch (e) { return e; } })(), -4, "outer2.getScreenCTM().e"); + is((function() { try { return outer2.getScreenCTM().f; } catch (e) { return e; } })(), 19, "outer2.getScreenCTM().f"); // g2.farthestViewportElement == outer (but actually == root) is((function() { try { return g2.getScreenCTM().e; } catch (e) { return e; } })(), 646, "g2.getScreenCTM().e"); is((function() { try { return g2.getScreenCTM().f; } catch (e) { return e; } })(), 769, "g2.getScreenCTM().f"); diff --git a/dom/tests/browser/browser_windowProxy_transplant.js b/dom/tests/browser/browser_windowProxy_transplant.js index 8e6e0f8413..10cd96c9ee 100644 --- a/dom/tests/browser/browser_windowProxy_transplant.js +++ b/dom/tests/browser/browser_windowProxy_transplant.js @@ -12,12 +12,6 @@ const URL3 = `http://example.org/${PATH}`; // A bunch of boilerplate which needs to be dealt with. add_task(async function () { - // Turn on BC preservation and frameloader rebuilding to ensure that the - // BrowsingContext is preserved. - await SpecialPowers.pushPrefEnv({ - set: [["fission.preserve_browsing_contexts", true]], - }); - // Open a window with fission force-enabled in it. let win = await BrowserTestUtils.openNewBrowserWindow({ fission: true, diff --git a/dom/tests/mochitest/general/test_bug861217.html b/dom/tests/mochitest/general/test_bug861217.html index 1af3f58ff6..b758fe53e4 100644 --- a/dom/tests/mochitest/general/test_bug861217.html +++ b/dom/tests/mochitest/general/test_bug861217.html @@ -48,7 +48,34 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=861217 SimpleTest.waitForExplicitFinish(); /** Test for Bug 861217 **/ -function runTest() { +async function runTest() { + // This test needs to be run on environments where the zoom level == 1, but + // there are a couple of cases it's not run on such kind of environments. + // 1) run this test solely like mach mochitest dom/tests/mochitest/general/test_bug861217.html . + // In this case the zoom level is smaller than 1 since there's no meta viewport. + // 2) run test test along with other tests in the same directory. + // In this case this test runs inside an iframe in the mochitest harness' + // top level document which doesn't have any meta viewport either. + // To avoid these situations we forcibly set the zoom level 1 here. + const resolution = await SpecialPowers.spawn(window.top, [], () => { + return SpecialPowers.getDOMWindowUtils(content.window).getResolution(); + }); + + SimpleTest.registerCleanupFunction(async () => { + await SpecialPowers.spawn(window.top, [resolution], (aResolution) => { + SpecialPowers.getDOMWindowUtils(content.window).setResolutionAndScaleTo(aResolution); + }); + }); + await SpecialPowers.spawn(window.top, [], () => { + SpecialPowers.getDOMWindowUtils(content.window).setResolutionAndScaleTo(1.0); + }); + await SimpleTest.promiseWaitForCondition(async () => { + const resolution = await SpecialPowers.spawn(window.top, [], () => { + return SpecialPowers.getDOMWindowUtils(content.window).getResolution(); + }); + return resolution == 1.0; + }, "Waiting for zoom level 1.0"); + var tableCell1 = document.getElementById("tableCell1"), bcr1 = tableCell1.getBoundingClientRect(), tableCell2 = document.getElementById("tableCell2"), diff --git a/dom/tests/mochitest/general/test_interfaces.js b/dom/tests/mochitest/general/test_interfaces.js index fd15348532..e6ea910e14 100644 --- a/dom/tests/mochitest/general/test_interfaces.js +++ b/dom/tests/mochitest/general/test_interfaces.js @@ -190,6 +190,8 @@ let interfaceNamesInGlobalScope = [ // IMPORTANT: Do not change this list without review from a DOM peer! { name: "AudioDestinationNode", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "AudioEncoder", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! { name: "AudioListener", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "AudioNode", insecureContext: true }, @@ -326,6 +328,8 @@ let interfaceNamesInGlobalScope = [ // IMPORTANT: Do not change this list without review from a DOM peer! { name: "CSSRuleList", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "CSSStartingStyleRule", insecureContext: true, disabled: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! { name: "CSSStyleDeclaration", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "CSSStyleRule", insecureContext: true }, @@ -340,6 +344,8 @@ let interfaceNamesInGlobalScope = [ // IMPORTANT: Do not change this list without review from a DOM peer! { name: "CustomEvent", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "CustomStateSet", insecureContext: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! { name: "DecompressionStream", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "DataTransfer", insecureContext: true }, @@ -1370,6 +1376,12 @@ let interfaceNamesInGlobalScope = [ // IMPORTANT: Do not change this list without review from a DOM peer! { name: "TextEncoderStream", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! + { + name: "TextEvent", + insecureContext: true, + disabled: !SpecialPowers.getBoolPref("dom.events.textevent.enabled"), + }, + // IMPORTANT: Do not change this list without review from a DOM peer! { name: "TextMetrics", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "TextTrack", insecureContext: true }, @@ -1977,9 +1989,9 @@ let interfaceNamesInGlobalScope = [ // IMPORTANT: Do not change this list without review from a DOM peer! { name: "visualViewport", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! - { name: "WakeLock", earlyBetaOrEarlier: true }, + { name: "WakeLock" }, // IMPORTANT: Do not change this list without review from a DOM peer! - { name: "WakeLockSentinel", earlyBetaOrEarlier: true }, + { name: "WakeLockSentinel" }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "webkitURL", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! diff --git a/dom/tests/mochitest/localstorage/frameLocalStorageSessionOnly.html b/dom/tests/mochitest/localstorage/frameLocalStorageSessionOnly.html deleted file mode 100644 index f6c7f0291f..0000000000 --- a/dom/tests/mochitest/localstorage/frameLocalStorageSessionOnly.html +++ /dev/null @@ -1,8 +0,0 @@ -<!doctype html> -<html> - <body> - <script> - parent.postMessage(SpecialPowers.wrap(localStorage).isSessionOnly, "*"); - </script> - </body> -</html> diff --git a/dom/tests/mochitest/localstorage/mochitest.toml b/dom/tests/mochitest/localstorage/mochitest.toml index 07b9eb313a..c4758a7356 100644 --- a/dom/tests/mochitest/localstorage/mochitest.toml +++ b/dom/tests/mochitest/localstorage/mochitest.toml @@ -15,7 +15,6 @@ support-files = [ "interOriginTest.js", "interOriginTest2.js", "localStorageCommon.js", - "frameLocalStorageSessionOnly.html", "file_tryAccessSessionStorage.html", "windowProxy.html", ] diff --git a/dom/tests/mochitest/webcomponents/test_xul_custom_element.xhtml b/dom/tests/mochitest/webcomponents/test_xul_custom_element.xhtml index 0e172cb70d..7048163372 100644 --- a/dom/tests/mochitest/webcomponents/test_xul_custom_element.xhtml +++ b/dom/tests/mochitest/webcomponents/test_xul_custom_element.xhtml @@ -150,13 +150,13 @@ function basicElementCreateBuiltIn() { let element = document.createElementNS(XUL_NS, "axulelement", { is: "test-built-in-element" }); ok(element instanceof TestCustomBuiltInElement, "Should be an instance of TestCustomBuiltInElement"); - is(element.getAttribute("is"), "", "The |is| attribute of the created element should not be the extended type."); + is(element.getAttribute("is"), null, "The |is| attribute of the created element should not be the extended type."); document.querySelector("#content").appendChild(element); is(element.textContent, "baz", "Should have set the textContent"); let element2 = element.cloneNode(false); is(element2.localName, "axulelement", "Should see the right tag"); - is(element2.getAttribute("is"), "", "The |is| attribute of the created element should not be the extended type."); + is(element2.getAttribute("is"), null, "The |is| attribute of the created element should not be the extended type."); is(element2.textContent, "", "Shouldn't have cloned the textContent"); document.querySelector("#content").appendChild(element2); is(element2.textContent, "baz", "Should have set the textContent"); @@ -188,13 +188,13 @@ function subclassElementCreateBuiltIn() { let element = document.createElementNS(XUL_NS, "menupopup", { is: "test-popup-extend" }); ok(element instanceof TestPopupExtendElement, "Should be an instance of TestPopupExtendElement"); - is(element.getAttribute("is"), "", "The |is| attribute of the created element should not be the extended type."); + is(element.getAttribute("is"), null, "The |is| attribute of the created element should not be the extended type."); document.querySelector("#content").appendChild(element); is(element.textContent, "quuz", "Should have set the textContent"); let element2 = element.cloneNode(false); is(element2.localName, "menupopup", "Should see the right tag"); - is(element2.getAttribute("is"), "", "The |is| attribute of the created element should not be the extended type."); + is(element2.getAttribute("is"), null, "The |is| attribute of the created element should not be the extended type."); is(element2.textContent, "", "Shouldn't have cloned the textContent"); document.querySelector("#content").appendChild(element2); is(element2.textContent, "quuz", "Should have set the textContent"); @@ -268,13 +268,13 @@ let element = document.createElementNS(XUL_NS, "testwithoutdash", { is: "testwithoutdash-extended" }); ok(element instanceof TestWithoutDashExtended, "Should be an instance of TestWithoutDashExtended"); ok(element instanceof TestWithoutDash, "Should be an instance of TestWithoutDash"); - is(element.getAttribute("is"), "", "The |is| attribute of the created element should not be the extended type."); + is(element.getAttribute("is"), null, "The |is| attribute of the created element should not be the extended type."); document.querySelector("#content").appendChild(element); is(element.textContent, "quux", "Should have set the textContent"); let element2 = element.cloneNode(false); is(element2.localName, "testwithoutdash", "Should see the right tag"); - is(element2.getAttribute("is"), "", "The |is| attribute of the created element should not be the extended type."); + is(element2.getAttribute("is"), null, "The |is| attribute of the created element should not be the extended type."); is(element2.textContent, "", "Shouldn't have cloned the textContent"); document.querySelector("#content").appendChild(element2); is(element2.textContent, "quux", "Should have set the textContent"); diff --git a/dom/tests/reftest/prettyprint-space-ref.xhtml b/dom/tests/reftest/prettyprint-space-ref.xhtml new file mode 100644 index 0000000000..9169b9236e --- /dev/null +++ b/dom/tests/reftest/prettyprint-space-ref.xhtml @@ -0,0 +1,67 @@ +<out> + <div id="top" xmlns="http://www.w3.org/1999/xhtml"> + <link href="chrome://global/content/xml/XMLPrettyPrint.css" type="text/css" rel="stylesheet"/> + <div id="header"> + <p> + This XML file does not appear to have any style information associated with it. The document tree is shown below. + </p> + </div> + <main id="tree" class="highlight"> + <div> + <details open="" class="expandable-body"> + <summary class="expandable-opening"> + <<span class="start-tag">out</span>> + </summary> + <div class="expandable-children"> + <span class="text">Here be sea hags. </span> + <div> + <details open="" class="expandable-body"> + <summary class="expandable-opening"> + <<span class="start-tag">b</span> <span class="attribute-name">xml:space</span>=<span class="attribute-value">"preserve"</span>> + </summary> + <div class="expandable-children"> + <span class="text" style="white-space: pre-wrap">They need a lot of space, this much: .</span> + <div> + <details open="" class="expandable-body"> + <summary class="expandable-opening"> + <<span class="start-tag">c</span>> + </summary> + <div class="expandable-children"> + <span class="text" style="white-space: pre-wrap">At least, most of them do: .</span> + <div> + <details open="" class="expandable-body"> + <summary class="expandable-opening"> + <<span class="start-tag">d</span> <span class="attribute-name">xml:space</span>=<span class="attribute-value">"default"</span>> + </summary> + <div class="expandable-children"> + <span class="text" style="white-space: normal">But some of them don't, they only need one: . </span> + <div> + <<span class="start-tag">e</span> <span class="attribute-name">xml:space</span>=<span class="attribute-value">"preserve"</span>><span class="text" style="white-space: pre-wrap">But their children might need more: .</span></<span class="end-tag">e</span>> + </div> + </div> + </details> + <span class="expandable-closing"> + </<span class="end-tag">d</span>> + </span> + </div> + </div> + </details> + <span class="expandable-closing"> + </<span class="end-tag">c</span>> + </span> + </div> + </div> + </details> + <span class="expandable-closing"> + </<span class="end-tag">b</span>> + </span> + </div> + </div> + </details> + <span class="expandable-closing"> + </<span class="end-tag">out</span>> + </span> + </div> + </main> + </div> +</out> diff --git a/dom/tests/reftest/prettyprint-space.xml b/dom/tests/reftest/prettyprint-space.xml new file mode 100644 index 0000000000..4f995608f8 --- /dev/null +++ b/dom/tests/reftest/prettyprint-space.xml @@ -0,0 +1 @@ +<out>Here be sea hags. <b xml:space="preserve">They need a lot of space, this much: . <c>At least, most of them do: . <d xml:space="default">But some of them don't, they only need one: . <e xml:space="preserve">But their children might need more: .</e></d></c></b></out> diff --git a/dom/tests/reftest/reftest.list b/dom/tests/reftest/reftest.list index ee7069e2af..41f6ff6644 100644 --- a/dom/tests/reftest/reftest.list +++ b/dom/tests/reftest/reftest.list @@ -12,3 +12,4 @@ fuzzy(0-1,0-5) == bug559996.html bug559996-ref.html == bug592366-2.xhtml bug592366-ref.xhtml == bug798068.xhtml bug798068-ref.xhtml == bug1389406.xml bug1389406-ref.xml +== prettyprint-space.xml prettyprint-space-ref.xhtml diff --git a/dom/tests/unit/test_PromiseDebugging.js b/dom/tests/unit/test_PromiseDebugging.js index 6148b65c75..d8cd4209fa 100644 --- a/dom/tests/unit/test_PromiseDebugging.js +++ b/dom/tests/unit/test_PromiseDebugging.js @@ -1,5 +1,4 @@ function run_test() { - // Hack around Promise.jsm being stuck on my global Assert.equal(false, PromiseDebugging === undefined); var res; var p = new Promise(function (resolve) { diff --git a/dom/tests/unit/test_xhr_init.js b/dom/tests/unit/test_xhr_init.js index a2eaf2b4d7..0f7255806e 100644 --- a/dom/tests/unit/test_xhr_init.js +++ b/dom/tests/unit/test_xhr_init.js @@ -8,6 +8,14 @@ function run_test() { Assert.ok(x.mozSystem); x = new XMLHttpRequest(); + Assert.ok(x.mozAnon); + Assert.ok(x.mozSystem); + + Services.prefs.setBoolPref( + "network.fetch.systemDefaultsToOmittingCredentials", + false + ); + x = new XMLHttpRequest(); Assert.ok(!x.mozAnon); Assert.ok(x.mozSystem); } diff --git a/dom/url/URL.cpp b/dom/url/URL.cpp index 799bb72cff..1011eda210 100644 --- a/dom/url/URL.cpp +++ b/dom/url/URL.cpp @@ -36,8 +36,8 @@ JSObject* URL::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { /* static */ already_AddRefed<URL> URL::Constructor(const GlobalObject& aGlobal, - const nsAString& aURL, - const Optional<nsAString>& aBase, + const nsACString& aURL, + const Optional<nsACString>& aBase, ErrorResult& aRv) { if (aBase.WasPassed()) { return Constructor(aGlobal.GetAsSupports(), aURL, aBase.Value(), aRv); @@ -48,20 +48,13 @@ already_AddRefed<URL> URL::Constructor(const GlobalObject& aGlobal, /* static */ already_AddRefed<URL> URL::Constructor(nsISupports* aParent, - const nsAString& aURL, - const nsAString& aBase, + const nsACString& aURL, + const nsACString& aBase, ErrorResult& aRv) { - // Don't use NS_ConvertUTF16toUTF8 because that doesn't let us handle OOM. - nsAutoCString base; - if (!AppendUTF16toUTF8(aBase, base, fallible)) { - aRv.Throw(NS_ERROR_OUT_OF_MEMORY); - return nullptr; - } - nsCOMPtr<nsIURI> baseUri; - nsresult rv = NS_NewURI(getter_AddRefs(baseUri), base); + nsresult rv = NS_NewURI(getter_AddRefs(baseUri), aBase); if (NS_WARN_IF(NS_FAILED(rv))) { - aRv.ThrowTypeError<MSG_INVALID_URL>(base); + aRv.ThrowTypeError<MSG_INVALID_URL>(aBase); return nullptr; } @@ -70,21 +63,14 @@ already_AddRefed<URL> URL::Constructor(nsISupports* aParent, /* static */ already_AddRefed<URL> URL::Constructor(nsISupports* aParent, - const nsAString& aURL, nsIURI* aBase, + const nsACString& aURL, nsIURI* aBase, ErrorResult& aRv) { - // Don't use NS_ConvertUTF16toUTF8 because that doesn't let us handle OOM. - nsAutoCString urlStr; - if (!AppendUTF16toUTF8(aURL, urlStr, fallible)) { - aRv.Throw(NS_ERROR_OUT_OF_MEMORY); - return nullptr; - } - nsCOMPtr<nsIURI> uri; - nsresult rv = NS_NewURI(getter_AddRefs(uri), urlStr, nullptr, aBase); + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, nullptr, aBase); if (NS_FAILED(rv)) { // No need to warn in this case. It's common to use the URL constructor // to determine if a URL is valid and an exception will be propagated. - aRv.ThrowTypeError<MSG_INVALID_URL>(urlStr); + aRv.ThrowTypeError<MSG_INVALID_URL>(aURL); return nullptr; } @@ -96,7 +82,7 @@ already_AddRefed<URL> URL::FromURI(GlobalObject& aGlobal, nsIURI* aURI) { } void URL::CreateObjectURL(const GlobalObject& aGlobal, Blob& aBlob, - nsAString& aResult, ErrorResult& aRv) { + nsACString& aResult, ErrorResult& aRv) { if (NS_IsMainThread()) { URLMainThread::CreateObjectURL(aGlobal, aBlob, aResult, aRv); } else { @@ -105,12 +91,12 @@ void URL::CreateObjectURL(const GlobalObject& aGlobal, Blob& aBlob, } void URL::CreateObjectURL(const GlobalObject& aGlobal, MediaSource& aSource, - nsAString& aResult, ErrorResult& aRv) { + nsACString& aResult, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); URLMainThread::CreateObjectURL(aGlobal, aSource, aResult, aRv); } -void URL::RevokeObjectURL(const GlobalObject& aGlobal, const nsAString& aURL, +void URL::RevokeObjectURL(const GlobalObject& aGlobal, const nsACString& aURL, ErrorResult& aRv) { if (aURL.Contains('#')) { // Don't revoke URLs that contain fragments. @@ -124,7 +110,7 @@ void URL::RevokeObjectURL(const GlobalObject& aGlobal, const nsAString& aURL, } } -bool URL::IsValidObjectURL(const GlobalObject& aGlobal, const nsAString& aURL, +bool URL::IsValidObjectURL(const GlobalObject& aGlobal, const nsACString& aURL, ErrorResult& aRv) { if (NS_IsMainThread()) { return URLMainThread::IsValidObjectURL(aGlobal, aURL, aRv); @@ -132,32 +118,40 @@ bool URL::IsValidObjectURL(const GlobalObject& aGlobal, const nsAString& aURL, return URLWorker::IsValidObjectURL(aGlobal, aURL, aRv); } -bool URL::CanParse(const GlobalObject& aGlobal, const nsAString& aURL, - const Optional<nsAString>& aBase) { +already_AddRefed<nsIURI> URL::ParseURI(const nsACString& aURL, + const Optional<nsACString>& aBase) { nsCOMPtr<nsIURI> baseUri; - if (aBase.WasPassed()) { - // Don't use NS_ConvertUTF16toUTF8 because that doesn't let us handle OOM. - nsAutoCString base; - if (!AppendUTF16toUTF8(aBase.Value(), base, fallible)) { - // Just return false with OOM errors as no ErrorResult. - return false; - } + nsCOMPtr<nsIURI> uri; - nsresult rv = NS_NewURI(getter_AddRefs(baseUri), base); + if (aBase.WasPassed()) { + nsresult rv = NS_NewURI(getter_AddRefs(baseUri), aBase.Value()); if (NS_FAILED(rv)) { - // Invalid base URL, return false. - return false; + return nullptr; } } - nsAutoCString urlStr; - if (!AppendUTF16toUTF8(aURL, urlStr, fallible)) { - // Just return false with OOM errors as no ErrorResult. - return false; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, nullptr, baseUri); + if (NS_FAILED(rv)) { + return nullptr; } - nsCOMPtr<nsIURI> uri; - return NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), urlStr, nullptr, baseUri)); + return uri.forget(); +}; + +already_AddRefed<URL> URL::Parse(const GlobalObject& aGlobal, + const nsACString& aURL, + const Optional<nsACString>& aBase) { + nsCOMPtr<nsIURI> uri = ParseURI(aURL, aBase); + if (!uri) { + return nullptr; + } + return MakeAndAddRef<URL>(aGlobal.GetAsSupports(), std::move(uri)); +} + +bool URL::CanParse(const GlobalObject& aGlobal, const nsACString& aURL, + const Optional<nsACString>& aBase) { + nsCOMPtr<nsIURI> uri = ParseURI(aURL, aBase); + return !!uri; } URLSearchParams* URL::SearchParams() { @@ -174,7 +168,7 @@ void URL::CreateSearchParamsIfNeeded() { } } -void URL::SetSearch(const nsAString& aSearch) { +void URL::SetSearch(const nsACString& aSearch) { SetSearchInternal(aSearch); UpdateURLSearchParams(); } @@ -183,35 +177,22 @@ void URL::URLSearchParamsUpdated(URLSearchParams* aSearchParams) { MOZ_ASSERT(mSearchParams); MOZ_ASSERT(mSearchParams == aSearchParams); - nsAutoString search; + nsAutoCString search; mSearchParams->Serialize(search); - SetSearchInternal(search); } -#define URL_GETTER(value, func) \ - MOZ_ASSERT(mURI); \ - value.Truncate(); \ - nsAutoCString tmp; \ - nsresult rv = mURI->func(tmp); \ - if (NS_SUCCEEDED(rv)) { \ - CopyUTF8toUTF16(tmp, value); \ - } - -void URL::GetHref(nsAString& aHref) const { URL_GETTER(aHref, GetSpec); } +#define URL_GETTER(value, func) \ + MOZ_ASSERT(mURI); \ + mURI->func(value); -void URL::SetHref(const nsAString& aHref, ErrorResult& aRv) { - // Don't use NS_ConvertUTF16toUTF8 because that doesn't let us handle OOM. - nsAutoCString href; - if (!AppendUTF16toUTF8(aHref, href, fallible)) { - aRv.Throw(NS_ERROR_OUT_OF_MEMORY); - return; - } +void URL::GetHref(nsACString& aHref) const { URL_GETTER(aHref, GetSpec); } +void URL::SetHref(const nsACString& aHref, ErrorResult& aRv) { nsCOMPtr<nsIURI> uri; - nsresult rv = NS_NewURI(getter_AddRefs(uri), href); + nsresult rv = NS_NewURI(getter_AddRefs(uri), aHref); if (NS_FAILED(rv)) { - aRv.ThrowTypeError<MSG_INVALID_URL>(href); + aRv.ThrowTypeError<MSG_INVALID_URL>(aHref); return; } @@ -219,7 +200,7 @@ void URL::SetHref(const nsAString& aHref, ErrorResult& aRv) { UpdateURLSearchParams(); } -void URL::GetOrigin(nsAString& aOrigin) const { +void URL::GetOrigin(nsACString& aOrigin) const { nsresult rv = nsContentUtils::GetWebExposedOriginSerialization(URI(), aOrigin); if (NS_WARN_IF(NS_FAILED(rv))) { @@ -227,12 +208,12 @@ void URL::GetOrigin(nsAString& aOrigin) const { } } -void URL::GetProtocol(nsAString& aProtocol) const { +void URL::GetProtocol(nsACString& aProtocol) const { URL_GETTER(aProtocol, GetScheme); aProtocol.Append(char16_t(':')); } -void URL::SetProtocol(const nsAString& aProtocol) { +void URL::SetProtocol(const nsACString& aProtocol) { nsCOMPtr<nsIURI> uri(URI()); if (!uri) { return; @@ -244,81 +225,67 @@ void URL::SetProtocol(const nsAString& aProtocol) { mURI = std::move(uri); } -void URL::GetUsername(nsAString& aUsername) const { +void URL::GetUsername(nsACString& aUsername) const { URL_GETTER(aUsername, GetUsername); } -void URL::SetUsername(const nsAString& aUsername) { +void URL::SetUsername(const nsACString& aUsername) { MOZ_ASSERT(mURI); - - Unused << NS_MutateURI(mURI) - .SetUsername(NS_ConvertUTF16toUTF8(aUsername)) - .Finalize(mURI); + Unused << NS_MutateURI(mURI).SetUsername(aUsername).Finalize(mURI); } -void URL::GetPassword(nsAString& aPassword) const { +void URL::GetPassword(nsACString& aPassword) const { URL_GETTER(aPassword, GetPassword); } -void URL::SetPassword(const nsAString& aPassword) { +void URL::SetPassword(const nsACString& aPassword) { MOZ_ASSERT(mURI); - Unused << NS_MutateURI(mURI) - .SetPassword(NS_ConvertUTF16toUTF8(aPassword)) - .Finalize(mURI); + Unused << NS_MutateURI(mURI).SetPassword(aPassword).Finalize(mURI); } -void URL::GetHost(nsAString& aHost) const { URL_GETTER(aHost, GetHostPort); } +void URL::GetHost(nsACString& aHost) const { URL_GETTER(aHost, GetHostPort); } -void URL::SetHost(const nsAString& aHost) { +void URL::SetHost(const nsACString& aHost) { MOZ_ASSERT(mURI); - - Unused << NS_MutateURI(mURI) - .SetHostPort(NS_ConvertUTF16toUTF8(aHost)) - .Finalize(mURI); + Unused << NS_MutateURI(mURI).SetHostPort(aHost).Finalize(mURI); } -void URL::GetHostname(nsAString& aHostname) const { +void URL::GetHostname(nsACString& aHostname) const { MOZ_ASSERT(mURI); - aHostname.Truncate(); nsContentUtils::GetHostOrIPv6WithBrackets(mURI, aHostname); } -void URL::SetHostname(const nsAString& aHostname) { +void URL::SetHostname(const nsACString& aHostname) { MOZ_ASSERT(mURI); // nsStandardURL returns NS_ERROR_UNEXPECTED for an empty hostname // The return code is silently ignored - mozilla::Unused << NS_MutateURI(mURI) - .SetHost(NS_ConvertUTF16toUTF8(aHostname)) - .Finalize(mURI); + Unused << NS_MutateURI(mURI).SetHost(aHostname).Finalize(mURI); } -void URL::GetPort(nsAString& aPort) const { +void URL::GetPort(nsACString& aPort) const { MOZ_ASSERT(mURI); - aPort.Truncate(); int32_t port; nsresult rv = mURI->GetPort(&port); if (NS_SUCCEEDED(rv) && port != -1) { - nsAutoString portStr; - portStr.AppendInt(port, 10); - aPort.Assign(portStr); + aPort.AppendInt(port, 10); } } -void URL::SetPort(const nsAString& aPort) { +void URL::SetPort(const nsACString& aPort) { nsresult rv; - nsAutoString portStr(aPort); + nsAutoCString portStr(aPort); int32_t port = -1; // nsIURI uses -1 as default value. portStr.StripTaggedASCII(ASCIIMask::MaskCRLFTab()); if (!portStr.IsEmpty()) { // To be valid, the port must start with an ASCII digit. - // (nsAString::ToInteger ignores leading junk, so check before calling.) + // (nsACString::ToInteger ignores leading junk, so check before calling.) if (!IsAsciiDigit(portStr[0])) { return; } @@ -331,32 +298,21 @@ void URL::SetPort(const nsAString& aPort) { Unused << NS_MutateURI(mURI).SetPort(port).Finalize(mURI); } -void URL::GetPathname(nsAString& aPathname) const { +void URL::GetPathname(nsACString& aPathname) const { MOZ_ASSERT(mURI); - - aPathname.Truncate(); - // Do not throw! Not having a valid URI or URL should result in an empty // string. - - nsAutoCString file; - nsresult rv = mURI->GetFilePath(file); - if (NS_SUCCEEDED(rv)) { - CopyUTF8toUTF16(file, aPathname); - } + mURI->GetFilePath(aPathname); } -void URL::SetPathname(const nsAString& aPathname) { +void URL::SetPathname(const nsACString& aPathname) { MOZ_ASSERT(mURI); // Do not throw! - - Unused << NS_MutateURI(mURI) - .SetFilePath(NS_ConvertUTF16toUTF8(aPathname)) - .Finalize(mURI); + Unused << NS_MutateURI(mURI).SetFilePath(aPathname).Finalize(mURI); } -void URL::GetSearch(nsAString& aSearch) const { +void URL::GetSearch(nsACString& aSearch) const { MOZ_ASSERT(mURI); aSearch.Truncate(); @@ -364,44 +320,33 @@ void URL::GetSearch(nsAString& aSearch) const { // Do not throw! Not having a valid URI or URL should result in an empty // string. - nsAutoCString search; nsresult rv; - - rv = mURI->GetQuery(search); - if (NS_SUCCEEDED(rv) && !search.IsEmpty()) { - aSearch.Assign(u'?'); - AppendUTF8toUTF16(search, aSearch); + rv = mURI->GetQuery(aSearch); + if (NS_SUCCEEDED(rv) && !aSearch.IsEmpty()) { + aSearch.Insert('?', 0); } } -void URL::GetHash(nsAString& aHash) const { +void URL::GetHash(nsACString& aHash) const { MOZ_ASSERT(mURI); - aHash.Truncate(); - - nsAutoCString ref; - nsresult rv = mURI->GetRef(ref); - if (NS_SUCCEEDED(rv) && !ref.IsEmpty()) { - aHash.Assign(char16_t('#')); - AppendUTF8toUTF16(ref, aHash); + nsresult rv = mURI->GetRef(aHash); + if (NS_SUCCEEDED(rv) && !aHash.IsEmpty()) { + aHash.Insert('#', 0); } } -void URL::SetHash(const nsAString& aHash) { +void URL::SetHash(const nsACString& aHash) { MOZ_ASSERT(mURI); - Unused - << NS_MutateURI(mURI).SetRef(NS_ConvertUTF16toUTF8(aHash)).Finalize(mURI); + Unused << NS_MutateURI(mURI).SetRef(aHash).Finalize(mURI); } -void URL::SetSearchInternal(const nsAString& aSearch) { +void URL::SetSearchInternal(const nsACString& aSearch) { MOZ_ASSERT(mURI); // Ignore failures to be compatible with NS4. - - Unused << NS_MutateURI(mURI) - .SetQuery(NS_ConvertUTF16toUTF8(aSearch)) - .Finalize(mURI); + Unused << NS_MutateURI(mURI).SetQuery(aSearch).Finalize(mURI); } void URL::UpdateURLSearchParams() { diff --git a/dom/url/URL.h b/dom/url/URL.h index 7c2563d095..cf72a7be63 100644 --- a/dom/url/URL.h +++ b/dom/url/URL.h @@ -44,79 +44,73 @@ class URL final : public URLSearchParamsObserver, public nsWrapperCache { JS::Handle<JSObject*> aGivenProto) override; static already_AddRefed<URL> Constructor(const GlobalObject& aGlobal, - const nsAString& aURL, - const Optional<nsAString>& aBase, + const nsACString& aURL, + const Optional<nsACString>& aBase, ErrorResult& aRv); static already_AddRefed<URL> Constructor(nsISupports* aParent, - const nsAString& aURL, - const nsAString& aBase, + const nsACString& aURL, + const nsACString& aBase, ErrorResult& aRv); static already_AddRefed<URL> Constructor(nsISupports* aParent, - const nsAString& aURL, nsIURI* aBase, - ErrorResult& aRv); + const nsACString& aURL, + nsIURI* aBase, ErrorResult& aRv); static void CreateObjectURL(const GlobalObject& aGlobal, Blob& aBlob, - nsAString& aResult, ErrorResult& aRv); + nsACString& aResult, ErrorResult& aRv); static void CreateObjectURL(const GlobalObject& aGlobal, MediaSource& aSource, - nsAString& aResult, ErrorResult& aRv); + nsACString& aResult, ErrorResult& aRv); static void RevokeObjectURL(const GlobalObject& aGlobal, - const nsAString& aURL, ErrorResult& aRv); + const nsACString& aURL, ErrorResult& aRv); static bool IsValidObjectURL(const GlobalObject& aGlobal, - const nsAString& aURL, ErrorResult& aRv); - - static bool CanParse(const GlobalObject& aGlobal, const nsAString& aURL, - const Optional<nsAString>& aBase); - - void GetHref(nsAString& aHref) const; - - void SetHref(const nsAString& aHref, ErrorResult& aRv); - - void GetOrigin(nsAString& aOrigin) const; - - void GetProtocol(nsAString& aProtocol) const; + const nsACString& aURL, ErrorResult& aRv); - void SetProtocol(const nsAString& aProtocol); + static already_AddRefed<URL> Parse(const GlobalObject& aGlobal, + const nsACString& aURL, + const Optional<nsACString>& aBase); - void GetUsername(nsAString& aUsername) const; + static bool CanParse(const GlobalObject& aGlobal, const nsACString& aURL, + const Optional<nsACString>& aBase); - void SetUsername(const nsAString& aUsername); + void GetHref(nsACString& aHref) const; + void SetHref(const nsACString& aHref, ErrorResult& aRv); - void GetPassword(nsAString& aPassword) const; + void GetOrigin(nsACString& aOrigin) const; - void SetPassword(const nsAString& aPassword); + void GetProtocol(nsACString& aProtocol) const; + void SetProtocol(const nsACString& aProtocol); - void GetHost(nsAString& aHost) const; + void GetUsername(nsACString& aUsername) const; + void SetUsername(const nsACString& aUsername); - void SetHost(const nsAString& aHost); + void GetPassword(nsACString& aPassword) const; + void SetPassword(const nsACString& aPassword); - void GetHostname(nsAString& aHostname) const; + void GetHost(nsACString& aHost) const; + void SetHost(const nsACString& aHost); - void SetHostname(const nsAString& aHostname); + void GetHostname(nsACString& aHostname) const; + void SetHostname(const nsACString& aHostname); - void GetPort(nsAString& aPort) const; + void GetPort(nsACString& aPort) const; + void SetPort(const nsACString& aPort); - void SetPort(const nsAString& aPort); + void GetPathname(nsACString& aPathname) const; + void SetPathname(const nsACString& aPathname); - void GetPathname(nsAString& aPathname) const; - - void SetPathname(const nsAString& aPathname); - - void GetSearch(nsAString& aSearch) const; - - void SetSearch(const nsAString& aSearch); + void GetSearch(nsACString& aSearch) const; + void SetSearch(const nsACString& aSearch); URLSearchParams* SearchParams(); - void GetHash(nsAString& aHost) const; + void GetHash(nsACString& aHash) const; + void SetHash(const nsACString& aHash); - void SetHash(const nsAString& aHash); - - void ToJSON(nsAString& aResult) const { GetHref(aResult); } + void ToJSON(nsACString& aResult) const { GetHref(aResult); } // URLSearchParamsObserver void URLSearchParamsUpdated(URLSearchParams* aSearchParams) override; @@ -127,10 +121,13 @@ class URL final : public URLSearchParamsObserver, public nsWrapperCache { private: ~URL() = default; + static already_AddRefed<nsIURI> ParseURI(const nsACString& aURL, + const Optional<nsACString>& aBase); + void UpdateURLSearchParams(); private: - void SetSearchInternal(const nsAString& aSearch); + void SetSearchInternal(const nsACString& aSearch); void CreateSearchParamsIfNeeded(); diff --git a/dom/url/URLMainThread.cpp b/dom/url/URLMainThread.cpp index 95afd0db45..fb57be4401 100644 --- a/dom/url/URLMainThread.cpp +++ b/dom/url/URLMainThread.cpp @@ -19,7 +19,7 @@ namespace mozilla::dom { /* static */ void URLMainThread::CreateObjectURL(const GlobalObject& aGlobal, Blob& aBlob, - nsAString& aResult, ErrorResult& aRv) { + nsACString& aResult, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); @@ -41,20 +41,18 @@ void URLMainThread::CreateObjectURL(const GlobalObject& aGlobal, Blob& aBlob, nsCOMPtr<nsIPrincipal> principal = nsContentUtils::ObjectPrincipal(aGlobal.Get()); - nsAutoCString url; aRv = BlobURLProtocolHandler::AddDataEntry( - aBlob.Impl(), principal, NS_ConvertUTF16toUTF8(partKey), url); + aBlob.Impl(), principal, NS_ConvertUTF16toUTF8(partKey), aResult); if (NS_WARN_IF(aRv.Failed())) { return; } - global->RegisterHostObjectURI(url); - CopyASCIItoUTF16(url, aResult); + global->RegisterHostObjectURI(aResult); } /* static */ void URLMainThread::CreateObjectURL(const GlobalObject& aGlobal, - MediaSource& aSource, nsAString& aResult, + MediaSource& aSource, nsACString& aResult, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); @@ -77,25 +75,23 @@ void URLMainThread::CreateObjectURL(const GlobalObject& aGlobal, nsCOMPtr<nsIPrincipal> principal = nsContentUtils::ObjectPrincipal(aGlobal.Get()); - nsAutoCString url; aRv = BlobURLProtocolHandler::AddDataEntry( - &aSource, principal, NS_ConvertUTF16toUTF8(partKey), url); + &aSource, principal, NS_ConvertUTF16toUTF8(partKey), aResult); if (NS_WARN_IF(aRv.Failed())) { return; } nsCOMPtr<nsIRunnable> revocation = NS_NewRunnableFunction( - "dom::URLMainThread::CreateObjectURL", - [url] { BlobURLProtocolHandler::RemoveDataEntry(url); }); + "dom::URLMainThread::CreateObjectURL", [result = nsCString(aResult)] { + BlobURLProtocolHandler::RemoveDataEntry(result); + }); nsContentUtils::RunInStableState(revocation.forget()); - - CopyASCIItoUTF16(url, aResult); } /* static */ void URLMainThread::RevokeObjectURL(const GlobalObject& aGlobal, - const nsAString& aURL, ErrorResult& aRv) { + const nsACString& aURL, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); if (!global) { @@ -113,21 +109,18 @@ void URLMainThread::RevokeObjectURL(const GlobalObject& aGlobal, } } - NS_LossyConvertUTF16toASCII asciiurl(aURL); - if (BlobURLProtocolHandler::RemoveDataEntry( - asciiurl, nsContentUtils::ObjectPrincipal(aGlobal.Get()), + aURL, nsContentUtils::ObjectPrincipal(aGlobal.Get()), NS_ConvertUTF16toUTF8(partKey))) { - global->UnregisterHostObjectURI(asciiurl); + global->UnregisterHostObjectURI(aURL); } } /* static */ bool URLMainThread::IsValidObjectURL(const GlobalObject& aGlobal, - const nsAString& aURL, ErrorResult& aRv) { + const nsACString& aURL, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); - NS_LossyConvertUTF16toASCII asciiurl(aURL); - return BlobURLProtocolHandler::HasDataEntry(asciiurl); + return BlobURLProtocolHandler::HasDataEntry(aURL); } } // namespace mozilla::dom diff --git a/dom/url/URLMainThread.h b/dom/url/URLMainThread.h index 5314f2b3e0..1d419aab78 100644 --- a/dom/url/URLMainThread.h +++ b/dom/url/URLMainThread.h @@ -14,16 +14,13 @@ namespace mozilla::dom { class URLMainThread final { public: static void CreateObjectURL(const GlobalObject& aGlobal, Blob& aBlob, - nsAString& aResult, ErrorResult& aRv); - + nsACString& aResult, ErrorResult& aRv); static void CreateObjectURL(const GlobalObject& aGlobal, MediaSource& aSource, - nsAString& aResult, ErrorResult& aRv); - + nsACString& aResult, ErrorResult& aRv); static void RevokeObjectURL(const GlobalObject& aGlobal, - const nsAString& aURL, ErrorResult& aRv); - + const nsACString& aURL, ErrorResult& aRv); static bool IsValidObjectURL(const GlobalObject& aGlobal, - const nsAString& aURL, ErrorResult& aRv); + const nsACString& aURL, ErrorResult& aRv); }; } // namespace mozilla::dom diff --git a/dom/url/URLSearchParams.cpp b/dom/url/URLSearchParams.cpp index d43217c8b6..c141766e83 100644 --- a/dom/url/URLSearchParams.cpp +++ b/dom/url/URLSearchParams.cpp @@ -60,23 +60,24 @@ JSObject* URLSearchParams::WrapObject(JSContext* aCx, /* static */ already_AddRefed<URLSearchParams> URLSearchParams::Constructor( const GlobalObject& aGlobal, - const USVStringSequenceSequenceOrUSVStringUSVStringRecordOrUSVString& aInit, + const UTF8StringSequenceSequenceOrUTF8StringUTF8StringRecordOrUTF8String& + aInit, ErrorResult& aRv) { RefPtr<URLSearchParams> sp = new URLSearchParams(aGlobal.GetAsSupports(), nullptr); - if (aInit.IsUSVString()) { - NS_ConvertUTF16toUTF8 input(aInit.GetAsUSVString()); + if (aInit.IsUTF8String()) { + const auto& input = aInit.GetAsUTF8String(); if (StringBeginsWith(input, "?"_ns)) { sp->ParseInput(Substring(input, 1, input.Length() - 1)); } else { sp->ParseInput(input); } - } else if (aInit.IsUSVStringSequenceSequence()) { - const Sequence<Sequence<nsString>>& list = - aInit.GetAsUSVStringSequenceSequence(); + } else if (aInit.IsUTF8StringSequenceSequence()) { + const Sequence<Sequence<nsCString>>& list = + aInit.GetAsUTF8StringSequenceSequence(); for (uint32_t i = 0; i < list.Length(); ++i) { - const Sequence<nsString>& item = list[i]; + const Sequence<nsCString>& item = list[i]; if (item.Length() != 2) { nsPrintfCString err("Expected 2 items in pair but got %zu", item.Length()); @@ -85,9 +86,9 @@ already_AddRefed<URLSearchParams> URLSearchParams::Constructor( } sp->Append(item[0], item[1]); } - } else if (aInit.IsUSVStringUSVStringRecord()) { - const Record<nsString, nsString>& record = - aInit.GetAsUSVStringUSVStringRecord(); + } else if (aInit.IsUTF8StringUTF8StringRecord()) { + const Record<nsCString, nsCString>& record = + aInit.GetAsUTF8StringUTF8StringRecord(); for (auto& entry : record.Entries()) { sp->Append(entry.mKey, entry.mValue); } @@ -104,35 +105,36 @@ void URLSearchParams::ParseInput(const nsACString& aInput) { uint32_t URLSearchParams::Size() const { return mParams->Length(); } -void URLSearchParams::Get(const nsAString& aName, nsString& aRetval) { +void URLSearchParams::Get(const nsACString& aName, nsACString& aRetval) { return mParams->Get(aName, aRetval); } -void URLSearchParams::GetAll(const nsAString& aName, - nsTArray<nsString>& aRetval) { +void URLSearchParams::GetAll(const nsACString& aName, + nsTArray<nsCString>& aRetval) { return mParams->GetAll(aName, aRetval); } -void URLSearchParams::Set(const nsAString& aName, const nsAString& aValue) { +void URLSearchParams::Set(const nsACString& aName, const nsACString& aValue) { mParams->Set(aName, aValue); NotifyObserver(); } -void URLSearchParams::Append(const nsAString& aName, const nsAString& aValue) { +void URLSearchParams::Append(const nsACString& aName, + const nsACString& aValue) { mParams->Append(aName, aValue); NotifyObserver(); } -bool URLSearchParams::Has(const nsAString& aName, - const Optional<nsAString>& aValue) { +bool URLSearchParams::Has(const nsACString& aName, + const Optional<nsACString>& aValue) { if (!aValue.WasPassed()) { return mParams->Has(aName); } return mParams->Has(aName, aValue.Value()); } -void URLSearchParams::Delete(const nsAString& aName, - const Optional<nsAString>& aValue) { +void URLSearchParams::Delete(const nsACString& aName, + const Optional<nsACString>& aValue) { if (!aValue.WasPassed()) { mParams->Delete(aName); NotifyObserver(); @@ -144,10 +146,17 @@ void URLSearchParams::Delete(const nsAString& aName, void URLSearchParams::DeleteAll() { mParams->DeleteAll(); } -void URLSearchParams::Serialize(nsAString& aValue) const { +void URLSearchParams::Serialize(nsACString& aValue) const { mParams->Serialize(aValue, true); } +// TODO(emilio): Allow stringifier attributes with CString return values. +void URLSearchParams::Stringify(nsAString& aValue) const { + nsAutoCString serialized; + mParams->Serialize(serialized, true); + CopyUTF8toUTF16(serialized, aValue); +} + void URLSearchParams::NotifyObserver() { if (mObserver) { mObserver->URLSearchParamsUpdated(this); @@ -158,11 +167,11 @@ uint32_t URLSearchParams::GetIterableLength() const { return mParams->Length(); } -const nsAString& URLSearchParams::GetKeyAtIndex(uint32_t aIndex) const { +const nsACString& URLSearchParams::GetKeyAtIndex(uint32_t aIndex) const { return mParams->GetKeyAtIndex(aIndex); } -const nsAString& URLSearchParams::GetValueAtIndex(uint32_t aIndex) const { +const nsACString& URLSearchParams::GetValueAtIndex(uint32_t aIndex) const { return mParams->GetValueAtIndex(aIndex); } @@ -182,11 +191,10 @@ nsresult URLSearchParams::GetSendInfo(nsIInputStream** aBody, "application/x-www-form-urlencoded;charset=UTF-8"); aCharset.AssignLiteral("UTF-8"); - nsAutoString serialized; + nsAutoCString serialized; Serialize(serialized); - NS_ConvertUTF16toUTF8 converted(serialized); - *aContentLength = converted.Length(); - return NS_NewCStringInputStream(aBody, std::move(converted)); + *aContentLength = serialized.Length(); + return NS_NewCStringInputStream(aBody, std::move(serialized)); } } // namespace mozilla::dom diff --git a/dom/url/URLSearchParams.h b/dom/url/URLSearchParams.h index 9ddff1e512..25bd293f4d 100644 --- a/dom/url/URLSearchParams.h +++ b/dom/url/URLSearchParams.h @@ -37,7 +37,7 @@ namespace dom { class GlobalObject; class URLSearchParams; -class USVStringSequenceSequenceOrUSVStringUSVStringRecordOrUSVString; +class UTF8StringSequenceSequenceOrUTF8StringUTF8StringRecordOrUTF8String; template <typename T> class Optional; @@ -66,42 +66,42 @@ class URLSearchParams final : public nsISupports, public nsWrapperCache { static already_AddRefed<URLSearchParams> Constructor( const GlobalObject& aGlobal, - const USVStringSequenceSequenceOrUSVStringUSVStringRecordOrUSVString& + const UTF8StringSequenceSequenceOrUTF8StringUTF8StringRecordOrUTF8String& aInit, ErrorResult& aRv); void ParseInput(const nsACString& aInput); - void Serialize(nsAString& aValue) const; + void Serialize(nsACString& aValue) const; uint32_t Size() const; - void Get(const nsAString& aName, nsString& aRetval); + void Get(const nsACString& aName, nsACString& aRetval); - void GetAll(const nsAString& aName, nsTArray<nsString>& aRetval); + void GetAll(const nsACString& aName, nsTArray<nsCString>& aRetval); - void Set(const nsAString& aName, const nsAString& aValue); + void Set(const nsACString& aName, const nsACString& aValue); - void Append(const nsAString& aName, const nsAString& aValue); + void Append(const nsACString& aName, const nsACString& aValue); - bool Has(const nsAString& aName, const Optional<nsAString>& aValue); + bool Has(const nsACString& aName, const Optional<nsACString>& aValue); - void Delete(const nsAString& aName, const Optional<nsAString>& aValue); + void Delete(const nsACString& aName, const Optional<nsACString>& aValue); uint32_t GetIterableLength() const; - const nsAString& GetKeyAtIndex(uint32_t aIndex) const; - const nsAString& GetValueAtIndex(uint32_t aIndex) const; + const nsACString& GetKeyAtIndex(uint32_t aIndex) const; + const nsACString& GetValueAtIndex(uint32_t aIndex) const; void Sort(ErrorResult& aRv); - void Stringify(nsString& aRetval) const { Serialize(aRetval); } + void Stringify(nsAString&) const; nsresult GetSendInfo(nsIInputStream** aBody, uint64_t* aContentLength, nsACString& aContentTypeWithCharset, nsACString& aCharset) const; private: - void AppendInternal(const nsAString& aName, const nsAString& aValue); + void AppendInternal(const nsACString& aName, const nsACString& aValue); void DeleteAll(); diff --git a/dom/url/URLWorker.cpp b/dom/url/URLWorker.cpp index e4ae3d6248..cb902b8cd3 100644 --- a/dom/url/URLWorker.cpp +++ b/dom/url/URLWorker.cpp @@ -18,11 +18,11 @@ namespace mozilla::dom { class CreateURLRunnable : public WorkerMainThreadRunnable { private: BlobImpl* mBlobImpl; - nsAString& mURL; + nsACString& mURL; public: CreateURLRunnable(WorkerPrivate* aWorkerPrivate, BlobImpl* aBlobImpl, - nsAString& aURL) + nsACString& aURL) : WorkerMainThreadRunnable(aWorkerPrivate, "URL :: CreateURL"_ns), mBlobImpl(aBlobImpl), mURL(aURL) { @@ -42,17 +42,13 @@ class CreateURLRunnable : public WorkerMainThreadRunnable { nsAutoString partKey; cookieJarSettings->GetPartitionKey(partKey); - nsAutoCString url; nsresult rv = BlobURLProtocolHandler::AddDataEntry( - mBlobImpl, principal, NS_ConvertUTF16toUTF8(partKey), url); - + mBlobImpl, principal, NS_ConvertUTF16toUTF8(partKey), mURL); if (NS_FAILED(rv)) { NS_WARNING("Failed to add data entry for the blob!"); - SetDOMStringToNull(mURL); + mURL.SetIsVoid(true); return false; } - - CopyUTF8toUTF16(url, mURL); return true; } }; @@ -60,26 +56,24 @@ class CreateURLRunnable : public WorkerMainThreadRunnable { // This class revokes an URL on the main thread. class RevokeURLRunnable : public WorkerMainThreadRunnable { private: - const nsString mURL; + const nsCString mURL; public: - RevokeURLRunnable(WorkerPrivate* aWorkerPrivate, const nsAString& aURL) + RevokeURLRunnable(WorkerPrivate* aWorkerPrivate, const nsACString& aURL) : WorkerMainThreadRunnable(aWorkerPrivate, "URL :: RevokeURL"_ns), mURL(aURL) {} bool MainThreadRun() override { AssertIsOnMainThread(); - NS_ConvertUTF16toUTF8 url(mURL); - nsCOMPtr<nsICookieJarSettings> cookieJarSettings = mWorkerPrivate->CookieJarSettings(); nsAutoString partKey; cookieJarSettings->GetPartitionKey(partKey); - BlobURLProtocolHandler::RemoveDataEntry(url, mWorkerPrivate->GetPrincipal(), - NS_ConvertUTF16toUTF8(partKey)); + BlobURLProtocolHandler::RemoveDataEntry( + mURL, mWorkerPrivate->GetPrincipal(), NS_ConvertUTF16toUTF8(partKey)); return true; } }; @@ -87,11 +81,11 @@ class RevokeURLRunnable : public WorkerMainThreadRunnable { // This class checks if an URL is valid on the main thread. class IsValidURLRunnable : public WorkerMainThreadRunnable { private: - const nsString mURL; + const nsCString mURL; bool mValid; public: - IsValidURLRunnable(WorkerPrivate* aWorkerPrivate, const nsAString& aURL) + IsValidURLRunnable(WorkerPrivate* aWorkerPrivate, const nsACString& aURL) : WorkerMainThreadRunnable(aWorkerPrivate, "URL :: IsValidURL"_ns), mURL(aURL), mValid(false) {} @@ -99,8 +93,7 @@ class IsValidURLRunnable : public WorkerMainThreadRunnable { bool MainThreadRun() override { AssertIsOnMainThread(); - NS_ConvertUTF16toUTF8 url(mURL); - mValid = BlobURLProtocolHandler::HasDataEntry(url); + mValid = BlobURLProtocolHandler::HasDataEntry(mURL); return true; } @@ -110,7 +103,8 @@ class IsValidURLRunnable : public WorkerMainThreadRunnable { /* static */ void URLWorker::CreateObjectURL(const GlobalObject& aGlobal, Blob& aBlob, - nsAString& aResult, mozilla::ErrorResult& aRv) { + nsACString& aResult, + mozilla::ErrorResult& aRv) { JSContext* cx = aGlobal.Context(); WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); @@ -128,12 +122,12 @@ void URLWorker::CreateObjectURL(const GlobalObject& aGlobal, Blob& aBlob, WorkerGlobalScope* scope = workerPrivate->GlobalScope(); MOZ_ASSERT(scope); - scope->RegisterHostObjectURI(NS_ConvertUTF16toUTF8(aResult)); + scope->RegisterHostObjectURI(aResult); } /* static */ void URLWorker::RevokeObjectURL(const GlobalObject& aGlobal, - const nsAString& aUrl, ErrorResult& aRv) { + const nsACString& aUrl, ErrorResult& aRv) { JSContext* cx = aGlobal.Context(); WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); @@ -148,12 +142,12 @@ void URLWorker::RevokeObjectURL(const GlobalObject& aGlobal, WorkerGlobalScope* scope = workerPrivate->GlobalScope(); MOZ_ASSERT(scope); - scope->UnregisterHostObjectURI(NS_ConvertUTF16toUTF8(aUrl)); + scope->UnregisterHostObjectURI(aUrl); } /* static */ bool URLWorker::IsValidObjectURL(const GlobalObject& aGlobal, - const nsAString& aUrl, ErrorResult& aRv) { + const nsACString& aUrl, ErrorResult& aRv) { JSContext* cx = aGlobal.Context(); WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); diff --git a/dom/url/URLWorker.h b/dom/url/URLWorker.h index be8ab512a1..8424e35cca 100644 --- a/dom/url/URLWorker.h +++ b/dom/url/URLWorker.h @@ -21,22 +21,20 @@ namespace dom { class URLWorker final { public: static already_AddRefed<URLWorker> Constructor( - const GlobalObject& aGlobal, const nsAString& aURL, - const Optional<nsAString>& aBase, ErrorResult& aRv); + const GlobalObject& aGlobal, const nsACString& aURL, + const Optional<nsACString>& aBase, ErrorResult& aRv); static already_AddRefed<URLWorker> Constructor(const GlobalObject& aGlobal, - const nsAString& aURL, - const nsAString& aBase, + const nsACString& aURL, + const nsACString& aBase, ErrorResult& aRv); static void CreateObjectURL(const GlobalObject& aGlobal, Blob& aBlob, - nsAString& aResult, mozilla::ErrorResult& aRv); - + nsACString& aResult, mozilla::ErrorResult& aRv); static void RevokeObjectURL(const GlobalObject& aGlobal, - const nsAString& aUrl, ErrorResult& aRv); - + const nsACString& aUrl, ErrorResult& aRv); static bool IsValidObjectURL(const GlobalObject& aGlobal, - const nsAString& aUrl, ErrorResult& aRv); + const nsACString& aUrl, ErrorResult& aRv); }; } // namespace dom diff --git a/dom/webauthn/AndroidWebAuthnService.cpp b/dom/webauthn/AndroidWebAuthnService.cpp index 162cc52033..a4fa230455 100644 --- a/dom/webauthn/AndroidWebAuthnService.cpp +++ b/dom/webauthn/AndroidWebAuthnService.cpp @@ -186,12 +186,6 @@ AndroidWebAuthnService::MakeCredential(uint64_t aTransactionId, GetCurrentSerialEventTarget(), __func__, [aPromise, credPropsResponse = std::move(credPropsResponse)]( RefPtr<WebAuthnRegisterResult>&& aValue) { - // We don't have a way for the user to consent to attestation - // on Android, so always anonymize the result. - nsresult rv = aValue->Anonymize(); - if (NS_FAILED(rv)) { - aPromise->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR); - } if (credPropsResponse.isSome()) { Unused << aValue->SetCredPropsRk(credPropsResponse.ref()); } @@ -357,8 +351,8 @@ AndroidWebAuthnService::PinCallback(uint64_t aTransactionId, } NS_IMETHODIMP -AndroidWebAuthnService::ResumeMakeCredential(uint64_t aTransactionId, - bool aForceNoneAttestation) { +AndroidWebAuthnService::SetHasAttestationConsent(uint64_t aTransactionId, + bool aHasConsent) { return NS_ERROR_NOT_IMPLEMENTED; } diff --git a/dom/webauthn/MacOSWebAuthnService.mm b/dom/webauthn/MacOSWebAuthnService.mm index cec9600e57..fc08ee1a48 100644 --- a/dom/webauthn/MacOSWebAuthnService.mm +++ b/dom/webauthn/MacOSWebAuthnService.mm @@ -338,6 +338,12 @@ nsTArray<uint8_t> NSDataToArray(NSData* data) { } #endif } else { + // The platform didn't tell us what transport was used, but we know it + // wasn't the internal transport. The transport response is not signed by + // the authenticator. It represents the "transports that the authenticator + // is believed to support, or an empty sequence if the information is + // unavailable". We believe macOS supports usb, so we return usb. + transports.AppendElement(u"usb"_ns); authenticatorAttachment.emplace(u"cross-platform"_ns); } mCallback->FinishMakeCredential(rawAttestationObject, credentialId, @@ -605,6 +611,9 @@ MacOSWebAuthnService::MakeCredential(uint64_t aTransactionId, userVerificationPreference = Nothing(); nsAutoString userVerification; Unused << aArgs->GetUserVerification(userVerification); + // This mapping needs to be reviewed if values are added to the + // UserVerificationRequirement enum. + static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3); if (userVerification.EqualsLiteral( MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED)) { userVerificationPreference.emplace( @@ -620,12 +629,51 @@ MacOSWebAuthnService::MakeCredential(uint64_t aTransactionId, ASAuthorizationPublicKeyCredentialUserVerificationPreferenceDiscouraged); } - // The API doesn't support attestation for platform passkeys and shows - // no consent UI for non-none attestation for cross-platform devices, - // so this must always be none. - ASAuthorizationPublicKeyCredentialAttestationKind - attestationPreference = - ASAuthorizationPublicKeyCredentialAttestationKindNone; + // The API doesn't support attestation for platform passkeys, so this is + // only used for security keys. + ASAuthorizationPublicKeyCredentialAttestationKind attestationPreference; + nsAutoString mozAttestationPreference; + Unused << aArgs->GetAttestationConveyancePreference( + mozAttestationPreference); + if (mozAttestationPreference.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT)) { + attestationPreference = + ASAuthorizationPublicKeyCredentialAttestationKindIndirect; + } else if (mozAttestationPreference.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT)) { + attestationPreference = + ASAuthorizationPublicKeyCredentialAttestationKindDirect; + } else if ( + mozAttestationPreference.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE)) { + attestationPreference = + ASAuthorizationPublicKeyCredentialAttestationKindEnterprise; + } else { + attestationPreference = + ASAuthorizationPublicKeyCredentialAttestationKindNone; + } + + ASAuthorizationPublicKeyCredentialResidentKeyPreference + residentKeyPreference; + nsAutoString mozResidentKey; + Unused << aArgs->GetResidentKey(mozResidentKey); + // This mapping needs to be reviewed if values are added to the + // ResidentKeyRequirement enum. + static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3); + if (mozResidentKey.EqualsLiteral( + MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED)) { + residentKeyPreference = + ASAuthorizationPublicKeyCredentialResidentKeyPreferenceRequired; + } else if (mozResidentKey.EqualsLiteral( + MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_PREFERRED)) { + residentKeyPreference = + ASAuthorizationPublicKeyCredentialResidentKeyPreferencePreferred; + } else { + MOZ_ASSERT(mozResidentKey.EqualsLiteral( + MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED)); + residentKeyPreference = + ASAuthorizationPublicKeyCredentialResidentKeyPreferenceDiscouraged; + } // Initialize the platform provider with the rpId. ASAuthorizationPlatformPublicKeyCredentialProvider* platformProvider = @@ -639,8 +687,10 @@ MacOSWebAuthnService::MakeCredential(uint64_t aTransactionId, name:userNameNS userID:userIdNS]; [platformProvider release]; + + // The API doesn't support attestation for platform passkeys platformRegistrationRequest.attestationPreference = - attestationPreference; + ASAuthorizationPublicKeyCredentialAttestationKindNone; if (userVerificationPreference.isSome()) { platformRegistrationRequest.userVerificationPreference = *userVerificationPreference; @@ -665,6 +715,8 @@ MacOSWebAuthnService::MakeCredential(uint64_t aTransactionId, attestationPreference; crossPlatformRegistrationRequest.credentialParameters = credentialParameters; + crossPlatformRegistrationRequest.residentKeyPreference = + residentKeyPreference; if (userVerificationPreference.isSome()) { crossPlatformRegistrationRequest.userVerificationPreference = *userVerificationPreference; @@ -914,6 +966,9 @@ void MacOSWebAuthnService::DoGetAssertion( userVerificationPreference = Nothing(); nsAutoString userVerification; Unused << aArgs->GetUserVerification(userVerification); + // This mapping needs to be reviewed if values are added to the + // UserVerificationRequirement enum. + static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3); if (userVerification.EqualsLiteral( MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED)) { userVerificationPreference.emplace( @@ -1115,8 +1170,8 @@ MacOSWebAuthnService::PinCallback(uint64_t aTransactionId, } NS_IMETHODIMP -MacOSWebAuthnService::ResumeMakeCredential(uint64_t aTransactionId, - bool aForceNoneAttestation) { +MacOSWebAuthnService::SetHasAttestationConsent(uint64_t aTransactionId, + bool aHasConsent) { return NS_ERROR_NOT_IMPLEMENTED; } diff --git a/dom/webauthn/WebAuthnArgs.cpp b/dom/webauthn/WebAuthnArgs.cpp index 7c78d39bef..c094a71963 100644 --- a/dom/webauthn/WebAuthnArgs.cpp +++ b/dom/webauthn/WebAuthnArgs.cpp @@ -155,7 +155,18 @@ WebAuthnRegisterArgs::GetTimeoutMS(uint32_t* aTimeoutMS) { NS_IMETHODIMP WebAuthnRegisterArgs::GetAttestationConveyancePreference( nsAString& aAttestationConveyancePreference) { - aAttestationConveyancePreference = mInfo.attestationConveyancePreference(); + const nsString& attPref = mInfo.attestationConveyancePreference(); + if (attPref.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT) || + attPref.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT) || + attPref.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE)) { + aAttestationConveyancePreference.Assign(attPref); + } else { + aAttestationConveyancePreference.AssignLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE); + } return NS_OK; } diff --git a/dom/webauthn/WebAuthnResult.cpp b/dom/webauthn/WebAuthnResult.cpp index 268dd62f20..6fd446ffa0 100644 --- a/dom/webauthn/WebAuthnResult.cpp +++ b/dom/webauthn/WebAuthnResult.cpp @@ -102,7 +102,27 @@ WebAuthnRegisterResult::GetAuthenticatorAttachment( return NS_ERROR_NOT_AVAILABLE; } -nsresult WebAuthnRegisterResult::Anonymize() { +NS_IMETHODIMP +WebAuthnRegisterResult::HasIdentifyingAttestation( + bool* aHasIdentifyingAttestation) { + // Assume the attestation statement is identifying in case the constructor or + // the getter below fail. + bool isIdentifying = true; + + nsCOMPtr<nsIWebAuthnAttObj> attObj; + nsresult rv = authrs_webauthn_att_obj_constructor(mAttestationObject, + /* anonymize */ false, + getter_AddRefs(attObj)); + if (NS_SUCCEEDED(rv)) { + Unused << attObj->IsIdentifying(&isIdentifying); + } + + *aHasIdentifyingAttestation = isIdentifying; + return NS_OK; +} + +NS_IMETHODIMP +WebAuthnRegisterResult::Anonymize() { // The anonymize flag in the nsIWebAuthnAttObj constructor causes the // attestation statement to be removed during deserialization. It also // causes the AAGUID to be zeroed out. If we can't deserialize the diff --git a/dom/webauthn/WebAuthnResult.h b/dom/webauthn/WebAuthnResult.h index f7653fd4b0..13cea0e187 100644 --- a/dom/webauthn/WebAuthnResult.h +++ b/dom/webauthn/WebAuthnResult.h @@ -63,8 +63,8 @@ class WebAuthnRegisterResult final : public nsIWebAuthnRegisterResult { mTransports.AppendElement( jni::String::LocalRef(transports->GetElement(i))->ToString()); } - // authenticator attachment is not available on Android - mAuthenticatorAttachment = Nothing(); + mAuthenticatorAttachment = + Some(aResponse->AuthenticatorAttachment()->ToString()); } #endif @@ -134,8 +134,6 @@ class WebAuthnRegisterResult final : public nsIWebAuthnRegisterResult { } #endif - nsresult Anonymize(); - private: ~WebAuthnRegisterResult() = default; @@ -191,8 +189,8 @@ class WebAuthnSignResult final : public nsIWebAuthnSignResult { reinterpret_cast<uint8_t*>( aResponse->UserHandle()->GetElements().Elements()), aResponse->UserHandle()->Length()); - // authenticator attachment is not available on Android - mAuthenticatorAttachment = Nothing(); + mAuthenticatorAttachment = + Some(aResponse->AuthenticatorAttachment()->ToString()); } #endif diff --git a/dom/webauthn/WebAuthnService.cpp b/dom/webauthn/WebAuthnService.cpp index 3e1557edbc..0c214ccd90 100644 --- a/dom/webauthn/WebAuthnService.cpp +++ b/dom/webauthn/WebAuthnService.cpp @@ -5,7 +5,9 @@ #include "mozilla/Services.h" #include "mozilla/StaticPrefs_security.h" #include "nsIObserverService.h" +#include "nsTextFormatter.h" #include "nsThreadUtils.h" +#include "WebAuthnEnumStrings.h" #include "WebAuthnService.h" #include "WebAuthnTransportIdentifiers.h" @@ -18,32 +20,139 @@ already_AddRefed<nsIWebAuthnService> NewWebAuthnService() { NS_IMPL_ISUPPORTS(WebAuthnService, nsIWebAuthnService) +void WebAuthnService::ShowAttestationConsentPrompt( + const nsString& aOrigin, uint64_t aTransactionId, + uint64_t aBrowsingContextId) { + RefPtr<WebAuthnService> self = this; +#ifdef MOZ_WIDGET_ANDROID + // We don't have a way to prompt the user for consent on Android, so just + // assume consent not granted. + nsCOMPtr<nsIRunnable> runnable( + NS_NewRunnableFunction(__func__, [self, aTransactionId]() { + self->SetHasAttestationConsent( + aTransactionId, + StaticPrefs:: + security_webauth_webauthn_testing_allow_direct_attestation()); + })); +#else + nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction( + __func__, [self, aOrigin, aTransactionId, aBrowsingContextId]() { + if (StaticPrefs:: + security_webauth_webauthn_testing_allow_direct_attestation()) { + self->SetHasAttestationConsent(aTransactionId, true); + return; + } + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + if (!os) { + return; + } + const nsLiteralString jsonFmt = + u"{\"prompt\": {\"type\":\"attestation-consent\"},"_ns + u"\"origin\": \"%S\","_ns + u"\"tid\": %llu, \"browsingContextId\": %llu}"_ns; + nsString json; + nsTextFormatter::ssprintf(json, jsonFmt.get(), aOrigin.get(), + aTransactionId, aBrowsingContextId); + MOZ_ALWAYS_SUCCEEDS( + os->NotifyObservers(nullptr, "webauthn-prompt", json.get())); + })); +#endif + NS_DispatchToMainThread(runnable.forget()); +} + NS_IMETHODIMP WebAuthnService::MakeCredential(uint64_t aTransactionId, - uint64_t browsingContextId, + uint64_t aBrowsingContextId, nsIWebAuthnRegisterArgs* aArgs, nsIWebAuthnRegisterPromise* aPromise) { + MOZ_ASSERT(aArgs); + MOZ_ASSERT(aPromise); + auto guard = mTransactionState.Lock(); - if (guard->isSome()) { - guard->ref().service->Reset(); - *guard = Nothing(); + ResetLocked(guard); + *guard = Some(TransactionState{.service = DefaultService(), + .transactionId = aTransactionId, + .parentRegisterPromise = Some(aPromise)}); + + // We may need to show an attestation consent prompt before we return a + // credential to WebAuthnTransactionParent, so we insert a new promise that + // chains to `aPromise` here. + + nsString attestation; + Unused << aArgs->GetAttestationConveyancePreference(attestation); + bool attestationRequested = !attestation.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE); + + nsString origin; + Unused << aArgs->GetOrigin(origin); + + RefPtr<WebAuthnRegisterPromiseHolder> promiseHolder = + new WebAuthnRegisterPromiseHolder(GetCurrentSerialEventTarget()); + + RefPtr<WebAuthnService> self = this; + RefPtr<WebAuthnRegisterPromise> promise = promiseHolder->Ensure(); + promise + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self, origin, aTransactionId, aBrowsingContextId, + attestationRequested]( + const WebAuthnRegisterPromise::ResolveOrRejectValue& aValue) { + auto guard = self->mTransactionState.Lock(); + if (guard->isNothing()) { + return; + } + MOZ_ASSERT(guard->ref().parentRegisterPromise.isSome()); + MOZ_ASSERT(guard->ref().registerResult.isNothing()); + MOZ_ASSERT(guard->ref().childRegisterRequest.Exists()); + + guard->ref().childRegisterRequest.Complete(); + + if (aValue.IsReject()) { + guard->ref().parentRegisterPromise.ref()->Reject( + aValue.RejectValue()); + guard->reset(); + return; + } + + nsIWebAuthnRegisterResult* result = aValue.ResolveValue(); + // If the RP requested attestation, we need to show a consent prompt + // before returning any identifying information. The platform may + // have already done this for us, so we need to inspect the + // attestation object at this point. + bool resultIsIdentifying = true; + Unused << result->HasIdentifyingAttestation(&resultIsIdentifying); + if (attestationRequested && resultIsIdentifying) { + guard->ref().registerResult = Some(result); + self->ShowAttestationConsentPrompt(origin, aTransactionId, + aBrowsingContextId); + return; + } + result->Anonymize(); + guard->ref().parentRegisterPromise.ref()->Resolve(result); + guard->reset(); + }) + ->Track(guard->ref().childRegisterRequest); + + nsresult rv = guard->ref().service->MakeCredential( + aTransactionId, aBrowsingContextId, aArgs, promiseHolder); + if (NS_FAILED(rv)) { + promiseHolder->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR); } - *guard = Some(TransactionState{DefaultService()}); - return guard->ref().service->MakeCredential(aTransactionId, browsingContextId, - aArgs, aPromise); + return NS_OK; } NS_IMETHODIMP WebAuthnService::GetAssertion(uint64_t aTransactionId, - uint64_t browsingContextId, + uint64_t aBrowsingContextId, nsIWebAuthnSignArgs* aArgs, nsIWebAuthnSignPromise* aPromise) { + MOZ_ASSERT(aArgs); + MOZ_ASSERT(aPromise); + auto guard = mTransactionState.Lock(); - if (guard->isSome()) { - guard->ref().service->Reset(); - *guard = Nothing(); - } - *guard = Some(TransactionState{DefaultService()}); + ResetLocked(guard); + *guard = Some(TransactionState{.service = DefaultService(), + .transactionId = aTransactionId}); nsresult rv; #if defined(XP_MACOSX) @@ -71,7 +180,7 @@ WebAuthnService::GetAssertion(uint64_t aTransactionId, } #endif - rv = guard->ref().service->GetAssertion(aTransactionId, browsingContextId, + rv = guard->ref().service->GetAssertion(aTransactionId, aBrowsingContextId, aArgs, aPromise); if (NS_FAILED(rv)) { return rv; @@ -125,13 +234,22 @@ WebAuthnService::ResumeConditionalGet(uint64_t aTransactionId) { return SelectedService()->ResumeConditionalGet(aTransactionId); } +void WebAuthnService::ResetLocked( + const TransactionStateMutex::AutoLock& aGuard) { + if (aGuard->isSome()) { + aGuard->ref().childRegisterRequest.DisconnectIfExists(); + if (aGuard->ref().parentRegisterPromise.isSome()) { + aGuard->ref().parentRegisterPromise.ref()->Reject(NS_ERROR_DOM_ABORT_ERR); + } + aGuard->ref().service->Reset(); + } + aGuard->reset(); +} + NS_IMETHODIMP WebAuthnService::Reset() { auto guard = mTransactionState.Lock(); - if (guard->isSome()) { - guard->ref().service->Reset(); - } - *guard = Nothing(); + ResetLocked(guard); return NS_OK; } @@ -146,10 +264,27 @@ WebAuthnService::PinCallback(uint64_t aTransactionId, const nsACString& aPin) { } NS_IMETHODIMP -WebAuthnService::ResumeMakeCredential(uint64_t aTransactionId, - bool aForceNoneAttestation) { - return SelectedService()->ResumeMakeCredential(aTransactionId, - aForceNoneAttestation); +WebAuthnService::SetHasAttestationConsent(uint64_t aTransactionId, + bool aHasConsent) { + auto guard = this->mTransactionState.Lock(); + if (guard->isNothing() || guard->ref().transactionId != aTransactionId) { + // This could happen if the transaction was reset just when the prompt was + // receiving user input. + return NS_OK; + } + + MOZ_ASSERT(guard->ref().parentRegisterPromise.isSome()); + MOZ_ASSERT(guard->ref().registerResult.isSome()); + MOZ_ASSERT(!guard->ref().childRegisterRequest.Exists()); + + if (!aHasConsent) { + guard->ref().registerResult.ref()->Anonymize(); + } + guard->ref().parentRegisterPromise.ref()->Resolve( + guard->ref().registerResult.ref()); + + guard->reset(); + return NS_OK; } NS_IMETHODIMP diff --git a/dom/webauthn/WebAuthnService.h b/dom/webauthn/WebAuthnService.h index 254b75d251..a3f4dab797 100644 --- a/dom/webauthn/WebAuthnService.h +++ b/dom/webauthn/WebAuthnService.h @@ -7,6 +7,7 @@ #include "nsIWebAuthnService.h" #include "AuthrsBridge_ffi.h" +#include "mozilla/dom/WebAuthnPromiseHolder.h" #ifdef MOZ_WIDGET_ANDROID # include "AndroidWebAuthnService.h" @@ -55,6 +56,21 @@ class WebAuthnService final : public nsIWebAuthnService { private: ~WebAuthnService() = default; + struct TransactionState { + nsCOMPtr<nsIWebAuthnService> service; + uint64_t transactionId; + Maybe<nsCOMPtr<nsIWebAuthnRegisterPromise>> parentRegisterPromise; + Maybe<nsCOMPtr<nsIWebAuthnRegisterResult>> registerResult; + MozPromiseRequestHolder<WebAuthnRegisterPromise> childRegisterRequest; + }; + using TransactionStateMutex = DataMutex<Maybe<TransactionState>>; + TransactionStateMutex mTransactionState; + + void ShowAttestationConsentPrompt(const nsString& aOrigin, + uint64_t aTransactionId, + uint64_t aBrowsingContextId); + void ResetLocked(const TransactionStateMutex::AutoLock& aGuard); + nsIWebAuthnService* DefaultService() { if (StaticPrefs::security_webauth_webauthn_enable_softtoken()) { return mAuthrsService; @@ -72,12 +88,6 @@ class WebAuthnService final : public nsIWebAuthnService { return DefaultService(); } - struct TransactionState { - nsCOMPtr<nsIWebAuthnService> service; - }; - using TransactionStateMutex = DataMutex<Maybe<TransactionState>>; - TransactionStateMutex mTransactionState; - nsCOMPtr<nsIWebAuthnService> mAuthrsService; nsCOMPtr<nsIWebAuthnService> mPlatformService; }; diff --git a/dom/webauthn/WinWebAuthnService.cpp b/dom/webauthn/WinWebAuthnService.cpp index 8667cf5615..a9f73a4cf7 100644 --- a/dom/webauthn/WinWebAuthnService.cpp +++ b/dom/webauthn/WinWebAuthnService.cpp @@ -411,14 +411,12 @@ WinWebAuthnService::MakeCredential(uint64_t aTransactionId, // AttestationConveyance nsString attestation; Unused << aArgs->GetAttestationConveyancePreference(attestation); - bool anonymize = false; // This mapping needs to be reviewed if values are added to the // AttestationConveyancePreference enum. static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3); if (attestation.EqualsLiteral( MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE)) { winAttestation = WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE; - anonymize = true; } else if ( attestation.EqualsLiteral( MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT)) { @@ -579,13 +577,6 @@ WinWebAuthnService::MakeCredential(uint64_t aTransactionId, } gWinWebauthnFreeCredentialAttestation(pWebAuthNCredentialAttestation); - if (anonymize) { - nsresult rv = result->Anonymize(); - if (NS_FAILED(rv)) { - aPromise->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR); - return; - } - } aPromise->Resolve(result); } else { PCWSTR errorName = gWinWebauthnGetErrorName(hr); @@ -977,8 +968,8 @@ WinWebAuthnService::PinCallback(uint64_t aTransactionId, } NS_IMETHODIMP -WinWebAuthnService::ResumeMakeCredential(uint64_t aTransactionId, - bool aForceNoneAttestation) { +WinWebAuthnService::SetHasAttestationConsent(uint64_t aTransactionId, + bool aHasConsent) { return NS_ERROR_NOT_IMPLEMENTED; } diff --git a/dom/webauthn/authrs_bridge/src/about_webauthn_controller.rs b/dom/webauthn/authrs_bridge/src/about_webauthn_controller.rs index 8d77a62df4..5c160b9333 100644 --- a/dom/webauthn/authrs_bridge/src/about_webauthn_controller.rs +++ b/dom/webauthn/authrs_bridge/src/about_webauthn_controller.rs @@ -4,9 +4,9 @@ use super::*; use authenticator::{ - ctap2::commands::{PinUvAuthResult, StatusCode}, + ctap2::commands::StatusCode, errors::{CommandError, HIDError}, - BioEnrollmentCmd, CredManagementCmd, InteractiveRequest, InteractiveUpdate, PinError, + BioEnrollmentCmd, CredManagementCmd, InteractiveUpdate, PinError, }; use serde::{Deserialize, Serialize}; diff --git a/dom/webauthn/authrs_bridge/src/lib.rs b/dom/webauthn/authrs_bridge/src/lib.rs index 353e1e89a4..b6d54a30b9 100644 --- a/dom/webauthn/authrs_bridge/src/lib.rs +++ b/dom/webauthn/authrs_bridge/src/lib.rs @@ -10,7 +10,7 @@ extern crate xpcom; use authenticator::{ authenticatorservice::{RegisterArgs, SignArgs}, - ctap2::attestation::AttestationObject, + ctap2::attestation::{AAGuid, AttestationObject, AttestationStatement}, ctap2::commands::{get_info::AuthenticatorVersion, PinUvAuthResult}, ctap2::server::{ AuthenticationExtensionsClientInputs, AuthenticatorAttachment, @@ -33,8 +33,8 @@ use nserror::{ }; use nsstring::{nsACString, nsAString, nsCString, nsString}; use serde::Serialize; -use serde_cbor; use serde_json::json; +use std::cell::RefCell; use std::fmt::Write; use std::sync::mpsc::{channel, Receiver, RecvError, Sender}; use std::sync::{Arc, Mutex, MutexGuard}; @@ -87,7 +87,6 @@ enum BrowserPromptType<'a> { }, PinIsTooLong, PinIsTooShort, - RegisterDirect, UvInvalid { retries: Option<u8>, }, @@ -167,7 +166,8 @@ fn cancel_prompts(tid: u64) -> Result<(), nsresult> { #[xpcom(implement(nsIWebAuthnRegisterResult), atomic)] pub struct WebAuthnRegisterResult { - result: RegisterResult, + // result is only borrowed mutably in `Anonymize`. + result: RefCell<RegisterResult>, } impl WebAuthnRegisterResult { @@ -179,13 +179,13 @@ impl WebAuthnRegisterResult { xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec<u8>); fn get_attestation_object(&self) -> Result<ThinVec<u8>, nsresult> { let mut out = ThinVec::new(); - serde_cbor::to_writer(&mut out, &self.result.att_obj).or(Err(NS_ERROR_FAILURE))?; + serde_cbor::to_writer(&mut out, &self.result.borrow().att_obj).or(Err(NS_ERROR_FAILURE))?; Ok(out) } xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>); fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> { - let Some(credential_data) = &self.result.att_obj.auth_data.credential_data else { + let Some(credential_data) = &self.result.borrow().att_obj.auth_data.credential_data else { return Err(NS_ERROR_FAILURE); }; Ok(credential_data.credential_id.as_slice().into()) @@ -198,7 +198,7 @@ impl WebAuthnRegisterResult { // In tests, the result is not very important, but we can at least return "internal" if // we're simulating platform attachment. if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") - && self.result.attachment == AuthenticatorAttachment::Platform + && self.result.borrow().attachment == AuthenticatorAttachment::Platform { Ok(thin_vec![nsString::from("internal")]) } else { @@ -208,7 +208,7 @@ impl WebAuthnRegisterResult { xpcom_method!(get_hmac_create_secret => GetHmacCreateSecret() -> bool); fn get_hmac_create_secret(&self) -> Result<bool, nsresult> { - let Some(hmac_create_secret) = self.result.extensions.hmac_create_secret else { + let Some(hmac_create_secret) = self.result.borrow().extensions.hmac_create_secret else { return Err(NS_ERROR_NOT_AVAILABLE); }; Ok(hmac_create_secret) @@ -216,7 +216,7 @@ impl WebAuthnRegisterResult { xpcom_method!(get_cred_props_rk => GetCredPropsRk() -> bool); fn get_cred_props_rk(&self) -> Result<bool, nsresult> { - let Some(cred_props) = &self.result.extensions.cred_props else { + let Some(cred_props) = &self.result.borrow().extensions.cred_props else { return Err(NS_ERROR_NOT_AVAILABLE); }; Ok(cred_props.rk) @@ -229,12 +229,29 @@ impl WebAuthnRegisterResult { xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString); fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> { - match self.result.attachment { + match self.result.borrow().attachment { AuthenticatorAttachment::CrossPlatform => Ok(nsString::from("cross-platform")), AuthenticatorAttachment::Platform => Ok(nsString::from("platform")), AuthenticatorAttachment::Unknown => Err(NS_ERROR_NOT_AVAILABLE), } } + + xpcom_method!(has_identifying_attestation => HasIdentifyingAttestation() -> bool); + fn has_identifying_attestation(&self) -> Result<bool, nsresult> { + if self.result.borrow().att_obj.att_stmt != AttestationStatement::None { + return Ok(true); + } + if let Some(data) = &self.result.borrow().att_obj.auth_data.credential_data { + return Ok(data.aaguid != AAGuid::default()); + } + Ok(false) + } + + xpcom_method!(anonymize => Anonymize()); + fn anonymize(&self) -> Result<nsresult, nsresult> { + self.result.borrow_mut().att_obj.anonymize(); + Ok(NS_OK) + } } #[xpcom(implement(nsIWebAuthnAttObj), atomic)] @@ -276,6 +293,17 @@ impl WebAuthnAttObj { // safe to cast to i32 by inspection of defined values Ok(credential_data.credential_public_key.alg as i32) } + + xpcom_method!(is_identifying => IsIdentifying() -> bool); + fn is_identifying(&self) -> Result<bool, nsresult> { + if self.att_obj.att_stmt != AttestationStatement::None { + return Ok(true); + } + if let Some(data) = &self.att_obj.auth_data.credential_data { + return Ok(data.aaguid != AAGuid::default()); + } + Ok(false) + } } #[xpcom(implement(nsIWebAuthnSignResult), atomic)] @@ -491,10 +519,11 @@ impl RegisterPromise { fn resolve_or_reject(&self, result: Result<RegisterResult, nsresult>) -> Result<(), nsresult> { match result { Ok(result) => { - let wrapped_result = - WebAuthnRegisterResult::allocate(InitWebAuthnRegisterResult { result }) - .query_interface::<nsIWebAuthnRegisterResult>() - .ok_or(NS_ERROR_FAILURE)?; + let wrapped_result = WebAuthnRegisterResult::allocate(InitWebAuthnRegisterResult { + result: RefCell::new(result), + }) + .query_interface::<nsIWebAuthnRegisterResult>() + .ok_or(NS_ERROR_FAILURE)?; unsafe { self.0.Resolve(wrapped_result.coerce()) }; } Err(result) => { @@ -544,7 +573,6 @@ impl TransactionPromise { } enum TransactionArgs { - Register(/* timeout */ u64, RegisterArgs), Sign(/* timeout */ u64, SignArgs), } @@ -714,13 +742,6 @@ impl AuthrsService { } } - let mut attestation_conveyance_preference = nsString::new(); - unsafe { args.GetAttestationConveyancePreference(&mut *attestation_conveyance_preference) } - .to_result()?; - let none_attestation = !(attestation_conveyance_preference.eq("indirect") - || attestation_conveyance_preference.eq("direct") - || attestation_conveyance_preference.eq("enterprise")); - let mut cred_props = false; unsafe { args.GetCredProps(&mut cred_props) }.to_result()?; @@ -761,51 +782,19 @@ impl AuthrsService { use_ctap1_fallback: !static_prefs::pref!("security.webauthn.ctap2"), }; - *self.transaction.lock().unwrap() = Some(TransactionState { + let mut guard = self.transaction.lock().unwrap(); + *guard = Some(TransactionState { tid, browsing_context_id, - pending_args: Some(TransactionArgs::Register(timeout_ms as u64, info)), + pending_args: None, promise: TransactionPromise::Register(promise), pin_receiver: None, selection_receiver: None, interactive_receiver: None, puat_cache: None, }); - - if none_attestation - || static_prefs::pref!("security.webauth.webauthn_testing_allow_direct_attestation") - { - self.resume_make_credential(tid, none_attestation) - } else { - send_prompt( - BrowserPromptType::RegisterDirect, - tid, - Some(&origin), - Some(browsing_context_id), - ) - } - } - - xpcom_method!(resume_make_credential => ResumeMakeCredential(aTid: u64, aForceNoneAttestation: bool)); - fn resume_make_credential( - &self, - tid: u64, - force_none_attestation: bool, - ) -> Result<(), nsresult> { - let mut guard = self.transaction.lock().unwrap(); - let Some(state) = guard.as_mut() else { - return Err(NS_ERROR_FAILURE); - }; - if state.tid != tid { - return Err(NS_ERROR_FAILURE); - }; - let browsing_context_id = state.browsing_context_id; - let Some(TransactionArgs::Register(timeout_ms, info)) = state.pending_args.take() else { - return Err(NS_ERROR_FAILURE); - }; - // We have to drop the guard here, as there _may_ still be another operation - // ongoing and `register()` below will try to cancel it. This will call the state - // callback of that operation, which in turn may try to access `transaction`, deadlocking. + // drop the guard here to ensure we don't deadlock if the call to `register()` below + // hairpins the state callback. drop(guard); let (status_tx, status_rx) = channel::<StatusUpdate>(); @@ -826,7 +815,7 @@ impl AuthrsService { let callback_transaction = self.transaction.clone(); let callback_origin = info.origin.clone(); let state_callback = StateCallback::<Result<RegisterResult, AuthenticatorError>>::new( - Box::new(move |mut result| { + Box::new(move |result| { let mut guard = callback_transaction.lock().unwrap(); let Some(state) = guard.as_mut() else { return; @@ -837,13 +826,6 @@ impl AuthrsService { let TransactionPromise::Register(ref promise) = state.promise else { return; }; - if let Ok(inner) = result.as_mut() { - // Tokens always provide attestation, but the user may have asked we not - // include the attestation statement in the response. - if force_none_attestation { - inner.att_obj.anonymize(); - } - } if let Err(AuthenticatorError::CredentialExcluded) = result { let _ = send_prompt( BrowserPromptType::AlreadyRegistered, @@ -875,14 +857,14 @@ impl AuthrsService { Some(browsing_context_id), )?; self.usb_token_manager.lock().unwrap().register( - timeout_ms, + timeout_ms.into(), info, status_tx, state_callback, ); } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { self.test_token_manager - .register(timeout_ms, info, status_tx, state_callback); + .register(timeout_ms.into(), info, status_tx, state_callback); } else { return Err(NS_ERROR_FAILURE); } @@ -890,6 +872,11 @@ impl AuthrsService { Ok(()) } + xpcom_method!(set_has_attestation_consent => SetHasAttestationConsent(aTid: u64, aHasConsent: bool)); + fn set_has_attestation_consent(&self, _tid: u64, _has_consent: bool) -> Result<(), nsresult> { + Err(NS_ERROR_NOT_IMPLEMENTED) + } + xpcom_method!(get_assertion => GetAssertion(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsIWebAuthnSignArgs, aPromise: *const nsIWebAuthnSignPromise)); fn get_assertion( &self, diff --git a/dom/webauthn/nsIWebAuthnArgs.idl b/dom/webauthn/nsIWebAuthnArgs.idl index 72999092fa..06a6c5ec85 100644 --- a/dom/webauthn/nsIWebAuthnArgs.idl +++ b/dom/webauthn/nsIWebAuthnArgs.idl @@ -40,9 +40,9 @@ interface nsIWebAuthnRegisterArgs : nsISupports { // WebAuthn AuthenticationExtensionsClientInputs. That's not feasible here. // So we define a getter for each supported extension input and use the // return code to signal presence. - [must_use] readonly attribute bool credProps; - [must_use] readonly attribute bool hmacCreateSecret; - [must_use] readonly attribute bool minPinLength; + [must_use] readonly attribute boolean credProps; + [must_use] readonly attribute boolean hmacCreateSecret; + [must_use] readonly attribute boolean minPinLength; // Options. readonly attribute AString residentKey; @@ -83,7 +83,7 @@ interface nsIWebAuthnSignArgs : nsISupports { // WebAuthn AuthenticationExtensionsClientInputs. That's not feasible here. // So we define a getter for each supported extension input and use the // return code to signal presence. - [must_use] readonly attribute bool hmacCreateSecret; + [must_use] readonly attribute boolean hmacCreateSecret; [must_use] readonly attribute AString appId; // Options @@ -94,5 +94,5 @@ interface nsIWebAuthnSignArgs : nsISupports { // cancel transactions. readonly attribute unsigned long timeoutMS; - readonly attribute bool conditionallyMediated; + readonly attribute boolean conditionallyMediated; }; diff --git a/dom/webauthn/nsIWebAuthnAttObj.idl b/dom/webauthn/nsIWebAuthnAttObj.idl index 32d4f0aba3..101d4d1433 100644 --- a/dom/webauthn/nsIWebAuthnAttObj.idl +++ b/dom/webauthn/nsIWebAuthnAttObj.idl @@ -17,4 +17,6 @@ interface nsIWebAuthnAttObj : nsISupports { readonly attribute Array<octet> publicKey; readonly attribute COSEAlgorithmIdentifier publicKeyAlgorithm; + + boolean isIdentifying(); }; diff --git a/dom/webauthn/nsIWebAuthnResult.idl b/dom/webauthn/nsIWebAuthnResult.idl index aaf34cc5f9..32ce698277 100644 --- a/dom/webauthn/nsIWebAuthnResult.idl +++ b/dom/webauthn/nsIWebAuthnResult.idl @@ -23,11 +23,14 @@ interface nsIWebAuthnRegisterResult : nsISupports { readonly attribute Array<AString> transports; - readonly attribute bool hmacCreateSecret; + readonly attribute boolean hmacCreateSecret; - [must_use] attribute bool credPropsRk; + [must_use] attribute boolean credPropsRk; [must_use] readonly attribute AString authenticatorAttachment; + + boolean hasIdentifyingAttestation(); + void anonymize(); }; // The nsIWebAuthnSignResult interface is used to construct IPDL-defined @@ -56,7 +59,7 @@ interface nsIWebAuthnSignResult : nsISupports { [must_use] readonly attribute ACString userName; // appId field of AuthenticationExtensionsClientOutputs (Optional) - [must_use] attribute bool usedAppId; + [must_use] attribute boolean usedAppId; [must_use] readonly attribute AString authenticatorAttachment; }; diff --git a/dom/webauthn/nsIWebAuthnService.idl b/dom/webauthn/nsIWebAuthnService.idl index 6525508057..f2993a9e47 100644 --- a/dom/webauthn/nsIWebAuthnService.idl +++ b/dom/webauthn/nsIWebAuthnService.idl @@ -11,7 +11,7 @@ interface nsICredentialParameters : nsISupports { readonly attribute ACString credentialId; - readonly attribute bool isResidentCredential; + readonly attribute boolean isResidentCredential; readonly attribute ACString rpId; readonly attribute ACString privateKey; readonly attribute ACString userHandle; @@ -37,14 +37,16 @@ interface nsIWebAuthnAutoFillEntry: nsISupports interface nsIWebAuthnService : nsISupports { // IsUserVerifyingPlatformAuthenticatorAvailable - readonly attribute bool isUVPAA; + readonly attribute boolean isUVPAA; + [noscript] void makeCredential( in uint64_t aTransactionId, in uint64_t browsingContextId, in nsIWebAuthnRegisterArgs args, in nsIWebAuthnRegisterPromise promise); + [noscript] void getAssertion( in uint64_t aTransactionId, in uint64_t browsingContextId, @@ -83,7 +85,7 @@ interface nsIWebAuthnService : nsISupports void resumeConditionalGet(in uint64_t aTransactionId); void pinCallback(in uint64_t aTransactionId, in ACString aPin); - void resumeMakeCredential(in uint64_t aTransactionId, in bool aForceNoneAttestation); + void setHasAttestationConsent(in uint64_t aTransactionId, in boolean aHasConsent); void selectionCallback(in uint64_t aTransactionId, in uint64_t aIndex); // Adds a virtual (software) authenticator for use in tests (particularly @@ -92,10 +94,10 @@ interface nsIWebAuthnService : nsISupports uint64_t addVirtualAuthenticator( in ACString protocol, in ACString transport, - in bool hasResidentKey, - in bool hasUserVerification, - in bool isUserConsenting, - in bool isUserVerified); + in boolean hasResidentKey, + in boolean hasUserVerification, + in boolean isUserConsenting, + in boolean isUserVerified); // Removes a previously-added virtual authenticator, as identified by its // id. See @@ -107,7 +109,7 @@ interface nsIWebAuthnService : nsISupports void addCredential( in uint64_t authenticatorId, in ACString credentialId, - in bool isResidentCredential, + in boolean isResidentCredential, in ACString rpId, in ACString privateKey, in ACString userHandle, @@ -127,7 +129,7 @@ interface nsIWebAuthnService : nsISupports // Sets the "isUserVerified" bit on a virtual authenticator. See // https://w3c.github.io/webauthn/#sctn-automation-set-user-verified - void setUserVerified(in uint64_t authenticatorId, in bool isUserVerified); + void setUserVerified(in uint64_t authenticatorId, in boolean isUserVerified); // about:webauthn-specific functions void listen(); diff --git a/dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js b/dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js index fff1ec5dab..7bbce1ae58 100644 --- a/dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js +++ b/dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js @@ -46,15 +46,13 @@ add_task(async function test_webauthn_modal_request_cancels_conditional_get() { ok(active, "conditional request should still be active"); let promptPromise = promiseNotification("webauthn-prompt-register-direct"); - let modalPromise = promiseWebAuthnMakeCredential(tab, "direct") - .then(arrivingHereIsBad) - .catch(gExpectNotAllowedError); + let modalPromise = promiseWebAuthnMakeCredential(tab, "direct"); await condPromise; ok(!active, "conditional request should not be active"); - // Cancel the modal request with the button. + // Proceed through the consent prompt await promptPromise; PopupNotifications.panel.firstElementChild.secondaryButton.click(); await modalPromise; diff --git a/dom/webauthn/tests/browser/browser_webauthn_prompts.js b/dom/webauthn/tests/browser/browser_webauthn_prompts.js index 05c77271d5..68f1bf81f4 100644 --- a/dom/webauthn/tests/browser/browser_webauthn_prompts.js +++ b/dom/webauthn/tests/browser/browser_webauthn_prompts.js @@ -43,34 +43,26 @@ add_task(async function test_setup_usbtoken() { }); add_task(test_register); add_task(test_register_escape); -add_task(test_register_direct_cancel); -add_task(test_register_direct_presence); add_task(test_sign); add_task(test_sign_escape); add_task(test_tab_switching); add_task(test_window_switching); -add_task(async function test_setup_fullscreen() { +add_task(async function test_setup_softtoken() { + gAuthenticatorId = add_virtual_authenticator(); return SpecialPowers.pushPrefEnv({ set: [ ["browser.fullscreen.autohide", true], ["full-screen-api.enabled", true], ["full-screen-api.allow-trusted-requests-only", false], - ], - }); -}); -add_task(test_fullscreen_show_nav_toolbar); -add_task(test_no_fullscreen_dom); -add_task(async function test_setup_softtoken() { - gAuthenticatorId = add_virtual_authenticator(); - return SpecialPowers.pushPrefEnv({ - set: [ ["security.webauth.webauthn_enable_softtoken", true], ["security.webauth.webauthn_enable_usbtoken", false], ], }); }); -add_task(test_register_direct_proceed); -add_task(test_register_direct_proceed_anon); +add_task(test_fullscreen_show_nav_toolbar); +add_task(test_no_fullscreen_dom); +add_task(test_register_direct_with_consent); +add_task(test_register_direct_without_consent); add_task(test_select_sign_result); function promiseNavToolboxStatus(aExpectedStatus) { @@ -215,53 +207,6 @@ async function test_sign_escape() { await BrowserTestUtils.removeTab(tab); } -async function test_register_direct_cancel() { - // Open a new tab. - let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); - - // Request a new credential with direct attestation and wait for the prompt. - let active = true; - let promise = promiseWebAuthnMakeCredential(tab, "direct") - .then(arrivingHereIsBad) - .catch(expectNotAllowedError) - .then(() => (active = false)); - await promiseNotification("webauthn-prompt-register-direct"); - - // Cancel the request. - ok(active, "request should still be active"); - PopupNotifications.panel.firstElementChild.secondaryButton.click(); - await promise; - - // Close tab. - await BrowserTestUtils.removeTab(tab); -} - -async function test_register_direct_presence() { - // Open a new tab. - let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); - - // Request a new credential with direct attestation and wait for the prompt. - let active = true; - let promise = promiseWebAuthnMakeCredential(tab, "direct") - .then(arrivingHereIsBad) - .catch(expectNotAllowedError) - .then(() => (active = false)); - await promiseNotification("webauthn-prompt-register-direct"); - - // Click "proceed" and wait for presence prompt - let presence = promiseNotification("webauthn-prompt-presence"); - PopupNotifications.panel.firstElementChild.button.click(); - await presence; - - // Cancel the request. - ok(active, "request should still be active"); - PopupNotifications.panel.firstElementChild.button.click(); - await promise; - - // Close tab. - await BrowserTestUtils.removeTab(tab); -} - // Add two tabs, open WebAuthn in the first, switch, assert the prompt is // not visible, switch back, assert the prompt is there and cancel it. async function test_tab_switching() { @@ -359,7 +304,7 @@ async function test_window_switching() { await BrowserTestUtils.removeTab(tab); } -async function test_register_direct_proceed() { +async function test_register_direct_with_consent() { // Open a new tab. let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); @@ -367,7 +312,7 @@ async function test_register_direct_proceed() { let request = promiseWebAuthnMakeCredential(tab, "direct"); await promiseNotification("webauthn-prompt-register-direct"); - // Proceed. + // Click "Allow". PopupNotifications.panel.firstElementChild.button.click(); // Ensure we got "direct" attestation. @@ -377,7 +322,7 @@ async function test_register_direct_proceed() { await BrowserTestUtils.removeTab(tab); } -async function test_register_direct_proceed_anon() { +async function test_register_direct_without_consent() { // Open a new tab. let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); @@ -385,9 +330,8 @@ async function test_register_direct_proceed_anon() { let request = promiseWebAuthnMakeCredential(tab, "direct"); await promiseNotification("webauthn-prompt-register-direct"); - // Check "anonymize anyway" and proceed. - PopupNotifications.panel.firstElementChild.checkbox.checked = true; - PopupNotifications.panel.firstElementChild.button.click(); + // Click "Block". + PopupNotifications.panel.firstElementChild.secondaryButton.click(); // Ensure we got "none" attestation. await request.then(verifyAnonymizedCertificate); @@ -438,23 +382,22 @@ async function test_fullscreen_show_nav_toolbar() { await navToolboxHiddenPromise; - // Request a new credential and wait for the direct attestation consent - // prompt. + // Request a new credential with direct attestation. The consent prompt will + // keep the request active until we can verify that the nav toolbar is shown. let promptPromise = promiseNotification("webauthn-prompt-register-direct"); let navToolboxShownPromise = promiseNavToolboxStatus("shown"); let active = true; - let requestPromise = promiseWebAuthnMakeCredential(tab, "direct") - .then(arrivingHereIsBad) - .catch(expectNotAllowedError) - .then(() => (active = false)); + let requestPromise = promiseWebAuthnMakeCredential(tab, "direct").then( + () => (active = false) + ); await Promise.all([promptPromise, navToolboxShownPromise]); ok(active, "request is active"); ok(window.fullScreen, "window is fullscreen"); - // Cancel the request. + // Proceed through the consent prompt. PopupNotifications.panel.firstElementChild.secondaryButton.click(); await requestPromise; @@ -475,23 +418,22 @@ async function test_no_fullscreen_dom() { await fullScreenPaintPromise; ok(!!document.fullscreenElement, "a DOM element is fullscreen"); - // Request a new credential and wait for the direct attestation consent - // prompt. + // Request a new credential with direct attestation. The consent prompt will + // keep the request active until we can verify that we've left fullscreen. let promptPromise = promiseNotification("webauthn-prompt-register-direct"); fullScreenPaintPromise = promiseFullScreenPaint(); let active = true; - let requestPromise = promiseWebAuthnMakeCredential(tab, "direct") - .then(arrivingHereIsBad) - .catch(expectNotAllowedError) - .then(() => (active = false)); + let requestPromise = promiseWebAuthnMakeCredential(tab, "direct").then( + () => (active = false) + ); await Promise.all([promptPromise, fullScreenPaintPromise]); ok(active, "request is active"); ok(!document.fullscreenElement, "no DOM element is fullscreen"); - // Cancel the request. + // Proceed through the consent prompt. await waitForPopupNotificationSecurityDelay(); PopupNotifications.panel.firstElementChild.secondaryButton.click(); await requestPromise; diff --git a/dom/webgpu/BindGroup.cpp b/dom/webgpu/BindGroup.cpp index 7ae5b8c3bb..b3cd3d9d5e 100644 --- a/dom/webgpu/BindGroup.cpp +++ b/dom/webgpu/BindGroup.cpp @@ -22,13 +22,21 @@ BindGroup::BindGroup(Device* const aParent, RawId aId) BindGroup::~BindGroup() { Cleanup(); } void BindGroup::Cleanup() { - if (mValid && mParent) { - mValid = false; - auto bridge = mParent->GetBridge(); - if (bridge && bridge->IsOpen()) { - bridge->SendBindGroupDrop(mId); - } + if (!mValid) { + return; } + mValid = false; + + auto bridge = mParent->GetBridge(); + if (!bridge) { + return; + } + + if (bridge->CanSend()) { + bridge->SendBindGroupDrop(mId); + } + + wgpu_client_free_bind_group_id(bridge->GetClient(), mId); } } // namespace mozilla::webgpu diff --git a/dom/webgpu/BindGroupLayout.cpp b/dom/webgpu/BindGroupLayout.cpp index cb5035b308..b447b63496 100644 --- a/dom/webgpu/BindGroupLayout.cpp +++ b/dom/webgpu/BindGroupLayout.cpp @@ -22,12 +22,22 @@ BindGroupLayout::BindGroupLayout(Device* const aParent, RawId aId, bool aOwning) BindGroupLayout::~BindGroupLayout() { Cleanup(); } void BindGroupLayout::Cleanup() { - if (mValid && mParent) { - mValid = false; - auto bridge = mParent->GetBridge(); - if (mOwning && bridge && bridge->IsOpen()) { + if (!mValid) { + return; + } + mValid = false; + + auto bridge = mParent->GetBridge(); + if (!bridge) { + return; + } + + if (mOwning) { + if (bridge->CanSend()) { bridge->SendBindGroupLayoutDrop(mId); } + + wgpu_client_free_bind_group_layout_id(bridge->GetClient(), mId); } } diff --git a/dom/webgpu/Buffer.cpp b/dom/webgpu/Buffer.cpp index b7b689a9a0..3bd1353e69 100644 --- a/dom/webgpu/Buffer.cpp +++ b/dom/webgpu/Buffer.cpp @@ -24,7 +24,7 @@ GPU_IMPL_JS_WRAP(Buffer) NS_IMPL_CYCLE_COLLECTION_CLASS(Buffer) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Buffer) - tmp->Drop(); + tmp->Cleanup(); NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent) NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_UNLINK_END @@ -51,7 +51,7 @@ Buffer::Buffer(Device* const aParent, RawId aId, BufferAddress aSize, } Buffer::~Buffer() { - Drop(); + Cleanup(); mozilla::DropJSObjects(this); } @@ -136,11 +136,10 @@ already_AddRefed<Buffer> Buffer::Create(Device* aDevice, RawId aDeviceId, return buffer.forget(); } -void Buffer::Drop() { +void Buffer::Cleanup() { if (!mValid) { return; } - mValid = false; AbortMapRequest(); @@ -158,9 +157,16 @@ void Buffer::Drop() { GetDevice().UntrackBuffer(this); - if (GetDevice().IsBridgeAlive()) { - GetDevice().GetBridge()->SendBufferDrop(mId); + auto bridge = GetDevice().GetBridge(); + if (!bridge) { + return; } + + if (bridge->CanSend()) { + bridge->SendBufferDrop(mId); + } + + wgpu_client_free_buffer_id(bridge->GetClient(), mId); } void Buffer::SetMapped(BufferAddress aOffset, BufferAddress aSize, diff --git a/dom/webgpu/Buffer.h b/dom/webgpu/Buffer.h index 2f809a4768..fa3881f38a 100644 --- a/dom/webgpu/Buffer.h +++ b/dom/webgpu/Buffer.h @@ -69,7 +69,7 @@ class Buffer final : public ObjectBase, public ChildOf<Device> { ipc::WritableSharedMemoryMapping&& aShmem); virtual ~Buffer(); Device& GetDevice() { return *mParent; } - void Drop(); + void Cleanup(); void UnmapArrayBuffers(JSContext* aCx, ErrorResult& aRv); void RejectMapRequest(dom::Promise* aPromise, nsACString& message); void AbortMapRequest(); diff --git a/dom/webgpu/CanvasContext.cpp b/dom/webgpu/CanvasContext.cpp index 49f34196c4..46633d9160 100644 --- a/dom/webgpu/CanvasContext.cpp +++ b/dom/webgpu/CanvasContext.cpp @@ -10,6 +10,7 @@ #include "nsDisplayList.h" #include "mozilla/dom/HTMLCanvasElement.h" #include "mozilla/gfx/CanvasManagerChild.h" +#include "mozilla/gfx/gfxVars.h" #include "mozilla/layers/CanvasRenderer.h" #include "mozilla/layers/CompositableForwarder.h" #include "mozilla/layers/ImageDataSerializer.h" @@ -125,6 +126,17 @@ void CanvasContext::Configure(const dom::GPUCanvasConfiguration& aConfig) { mUseExternalTextureInSwapChain = wgpu_client_use_external_texture_in_swapChain( aConfig.mDevice->mId, ConvertTextureFormat(aConfig.mFormat)); + if (!gfx::gfxVars::AllowWebGPUPresentWithoutReadback()) { + mUseExternalTextureInSwapChain = false; + } +#ifdef XP_WIN + // When WebRender does not use hardware acceleration, disable external texture + // in swap chain. Since compositor device might not exist. + if (gfx::gfxVars::UseSoftwareWebRender() && + !gfx::gfxVars::AllowSoftwareWebRenderD3D11()) { + mUseExternalTextureInSwapChain = false; + } +#endif mTexture = aConfig.mDevice->InitSwapChain( mConfig.get(), mRemoteTextureOwnerId.ref(), mUseExternalTextureInSwapChain, mGfxFormat, mCanvasSize); @@ -143,7 +155,7 @@ void CanvasContext::Configure(const dom::GPUCanvasConfiguration& aConfig) { } void CanvasContext::Unconfigure() { - if (mBridge && mBridge->IsOpen() && mRemoteTextureOwnerId) { + if (mBridge && mBridge->CanSend() && mRemoteTextureOwnerId) { mBridge->SendSwapChainDrop( *mRemoteTextureOwnerId, layers::ToRemoteTextureTxnType(mFwdTransactionTracker), @@ -213,7 +225,7 @@ void CanvasContext::MaybeQueueSwapChainPresent() { Maybe<layers::SurfaceDescriptor> CanvasContext::SwapChainPresent() { mPendingSwapChainPresent = false; - if (!mBridge || !mBridge->IsOpen() || mRemoteTextureOwnerId.isNothing() || + if (!mBridge || !mBridge->CanSend() || mRemoteTextureOwnerId.isNothing() || !mTexture) { return Nothing(); } @@ -325,7 +337,7 @@ already_AddRefed<mozilla::gfx::SourceSurface> CanvasContext::GetSurfaceSnapshot( return nullptr; } - if (!mBridge || !mBridge->IsOpen() || mRemoteTextureOwnerId.isNothing()) { + if (!mBridge || !mBridge->CanSend() || mRemoteTextureOwnerId.isNothing()) { return nullptr; } diff --git a/dom/webgpu/CommandEncoder.cpp b/dom/webgpu/CommandEncoder.cpp index f254c9d8b9..3775f63555 100644 --- a/dom/webgpu/CommandEncoder.cpp +++ b/dom/webgpu/CommandEncoder.cpp @@ -85,9 +85,16 @@ void CommandEncoder::Cleanup() { return; } mValid = false; - if (mBridge->IsOpen()) { + + if (!mBridge) { + return; + } + + if (mBridge->CanSend()) { mBridge->SendCommandEncoderDrop(mId); } + + wgpu_client_free_command_encoder_id(mBridge->GetClient(), mId); } void CommandEncoder::TrackPresentationContext(CanvasContext* aTargetContext) { @@ -103,7 +110,7 @@ void CommandEncoder::CopyBufferToBuffer(const Buffer& aSource, const Buffer& aDestination, BufferAddress aDestinationOffset, BufferAddress aSize) { - if (!mBridge->IsOpen()) { + if (!mBridge->CanSend()) { return; } @@ -118,7 +125,7 @@ void CommandEncoder::CopyBufferToTexture( const dom::GPUImageCopyBuffer& aSource, const dom::GPUImageCopyTexture& aDestination, const dom::GPUExtent3D& aCopySize) { - if (!mBridge->IsOpen()) { + if (!mBridge->CanSend()) { return; } @@ -136,7 +143,7 @@ void CommandEncoder::CopyTextureToBuffer( const dom::GPUImageCopyTexture& aSource, const dom::GPUImageCopyBuffer& aDestination, const dom::GPUExtent3D& aCopySize) { - if (!mBridge->IsOpen()) { + if (!mBridge->CanSend()) { return; } @@ -152,7 +159,7 @@ void CommandEncoder::CopyTextureToTexture( const dom::GPUImageCopyTexture& aSource, const dom::GPUImageCopyTexture& aDestination, const dom::GPUExtent3D& aCopySize) { - if (!mBridge->IsOpen()) { + if (!mBridge->CanSend()) { return; } @@ -181,7 +188,7 @@ void CommandEncoder::ClearBuffer(const Buffer& aBuffer, const uint64_t aOffset, } void CommandEncoder::PushDebugGroup(const nsAString& aString) { - if (!mBridge->IsOpen()) { + if (!mBridge->CanSend()) { return; } @@ -191,7 +198,7 @@ void CommandEncoder::PushDebugGroup(const nsAString& aString) { mBridge->SendCommandEncoderAction(mId, mParent->mId, std::move(bb)); } void CommandEncoder::PopDebugGroup() { - if (!mBridge->IsOpen()) { + if (!mBridge->CanSend()) { return; } @@ -200,7 +207,7 @@ void CommandEncoder::PopDebugGroup() { mBridge->SendCommandEncoderAction(mId, mParent->mId, std::move(bb)); } void CommandEncoder::InsertDebugMarker(const nsAString& aString) { - if (!mBridge->IsOpen()) { + if (!mBridge->CanSend()) { return; } @@ -230,7 +237,7 @@ already_AddRefed<RenderPassEncoder> CommandEncoder::BeginRenderPass( } void CommandEncoder::EndComputePass(ffi::WGPURecordedComputePass& aPass) { - if (!mBridge->IsOpen()) { + if (!mBridge->CanSend()) { return; } @@ -240,7 +247,7 @@ void CommandEncoder::EndComputePass(ffi::WGPURecordedComputePass& aPass) { } void CommandEncoder::EndRenderPass(ffi::WGPURecordedRenderPass& aPass) { - if (!mBridge->IsOpen()) { + if (!mBridge->CanSend()) { return; } diff --git a/dom/webgpu/CompilationInfo.cpp b/dom/webgpu/CompilationInfo.cpp index 0d46db2e59..995fb17c1a 100644 --- a/dom/webgpu/CompilationInfo.cpp +++ b/dom/webgpu/CompilationInfo.cpp @@ -18,8 +18,21 @@ CompilationInfo::CompilationInfo(Device* const aParent) : ChildOf(aParent) {} void CompilationInfo::SetMessages( nsTArray<mozilla::webgpu::WebGPUCompilationMessage>& aMessages) { for (auto& msg : aMessages) { + auto messageType = dom::GPUCompilationMessageType::Error; + switch (msg.messageType) { + case WebGPUCompilationMessageType::Error: + messageType = dom::GPUCompilationMessageType::Error; + break; + case WebGPUCompilationMessageType::Warning: + messageType = dom::GPUCompilationMessageType::Warning; + break; + case WebGPUCompilationMessageType::Info: + messageType = dom::GPUCompilationMessageType::Info; + break; + } mMessages.AppendElement(MakeAndAddRef<mozilla::webgpu::CompilationMessage>( - mParent, msg.lineNum, msg.linePos, msg.offset, std::move(msg.message))); + mParent, messageType, msg.lineNum, msg.linePos, msg.offset, msg.length, + std::move(msg.message))); } } diff --git a/dom/webgpu/CompilationMessage.cpp b/dom/webgpu/CompilationMessage.cpp index 1463ec408e..7aa4d01622 100644 --- a/dom/webgpu/CompilationMessage.cpp +++ b/dom/webgpu/CompilationMessage.cpp @@ -12,13 +12,17 @@ namespace mozilla::webgpu { GPU_IMPL_CYCLE_COLLECTION(CompilationMessage, mParent) GPU_IMPL_JS_WRAP(CompilationMessage) -CompilationMessage::CompilationMessage(Device* const aParent, uint64_t aLineNum, - uint64_t aLinePos, uint64_t aOffset, +CompilationMessage::CompilationMessage(Device* const aParent, + dom::GPUCompilationMessageType aType, + uint64_t aLineNum, uint64_t aLinePos, + uint64_t aOffset, uint64_t aLength, nsString&& aMessage) : ChildOf(aParent), + mType(aType), mLineNum(aLineNum), mLinePos(aLinePos), mOffset(aOffset), + mLength(aLength), mMessage(std::move(aMessage)) {} } // namespace mozilla::webgpu diff --git a/dom/webgpu/CompilationMessage.h b/dom/webgpu/CompilationMessage.h index bac786dec6..d5f252f26a 100644 --- a/dom/webgpu/CompilationMessage.h +++ b/dom/webgpu/CompilationMessage.h @@ -18,7 +18,7 @@ namespace webgpu { class CompilationInfo; class CompilationMessage final : public nsWrapperCache, public ChildOf<Device> { - dom::GPUCompilationMessageType mType = dom::GPUCompilationMessageType::Error; + dom::GPUCompilationMessageType mType; uint64_t mLineNum = 0; uint64_t mLinePos = 0; uint64_t mOffset = 0; @@ -29,8 +29,10 @@ class CompilationMessage final : public nsWrapperCache, public ChildOf<Device> { GPU_DECL_CYCLE_COLLECTION(CompilationMessage) GPU_DECL_JS_WRAP(CompilationMessage) - explicit CompilationMessage(Device* const aParent, uint64_t aLineNum, - uint64_t aLinePos, uint64_t aOffset, + explicit CompilationMessage(Device* const aParent, + dom::GPUCompilationMessageType aType, + uint64_t aLineNum, uint64_t aLinePos, + uint64_t aOffset, uint64_t aLength, nsString&& aMessage); void GetMessage(dom::DOMString& aMessage) { diff --git a/dom/webgpu/ComputePipeline.cpp b/dom/webgpu/ComputePipeline.cpp index ca50c5583a..ecd43289f3 100644 --- a/dom/webgpu/ComputePipeline.cpp +++ b/dom/webgpu/ComputePipeline.cpp @@ -27,17 +27,28 @@ ComputePipeline::ComputePipeline(Device* const aParent, RawId aId, ComputePipeline::~ComputePipeline() { Cleanup(); } void ComputePipeline::Cleanup() { - if (mValid && mParent) { - mValid = false; - auto bridge = mParent->GetBridge(); - if (bridge && bridge->IsOpen()) { - bridge->SendComputePipelineDrop(mId); - if (mImplicitPipelineLayoutId) { - bridge->SendImplicitLayoutDrop(mImplicitPipelineLayoutId, - mImplicitBindGroupLayoutIds); - } + if (mValid) { + return; + } + mValid = false; + + auto bridge = mParent->GetBridge(); + if (bridge->CanSend()) { + bridge->SendComputePipelineDrop(mId); + if (mImplicitPipelineLayoutId) { + bridge->SendImplicitLayoutDrop(mImplicitPipelineLayoutId, + mImplicitBindGroupLayoutIds); } } + + if (mImplicitPipelineLayoutId) { + wgpu_client_free_pipeline_layout_id(bridge->GetClient(), + mImplicitPipelineLayoutId); + } + + for (const auto& id : mImplicitBindGroupLayoutIds) { + wgpu_client_free_bind_group_layout_id(bridge->GetClient(), id); + } } already_AddRefed<BindGroupLayout> ComputePipeline::GetBindGroupLayout( diff --git a/dom/webgpu/Instance.cpp b/dom/webgpu/Instance.cpp index 4bf58b7fa8..30a82cbb26 100644 --- a/dom/webgpu/Instance.cpp +++ b/dom/webgpu/Instance.cpp @@ -90,7 +90,7 @@ already_AddRefed<dom::Promise> Instance::RequestAdapter( auto* const canvasManager = gfx::CanvasManagerChild::Get(); if (!canvasManager) { promise->MaybeRejectWithInvalidStateError( - "Failed to create CanavasManagerChild"); + "Failed to create CanvasManagerChild"); return promise.forget(); } diff --git a/dom/webgpu/ObjectModel.h b/dom/webgpu/ObjectModel.h index 59e154bd44..72a6d848d3 100644 --- a/dom/webgpu/ObjectModel.h +++ b/dom/webgpu/ObjectModel.h @@ -26,28 +26,38 @@ class ChildOf { nsIGlobalObject* GetParentObject() const; }; +/// Most WebGPU DOM objects inherit from this class. +/// +/// mValid should only be used in the destruction steps in Cleanup() to check +/// whether they have already run. This is because the destruction steps can be +/// triggered by either the object's destructor or the cycle collector +/// attempting to break a cycle. As a result, all methods accessible from JS can +/// assume that mValid is true. +/// +/// Similarly, pointers to the device and the IPDL actor (bridge) can be assumed +/// to be non-null whenever the object is accessible from JS but not during +/// cleanup as they might have been snatched by cycle collection. +/// +/// The general pattern is that all objects should implement Cleanup more or +/// less the same way. Cleanup should be the only function sending the +/// corresponding Drop message and cleanup should *never* be called by anything +/// other than the object destructor or the cycle collector. +/// +/// These rules guarantee that: +/// - The Drop message is called only once and that no other IPC message +/// referring +/// to the same object is send after Drop. +/// - Any method outside of the destruction sequence can assume the pointers are +/// non-null. They only have to check that the IPDL actor can send messages +/// using `WebGPUChild::CanSend()`. class ObjectBase : public nsWrapperCache { protected: virtual ~ObjectBase() = default; - // False if this object is definitely invalid. - // - // See WebGPU §3.2, "Invalid Internal Objects & Contagious Invalidity". - // - // There could also be state in the GPU process indicating that our - // counterpart object there is invalid; certain GPU process operations will - // report an error back to use if we try to use it. But if it's useful to know - // whether the object is "definitely invalid", this should suffice. + /// False during the destruction sequence of the object. bool mValid = true; public: - // Return true if this WebGPU object may be valid. - // - // This is used by methods that want to know whether somebody other than - // `this` is valid. Generally, WebGPU object methods check `this->mValid` - // directly. - bool IsValid() const { return mValid; } - void GetLabel(nsAString& aValue) const; void SetLabel(const nsAString& aLabel); diff --git a/dom/webgpu/PipelineLayout.cpp b/dom/webgpu/PipelineLayout.cpp index 716be9c74a..5aa12dff21 100644 --- a/dom/webgpu/PipelineLayout.cpp +++ b/dom/webgpu/PipelineLayout.cpp @@ -22,13 +22,21 @@ PipelineLayout::PipelineLayout(Device* const aParent, RawId aId) PipelineLayout::~PipelineLayout() { Cleanup(); } void PipelineLayout::Cleanup() { - if (mValid && mParent) { - mValid = false; - auto bridge = mParent->GetBridge(); - if (bridge && bridge->IsOpen()) { - bridge->SendPipelineLayoutDrop(mId); - } + if (!mValid) { + return; } + mValid = false; + + auto bridge = mParent->GetBridge(); + if (!bridge) { + return; + } + + if (bridge->CanSend()) { + bridge->SendPipelineLayoutDrop(mId); + } + + wgpu_client_free_pipeline_layout_id(bridge->GetClient(), mId); } } // namespace mozilla::webgpu diff --git a/dom/webgpu/RenderBundle.cpp b/dom/webgpu/RenderBundle.cpp index a60d8d5f8e..dc3bd8cabb 100644 --- a/dom/webgpu/RenderBundle.cpp +++ b/dom/webgpu/RenderBundle.cpp @@ -23,13 +23,20 @@ RenderBundle::RenderBundle(Device* const aParent, RawId aId) RenderBundle::~RenderBundle() { Cleanup(); } void RenderBundle::Cleanup() { - if (mValid && mParent) { - mValid = false; - auto bridge = mParent->GetBridge(); - if (bridge && bridge->IsOpen()) { - bridge->SendRenderBundleDrop(mId); - } + if (!mValid) { + return; } + mValid = false; + + auto bridge = mParent->GetBridge(); + if (!bridge) { + return; + } + + if (bridge->CanSend()) { + bridge->SendRenderBundleDrop(mId); + } + wgpu_client_free_render_bundle_id(bridge->GetClient(), mId); } } // namespace mozilla::webgpu diff --git a/dom/webgpu/RenderPipeline.cpp b/dom/webgpu/RenderPipeline.cpp index 78e13d31ef..aa9ffcdf9a 100644 --- a/dom/webgpu/RenderPipeline.cpp +++ b/dom/webgpu/RenderPipeline.cpp @@ -27,17 +27,32 @@ RenderPipeline::RenderPipeline(Device* const aParent, RawId aId, RenderPipeline::~RenderPipeline() { Cleanup(); } void RenderPipeline::Cleanup() { - if (mValid && mParent) { - mValid = false; - auto bridge = mParent->GetBridge(); - if (bridge && bridge->IsOpen()) { - bridge->SendRenderPipelineDrop(mId); - if (mImplicitPipelineLayoutId) { - bridge->SendImplicitLayoutDrop(mImplicitPipelineLayoutId, - mImplicitBindGroupLayoutIds); - } + if (!mValid) { + return; + } + mValid = false; + + auto bridge = mParent->GetBridge(); + if (!bridge) { + return; + } + + if (bridge->CanSend()) { + bridge->SendRenderPipelineDrop(mId); + if (mImplicitPipelineLayoutId) { + bridge->SendImplicitLayoutDrop(mImplicitPipelineLayoutId, + mImplicitBindGroupLayoutIds); } } + + if (mImplicitPipelineLayoutId) { + wgpu_client_free_pipeline_layout_id(bridge->GetClient(), + mImplicitPipelineLayoutId); + } + + for (const auto& id : mImplicitBindGroupLayoutIds) { + wgpu_client_free_bind_group_layout_id(bridge->GetClient(), id); + } } already_AddRefed<BindGroupLayout> RenderPipeline::GetBindGroupLayout( diff --git a/dom/webgpu/Sampler.cpp b/dom/webgpu/Sampler.cpp index 6ee5f3c41f..6abd8ca357 100644 --- a/dom/webgpu/Sampler.cpp +++ b/dom/webgpu/Sampler.cpp @@ -22,13 +22,21 @@ Sampler::Sampler(Device* const aParent, RawId aId) Sampler::~Sampler() { Cleanup(); } void Sampler::Cleanup() { - if (mValid && mParent) { - mValid = false; - auto bridge = mParent->GetBridge(); - if (bridge && bridge->IsOpen()) { - bridge->SendSamplerDrop(mId); - } + if (!mValid) { + return; } + + mValid = false; + auto bridge = mParent->GetBridge(); + if (!bridge) { + return; + } + + if (bridge->CanSend()) { + bridge->SendSamplerDrop(mId); + } + + wgpu_client_free_sampler_id(bridge->GetClient(), mId); } } // namespace mozilla::webgpu diff --git a/dom/webgpu/ShaderModule.cpp b/dom/webgpu/ShaderModule.cpp index bd123c8423..fce469a174 100644 --- a/dom/webgpu/ShaderModule.cpp +++ b/dom/webgpu/ShaderModule.cpp @@ -25,13 +25,21 @@ ShaderModule::ShaderModule(Device* const aParent, RawId aId, ShaderModule::~ShaderModule() { Cleanup(); } void ShaderModule::Cleanup() { - if (mValid && mParent) { - mValid = false; - auto bridge = mParent->GetBridge(); - if (bridge && bridge->IsOpen()) { - bridge->SendShaderModuleDrop(mId); - } + if (!mValid) { + return; } + mValid = false; + + auto bridge = mParent->GetBridge(); + if (!bridge) { + return; + } + + if (bridge->CanSend()) { + bridge->SendShaderModuleDrop(mId); + } + + wgpu_client_free_shader_module_id(bridge->GetClient(), mId); } already_AddRefed<dom::Promise> ShaderModule::CompilationInfo(ErrorResult& aRv) { diff --git a/dom/webgpu/Texture.cpp b/dom/webgpu/Texture.cpp index c7bc406118..82e17c51c6 100644 --- a/dom/webgpu/Texture.cpp +++ b/dom/webgpu/Texture.cpp @@ -46,21 +46,21 @@ Texture::Texture(Device* const aParent, RawId aId, } void Texture::Cleanup() { - if (!mParent) { + if (!mValid) { return; } + mValid = false; auto bridge = mParent->GetBridge(); - if (bridge && bridge->IsOpen()) { + if (!bridge) { + return; + } + + if (bridge->CanSend()) { bridge->SendTextureDrop(mId); } - // After cleanup is called, no other method should ever be called on the - // object so we don't have to null-check mParent in other places. - // This serves the purpose of preventing SendTextureDrop from happening - // twice. TODO: Does it matter for breaking cycles too? Cleanup is called - // by the macros that deal with cycle colleciton. - mParent = nullptr; + wgpu_client_free_texture_id(bridge->GetClient(), mId); } Texture::~Texture() { Cleanup(); } @@ -113,7 +113,7 @@ already_AddRefed<TextureView> Texture::CreateView( void Texture::Destroy() { auto bridge = mParent->GetBridge(); - if (bridge && bridge->IsOpen()) { + if (bridge && bridge->CanSend()) { bridge->SendTextureDestroy(mId, mParent->GetId()); } } diff --git a/dom/webgpu/Texture.h b/dom/webgpu/Texture.h index e31878f825..12677abe80 100644 --- a/dom/webgpu/Texture.h +++ b/dom/webgpu/Texture.h @@ -35,7 +35,7 @@ class Texture final : public ObjectBase, public ChildOf<Device> { Texture(Device* const aParent, RawId aId, const dom::GPUTextureDescriptor& aDesc); - Device* GetParentDevice() { return mParent; } + Device* GetDevice() { return mParent; } const RawId mId; const dom::GPUTextureFormat mFormat; const Maybe<uint8_t> mBytesPerBlock; diff --git a/dom/webgpu/TextureView.cpp b/dom/webgpu/TextureView.cpp index c36818e9ea..4964de4b7f 100644 --- a/dom/webgpu/TextureView.cpp +++ b/dom/webgpu/TextureView.cpp @@ -27,13 +27,21 @@ CanvasContext* TextureView::GetTargetContext() const { } // namespace webgpu void TextureView::Cleanup() { - if (mValid && mParent && mParent->GetParentDevice()) { - mValid = false; - auto bridge = mParent->GetParentDevice()->GetBridge(); - if (bridge && bridge->IsOpen()) { - bridge->SendTextureViewDrop(mId); - } + if (!mValid || !mParent || !mParent->GetDevice()) { + return; } + mValid = false; + + auto bridge = mParent->GetDevice()->GetBridge(); + if (!bridge) { + return; + } + + if (bridge->CanSend()) { + bridge->SendTextureViewDrop(mId); + } + + wgpu_client_free_texture_view_id(bridge->GetClient(), mId); } } // namespace mozilla::webgpu diff --git a/dom/webgpu/ipc/WebGPUChild.cpp b/dom/webgpu/ipc/WebGPUChild.cpp index ab1a100736..d89923a5b9 100644 --- a/dom/webgpu/ipc/WebGPUChild.cpp +++ b/dom/webgpu/ipc/WebGPUChild.cpp @@ -226,7 +226,7 @@ void WebGPUChild::RegisterDevice(Device* const aDevice) { } void WebGPUChild::UnregisterDevice(RawId aDeviceId) { - if (IsOpen()) { + if (CanSend()) { SendDeviceDrop(aDeviceId); } mDeviceMap.erase(aDeviceId); diff --git a/dom/webgpu/ipc/WebGPUChild.h b/dom/webgpu/ipc/WebGPUChild.h index 37525420bd..bb572e5a03 100644 --- a/dom/webgpu/ipc/WebGPUChild.h +++ b/dom/webgpu/ipc/WebGPUChild.h @@ -58,8 +58,6 @@ class WebGPUChild final : public PWebGPUChild, public SupportsWeakPtr { public: explicit WebGPUChild(); - bool IsOpen() const { return CanSend(); } - RefPtr<AdapterPromise> InstanceRequestAdapter( const dom::GPURequestAdapterOptions& aOptions); Maybe<DeviceRequest> AdapterRequestDevice(RawId aSelfId, diff --git a/dom/webgpu/ipc/WebGPUParent.cpp b/dom/webgpu/ipc/WebGPUParent.cpp index 1c0560d31e..cc1fab90b6 100644 --- a/dom/webgpu/ipc/WebGPUParent.cpp +++ b/dom/webgpu/ipc/WebGPUParent.cpp @@ -1532,7 +1532,6 @@ std::shared_ptr<ExternalTexture> WebGPUParent::CreateExternalTexture( UniquePtr<ExternalTexture> texture = ExternalTexture::Create(aWidth, aHeight, aFormat, aUsage); if (!texture) { - MOZ_ASSERT_UNREACHABLE("unexpected to be called"); return nullptr; } diff --git a/dom/webgpu/ipc/WebGPUSerialize.h b/dom/webgpu/ipc/WebGPUSerialize.h index 03f9ee1676..4296c905f4 100644 --- a/dom/webgpu/ipc/WebGPUSerialize.h +++ b/dom/webgpu/ipc/WebGPUSerialize.h @@ -46,7 +46,7 @@ DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::webgpu::PopErrorScopeResult, resultType, message); DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::webgpu::WebGPUCompilationMessage, - message, lineNum, linePos); + message, lineNum, linePos, offset, length); #undef DEFINE_IPC_SERIALIZER_FFI_ENUM #undef DEFINE_IPC_SERIALIZER_DOM_ENUM diff --git a/dom/webgpu/mochitest/mochitest-no-pref.toml b/dom/webgpu/mochitest/mochitest-no-pref.toml index 511b840c0b..ef83071c66 100644 --- a/dom/webgpu/mochitest/mochitest-no-pref.toml +++ b/dom/webgpu/mochitest/mochitest-no-pref.toml @@ -1,6 +1,7 @@ [DEFAULT] subsuite = "webgpu" run-if = ["release_or_beta"] +skip-if = ["verify"] # `test-verify` jobs don't guarantee a GPU, so skip them. # Even if the pref were enabled, WebGPU is only available in secure contexts. # diff --git a/dom/webgpu/mochitest/mochitest.toml b/dom/webgpu/mochitest/mochitest.toml index f32d7a9a86..d83dca2808 100644 --- a/dom/webgpu/mochitest/mochitest.toml +++ b/dom/webgpu/mochitest/mochitest.toml @@ -1,6 +1,7 @@ [DEFAULT] subsuite = "webgpu" run-if = ["!release_or_beta"] +skip-if = ["verify"] # `test-verify` jobs don't guarantee a GPU, so skip them. prefs = [ "dom.webgpu.enabled=true", "dom.webgpu.workers.enabled=true", @@ -18,60 +19,26 @@ support-files = [ scheme = "https" ["test_basic_canvas.worker.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] ["test_buffer_mapping.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] ["test_buffer_mapping_invalid_device.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] ["test_command_buffer_creation.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] + +["test_compilation_message_pos.html"] ["test_context_configure.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] ["test_device_creation.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] ["test_device_lost.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] ["test_double_encoder_finish.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] ["test_enabled.html"] ["test_error_scope.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] ["test_insecure_context.html"] # This test checks that WebGPU is not available in insecure contexts. @@ -80,37 +47,13 @@ scheme = "http" ["test_navigator_gpu_not_replaceable.html"] ["test_queue_copyExternalImageToTexture.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] ["test_queue_write.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] ["test_queue_write_invalid_device.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] ["test_submit_compute_empty.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] ["test_submit_render_empty.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] ["test_submit_render_empty.worker.html"] -fail-if = [ - "os == 'linux' && os_version == '18.04'", - "os == 'mac'", -] diff --git a/dom/webgpu/mochitest/test_compilation_message_pos.html b/dom/webgpu/mochitest/test_compilation_message_pos.html new file mode 100644 index 0000000000..f582cac5c0 --- /dev/null +++ b/dom/webgpu/mochitest/test_compilation_message_pos.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <script> + ok( + SpecialPowers.getBoolPref("dom.webgpu.enabled"), + "Pref should be enabled." + ); + + async function testBody() { + const adapter = await navigator.gpu.requestAdapter(); + const device = await adapter.requestDevice(); + + device.pushErrorScope("validation"); + const shaderModule = device.createShaderModule({ + // `?` is invalid, and we want to see if the offset will be right with the kitten emoji + // before it. + code: "/*🐈🐈🐈🐈🐈🐈🐈*/?", + }); + + const error = await device.popErrorScope(); + ok(error, "expected error from invalid shader"); + is(error.message, "Shader module creation failed: Parsing error"); + + const compilationInfo = await shaderModule.getCompilationInfo(); + is(compilationInfo.messages.length, 1); + + const { length, lineNum, linePos, message, offset } = + compilationInfo.messages[0]; + is(length, 1); + is(lineNum, 1); + is(linePos, 19); + is( + message, + ` +Shader '' parsing error: expected global item ('struct', 'const', 'var', 'alias', ';', 'fn') or the end of the file, found '?' + ┌─ wgsl:1:12 + │ +1 │ /*🐈🐈🐈🐈🐈🐈🐈*/? + │ ^ expected global item ('struct', 'const', 'var', 'alias', ';', 'fn') or the end of the file + +` + ); + is(offset, 18); + } + + SimpleTest.waitForExplicitFinish(); + testBody() + .catch(e => ok(false, "Unhandled exception " + e)) + .finally(() => SimpleTest.finish()); + </script> + </body> +</html> diff --git a/dom/webidl/AudioEncoder.webidl b/dom/webidl/AudioEncoder.webidl new file mode 100644 index 0000000000..e3800c83f1 --- /dev/null +++ b/dom/webidl/AudioEncoder.webidl @@ -0,0 +1,80 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * The origin of this IDL file is + * https://w3c.github.io/webcodecs/#audioencoder + + * Some members of this API are codec-specific, in which case the source of the + * IDL are in the codec-specific registry entries, that are listed in + * https://w3c.github.io/webcodecs/codec_registry.html. Those members are + * commented with a link of the document in which the member is listed. + */ + +dictionary AudioEncoderSupport { + boolean supported; + AudioEncoderConfig config; +}; + +dictionary AudioEncoderConfig { + required DOMString codec; + [EnforceRange] unsigned long sampleRate; + [EnforceRange] unsigned long numberOfChannels; + [EnforceRange] unsigned long long bitrate; + BitrateMode bitrateMode = "variable"; + OpusEncoderConfig opus; +}; + +// Opus specific configuration options: +// https://w3c.github.io/webcodecs/opus_codec_registration.html +enum OpusBitstreamFormat { + "opus", + "ogg", +}; + +dictionary OpusEncoderConfig { + OpusBitstreamFormat format = "opus"; + [EnforceRange] unsigned long long frameDuration = 20000; + [EnforceRange] unsigned long complexity; + [EnforceRange] unsigned long packetlossperc = 0; + boolean useinbandfec = false; + boolean usedtx = false; +}; + +[Exposed=(Window,DedicatedWorker), SecureContext, Pref="dom.media.webcodecs.enabled"] +interface AudioEncoder : EventTarget { + [Throws] + constructor(AudioEncoderInit init); + + readonly attribute CodecState state; + readonly attribute unsigned long encodeQueueSize; + attribute EventHandler ondequeue; + + [Throws] + undefined configure(AudioEncoderConfig config); + [Throws, BinaryName="AudioEncoder::EncodeAudioData"] + undefined encode(AudioData data); + [Throws] + Promise<undefined> flush(); + [Throws] + undefined reset(); + [Throws] + undefined close(); + + [NewObject, Throws] + static Promise<AudioEncoderSupport> isConfigSupported(AudioEncoderConfig config); +}; + +dictionary AudioEncoderInit { + required EncodedAudioChunkOutputCallback output; + required WebCodecsErrorCallback error; +}; + +callback EncodedAudioChunkOutputCallback = + undefined (EncodedAudioChunk output, + optional EncodedAudioChunkMetadata metadata = {}); + +dictionary EncodedAudioChunkMetadata { + AudioDecoderConfig decoderConfig; +}; diff --git a/dom/webidl/CSSPageRule.webidl b/dom/webidl/CSSPageRule.webidl index afc999a7ee..2be3f8a66f 100644 --- a/dom/webidl/CSSPageRule.webidl +++ b/dom/webidl/CSSPageRule.webidl @@ -8,10 +8,8 @@ */ // https://drafts.csswg.org/cssom/#the-csspagerule-interface -// Per spec, this should inherit from CSSGroupingRule, but we don't -// implement this yet. [Exposed=Window] -interface CSSPageRule : CSSRule { +interface CSSPageRule : CSSGroupingRule { attribute UTF8String selectorText; [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style; }; diff --git a/dom/webidl/CSSScopeRule.webidl b/dom/webidl/CSSScopeRule.webidl new file mode 100644 index 0000000000..009209beaf --- /dev/null +++ b/dom/webidl/CSSScopeRule.webidl @@ -0,0 +1,14 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * The origin of this IDL file is + * https://drafts.csswg.org/css-cascade-6/#the-cssscoperule-interface + */ + +[Exposed=Window, Pref="layout.css.at-scope.enabled"] +interface CSSScopeRule : CSSGroupingRule { + readonly attribute UTF8String? start; + readonly attribute UTF8String? end; +}; diff --git a/dom/webidl/CSSStartingStyleRule.webidl b/dom/webidl/CSSStartingStyleRule.webidl new file mode 100644 index 0000000000..10a1f54377 --- /dev/null +++ b/dom/webidl/CSSStartingStyleRule.webidl @@ -0,0 +1,12 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * The origin of this IDL file is + * https://drafts.csswg.org/css-transitions-2/#the-cssstartingstylerule-interface + */ + +[Exposed=Window, Pref="layout.css.starting-style-at-rules.enabled"] +interface CSSStartingStyleRule : CSSGroupingRule { +}; diff --git a/dom/webidl/Document.webidl b/dom/webidl/Document.webidl index c82710bf8f..0b878da9ab 100644 --- a/dom/webidl/Document.webidl +++ b/dom/webidl/Document.webidl @@ -13,6 +13,7 @@ * https://drafts.csswg.org/cssom/#extensions-to-the-document-interface * https://drafts.csswg.org/cssom-view/#extensions-to-the-document-interface * https://wicg.github.io/feature-policy/#policy + * https://wicg.github.io/scroll-to-text-fragment/#feature-detectability */ interface ContentSecurityPolicy; @@ -98,8 +99,6 @@ interface Document : Node { HTMLCollection getElementsByTagNameNS(DOMString? namespace, DOMString localName); [Pure] HTMLCollection getElementsByClassName(DOMString classNames); - [Pure] - Element? getElementById(DOMString elementId); // These DOM methods cannot be accessed by UA Widget scripts // because the DOM element reflectors will be in the content scope, @@ -156,7 +155,7 @@ partial interface Document { [PutForwards=href, LegacyUnforgeable] readonly attribute Location? location; [SetterThrows] attribute DOMString domain; - readonly attribute DOMString referrer; + readonly attribute UTF8String referrer; [Throws] attribute DOMString cookie; readonly attribute DOMString lastModified; readonly attribute DOMString readyState; @@ -387,17 +386,6 @@ partial interface Document { readonly attribute Element? scrollingElement; }; -// http://dev.w3.org/2006/webapi/selectors-api2/#interface-definitions -partial interface Document { - [Throws, Pure] - Element? querySelector(UTF8String selectors); - [Throws, Pure] - NodeList querySelectorAll(UTF8String selectors); - - //(Not implemented)Element? find(DOMString selectors, optional (Element or sequence<Node>)? refNodes); - //(Not implemented)NodeList findAll(DOMString selectors, optional (Element or sequence<Node>)? refNodes); -}; - // https://drafts.csswg.org/web-animations/#extensions-to-the-document-interface partial interface Document { [Func="Document::AreWebAnimationsTimelinesEnabled"] @@ -758,3 +746,14 @@ partial interface Document { [ChromeOnly] boolean isActive(); }; + +Document includes NonElementParentNode; + +/** + * Extension to add the fragmentDirective property. + * https://wicg.github.io/scroll-to-text-fragment/#feature-detectability + */ +partial interface Document { + [Pref="dom.text_fragments.enabled", SameObject] + readonly attribute FragmentDirective fragmentDirective; +}; diff --git a/dom/webidl/DocumentFragment.webidl b/dom/webidl/DocumentFragment.webidl index 0b760abd04..5425c3d374 100644 --- a/dom/webidl/DocumentFragment.webidl +++ b/dom/webidl/DocumentFragment.webidl @@ -4,8 +4,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. * * The origin of this IDL file is - * http://www.w3.org/TR/2012/WD-dom-20120405/#interface-documentfragment - * http://www.w3.org/TR/2012/WD-selectors-api-20120628/#interface-definitions + * https://dom.spec.whatwg.org/#documentfragment * * Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C * liability, trademark and document use rules apply. @@ -15,16 +14,7 @@ interface DocumentFragment : Node { [Throws] constructor(); - - Element? getElementById(DOMString elementId); -}; - -// http://www.w3.org/TR/2012/WD-selectors-api-20120628/#interface-definitions -partial interface DocumentFragment { - [Throws] - Element? querySelector(UTF8String selectors); - [Throws] - NodeList querySelectorAll(UTF8String selectors); }; DocumentFragment includes ParentNode; +DocumentFragment includes NonElementParentNode; diff --git a/dom/webidl/Element.webidl b/dom/webidl/Element.webidl index 32cb1dd30b..bbca476b38 100644 --- a/dom/webidl/Element.webidl +++ b/dom/webidl/Element.webidl @@ -4,10 +4,9 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. * * The origin of this IDL file is - * http://dom.spec.whatwg.org/#element and - * http://domparsing.spec.whatwg.org/ and - * http://dev.w3.org/csswg/cssom-view/ and - * http://www.w3.org/TR/selectors-api/ + * https://dom.spec.whatwg.org/#interface-element + * https://domparsing.spec.whatwg.org/ + * https://drafts.csswg.org/cssom-view/ * * Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C * liability, trademark and document use rules apply. @@ -112,7 +111,7 @@ interface Element : Node { * Returns whether this element would be selected by the given selector * string. * - * See <http://dev.w3.org/2006/webapi/selectors-api2/#matchesselector> + * https://dom.spec.whatwg.org/#dom-element-matches */ [Throws, Pure, BinaryName="matches"] boolean mozMatchesSelector(UTF8String selector); @@ -192,7 +191,7 @@ interface mixin ElementCSSInlineStyle { readonly attribute CSSStyleDeclaration style; }; -// http://dev.w3.org/csswg/cssom-view/ +// https://drafts.csswg.org/cssom-view/ enum ScrollLogicalPosition { "start", "center", "end", "nearest" }; dictionary ScrollIntoViewOptions : ScrollOptions { ScrollLogicalPosition block = "start"; @@ -208,7 +207,7 @@ dictionary CheckVisibilityOptions { [ChromeOnly] boolean flush = true; }; -// http://dev.w3.org/csswg/cssom-view/#extensions-to-the-element-interface +// https://drafts.csswg.org/cssom-view/#extensions-to-the-element-interface partial interface Element { DOMRectList getClientRects(); DOMRect getBoundingClientRect(); @@ -256,6 +255,8 @@ partial interface Element { readonly attribute long scrollTopMax; [ChromeOnly] readonly attribute long scrollLeftMin; readonly attribute long scrollLeftMax; + + [Pref="layout.css.zoom.enabled"] readonly attribute double currentCSSZoom; }; // http://domparsing.spec.whatwg.org/#extensions-to-the-element-interface @@ -268,14 +269,6 @@ partial interface Element { undefined insertAdjacentHTML(DOMString position, DOMString text); }; -// http://www.w3.org/TR/selectors-api/#interface-definitions -partial interface Element { - [Throws, Pure] - Element? querySelector(UTF8String selectors); - [Throws, Pure] - NodeList querySelectorAll(UTF8String selectors); -}; - // https://dom.spec.whatwg.org/#dictdef-shadowrootinit dictionary ShadowRootInit { required ShadowRootMode mode; diff --git a/dom/webidl/FragmentDirective.webidl b/dom/webidl/FragmentDirective.webidl new file mode 100644 index 0000000000..7793eaa512 --- /dev/null +++ b/dom/webidl/FragmentDirective.webidl @@ -0,0 +1,11 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * The origin of this IDL file is + * https://wicg.github.io/scroll-to-text-fragment/ + */ +[Exposed=Window, Pref="dom.text_fragments.enabled"] +interface FragmentDirective { +}; diff --git a/dom/webidl/GleanMetrics.webidl b/dom/webidl/GleanMetrics.webidl index 47dc3262f1..162b9448a5 100644 --- a/dom/webidl/GleanMetrics.webidl +++ b/dom/webidl/GleanMetrics.webidl @@ -222,6 +222,16 @@ interface GleanCustomDistribution : GleanMetric { undefined accumulateSamples(sequence<long long> aSamples); /** + * Accumulates the provided single signed sample in the metric. + * + * @param aSample - The sample to be recorded by the metric. + * + * Notes: Discards any negative value of `sample` and reports an + * `ErrorType::InvalidValue`. + */ + undefined accumulateSingleSample(long long aSample); + + /** * **Test-only API** * * Gets the currently stored value as a DistributionData. diff --git a/dom/webidl/HTMLHyperlinkElementUtils.webidl b/dom/webidl/HTMLHyperlinkElementUtils.webidl index 652437c81b..4f147bc74b 100644 --- a/dom/webidl/HTMLHyperlinkElementUtils.webidl +++ b/dom/webidl/HTMLHyperlinkElementUtils.webidl @@ -12,25 +12,25 @@ interface mixin HTMLHyperlinkElementUtils { [CEReactions, SetterThrows] - stringifier attribute USVString href; + stringifier attribute UTF8String href; - readonly attribute USVString origin; + readonly attribute UTF8String origin; [CEReactions] - attribute USVString protocol; + attribute UTF8String protocol; [CEReactions] - attribute USVString username; + attribute UTF8String username; [CEReactions] - attribute USVString password; + attribute UTF8String password; [CEReactions] - attribute USVString host; + attribute UTF8String host; [CEReactions] - attribute USVString hostname; + attribute UTF8String hostname; [CEReactions] - attribute USVString port; + attribute UTF8String port; [CEReactions] - attribute USVString pathname; + attribute UTF8String pathname; [CEReactions] - attribute USVString search; + attribute UTF8String search; [CEReactions] - attribute USVString hash; + attribute UTF8String hash; }; diff --git a/dom/webidl/HTMLMarqueeElement.webidl b/dom/webidl/HTMLMarqueeElement.webidl index 58bc8416f9..3736cec67f 100644 --- a/dom/webidl/HTMLMarqueeElement.webidl +++ b/dom/webidl/HTMLMarqueeElement.webidl @@ -28,10 +28,6 @@ interface HTMLMarqueeElement : HTMLElement { [CEReactions, SetterThrows] attribute unsigned long vspace; [CEReactions, SetterThrows] attribute DOMString width; - attribute EventHandler onbounce; - attribute EventHandler onfinish; - attribute EventHandler onstart; - undefined start(); undefined stop(); }; diff --git a/dom/webidl/HashChangeEvent.webidl b/dom/webidl/HashChangeEvent.webidl index 0a99a41018..fb0ae7d119 100644 --- a/dom/webidl/HashChangeEvent.webidl +++ b/dom/webidl/HashChangeEvent.webidl @@ -15,12 +15,6 @@ interface HashChangeEvent : Event readonly attribute DOMString oldURL; readonly attribute DOMString newURL; - - undefined initHashChangeEvent(DOMString typeArg, - optional boolean canBubbleArg = false, - optional boolean cancelableArg = false, - optional DOMString oldURLArg = "", - optional DOMString newURLArg = ""); }; dictionary HashChangeEventInit : EventInit diff --git a/dom/webidl/IDBDatabase.webidl b/dom/webidl/IDBDatabase.webidl index 310c662c15..71002b57b2 100644 --- a/dom/webidl/IDBDatabase.webidl +++ b/dom/webidl/IDBDatabase.webidl @@ -5,11 +5,18 @@ * * The origin of this IDL file is * https://w3c.github.io/IndexedDB/#database-interface + * https://w3c.github.io/IndexedDB/#enumdef-idbtransactiondurability * * Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C * liability, trademark and document use rules apply. */ +enum IDBTransactionDurability { "default", "strict", "relaxed" }; + +dictionary IDBTransactionOptions { + IDBTransactionDurability durability = "default"; +}; + [Exposed=(Window,Worker)] interface IDBDatabase : EventTarget { [Constant] readonly attribute DOMString name; @@ -19,7 +26,8 @@ interface IDBDatabase : EventTarget { [NewObject, Throws] IDBTransaction transaction((DOMString or sequence<DOMString>) storeNames, - optional IDBTransactionMode mode = "readonly"); + optional IDBTransactionMode mode = "readonly", + optional IDBTransactionOptions options = {}); [NewObject, Throws] IDBObjectStore createObjectStore( DOMString name, diff --git a/dom/webidl/IDBFactory.webidl b/dom/webidl/IDBFactory.webidl index 37143bcbb5..76553037fc 100644 --- a/dom/webidl/IDBFactory.webidl +++ b/dom/webidl/IDBFactory.webidl @@ -39,6 +39,8 @@ interface IDBFactory { deleteDatabase(DOMString name, optional IDBOpenDBOptions options = {}); + Promise<sequence<IDBDatabaseInfo>> databases(); + [Throws] short cmp(any first, @@ -62,3 +64,8 @@ interface IDBFactory { DOMString name, optional IDBOpenDBOptions options = {}); }; + +dictionary IDBDatabaseInfo { + DOMString name; + unsigned long long version; +}; diff --git a/dom/webidl/IDBTransaction.webidl b/dom/webidl/IDBTransaction.webidl index 1bb52ff448..83d0edbcbf 100644 --- a/dom/webidl/IDBTransaction.webidl +++ b/dom/webidl/IDBTransaction.webidl @@ -23,6 +23,10 @@ enum IDBTransactionMode { interface IDBTransaction : EventTarget { [Throws] readonly attribute IDBTransactionMode mode; + + [Throws] + readonly attribute IDBTransactionDurability durability; + [SameObject] readonly attribute IDBDatabase db; readonly attribute DOMException? error; diff --git a/dom/webidl/IdentityCredential.webidl b/dom/webidl/IdentityCredential.webidl index 801b2dc0f8..fce994115c 100644 --- a/dom/webidl/IdentityCredential.webidl +++ b/dom/webidl/IdentityCredential.webidl @@ -23,8 +23,8 @@ dictionary IdentityCredentialRequestOptions { [GenerateConversionToJS] dictionary IdentityProviderConfig { required UTF8String configURL; - required USVString clientId; - USVString nonce; + required UTF8String clientId; + UTF8String nonce; }; // https://fedidcg.github.io/FedCM/#dictdef-identityproviderwellknown @@ -35,7 +35,7 @@ dictionary IdentityProviderWellKnown { // https://fedidcg.github.io/FedCM/#dictdef-identityprovidericon dictionary IdentityProviderIcon { - required USVString url; + required UTF8String url; unsigned long size; }; diff --git a/dom/webidl/MediaDebugInfo.webidl b/dom/webidl/MediaDebugInfo.webidl index 46192d8e59..7740a9dbd3 100644 --- a/dom/webidl/MediaDebugInfo.webidl +++ b/dom/webidl/MediaDebugInfo.webidl @@ -150,6 +150,7 @@ dictionary MediaDecoderStateMachineDebugInfo { boolean videoCompleted = false; MediaDecoderStateMachineDecodingStateDebugInfo stateObj = {}; MediaSinkDebugInfo mediaSink = {}; + double totalBufferingTimeMs = 0; }; dictionary MediaStateDebugInfo { @@ -195,6 +196,8 @@ dictionary MediaFormatReaderDebugInfo { MediaStateDebugInfo audioState = {}; MediaStateDebugInfo videoState = {}; MediaFrameStats frameStats = {}; + double totalReadMetadataTimeMs = 0.0; + double totalWaitingForVideoDataTimeMs = 0.0; }; dictionary BufferRange { diff --git a/dom/webidl/MutationEvent.webidl b/dom/webidl/MutationEvent.webidl index 1fca8d1c07..2f8bfc6b42 100644 --- a/dom/webidl/MutationEvent.webidl +++ b/dom/webidl/MutationEvent.webidl @@ -15,8 +15,6 @@ interface MutationEvent : Event const unsigned short MODIFICATION = 1; const unsigned short ADDITION = 2; const unsigned short REMOVAL = 3; - [ChromeOnly] - const unsigned short SMIL = 4; readonly attribute Node? relatedNode; readonly attribute DOMString prevValue; diff --git a/dom/webidl/NonElementParentNode.webidl b/dom/webidl/NonElementParentNode.webidl new file mode 100644 index 0000000000..574fb07cc7 --- /dev/null +++ b/dom/webidl/NonElementParentNode.webidl @@ -0,0 +1,12 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * The origin of this IDL file is + * https://dom.spec.whatwg.org/#nonelementparentnode + */ +interface mixin NonElementParentNode { + [Pure] + Element? getElementById(DOMString elementId); +}; diff --git a/dom/webidl/ParentNode.webidl b/dom/webidl/ParentNode.webidl index 4350fb8a05..07906ece57 100644 --- a/dom/webidl/ParentNode.webidl +++ b/dom/webidl/ParentNode.webidl @@ -30,4 +30,9 @@ interface mixin ParentNode { undefined append((Node or DOMString)... nodes); [CEReactions, Throws, Unscopable] undefined replaceChildren((Node or DOMString)... nodes); + + [Throws, Pure] + Element? querySelector(UTF8String selectors); + [Throws, Pure] + NodeList querySelectorAll(UTF8String selectors); }; diff --git a/dom/webidl/RTCIceCandidate.webidl b/dom/webidl/RTCIceCandidate.webidl index 865b029ab9..6e9a2caa93 100644 --- a/dom/webidl/RTCIceCandidate.webidl +++ b/dom/webidl/RTCIceCandidate.webidl @@ -4,7 +4,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. * * The origin of this IDL file is - * http://dev.w3.org/2011/webrtc/editor/webrtc.html#idl-def-RTCIceCandidate + * https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface */ dictionary RTCIceCandidateInit { @@ -14,16 +14,51 @@ dictionary RTCIceCandidateInit { DOMString? usernameFragment = null; }; +enum RTCIceComponent { + "rtp", + "rtcp" +}; + +enum RTCIceProtocol { + "udp", + "tcp" +}; + +enum RTCIceCandidateType { + "host", + "srflx", + "prflx", + "relay" +}; + +enum RTCIceTcpCandidateType { + "active", + "passive", + "so" +}; + [Pref="media.peerconnection.enabled", JSImplementation="@mozilla.org/dom/rtcicecandidate;1", Exposed=Window] interface RTCIceCandidate { [Throws] constructor(optional RTCIceCandidateInit candidateInitDict = {}); - - attribute DOMString candidate; - attribute DOMString? sdpMid; - attribute unsigned short? sdpMLineIndex; - attribute DOMString? usernameFragment; - [Default] object toJSON(); + readonly attribute DOMString candidate; + readonly attribute DOMString? sdpMid; + readonly attribute unsigned short? sdpMLineIndex; + readonly attribute DOMString? foundation; + readonly attribute RTCIceComponent? component; + readonly attribute unsigned long? priority; + readonly attribute DOMString? address; + readonly attribute RTCIceProtocol? protocol; + readonly attribute unsigned short? port; + readonly attribute RTCIceCandidateType? type; + readonly attribute RTCIceTcpCandidateType? tcpType; + readonly attribute DOMString? relatedAddress; + readonly attribute unsigned short? relatedPort; + readonly attribute DOMString? usernameFragment; + // TODO: add remaining members relayProtocol and url (bug 1886013) + // readonly attribute RTCIceServerTransportProtocol? relayProtocol; + // readonly attribute DOMString? url; + RTCIceCandidateInit toJSON(); }; diff --git a/dom/webidl/RTCStatsReport.webidl b/dom/webidl/RTCStatsReport.webidl index 8f61dceeb9..1305575e4a 100644 --- a/dom/webidl/RTCStatsReport.webidl +++ b/dom/webidl/RTCStatsReport.webidl @@ -200,13 +200,6 @@ dictionary RTCIceCandidatePairStats : RTCStats { unsigned long componentId; // moz }; -enum RTCIceCandidateType { - "host", - "srflx", - "prflx", - "relay" -}; - dictionary RTCIceCandidateStats : RTCStats { DOMString address; long port; diff --git a/dom/webidl/Range.webidl b/dom/webidl/Range.webidl index 9d06ec09c7..95dff7721a 100644 --- a/dom/webidl/Range.webidl +++ b/dom/webidl/Range.webidl @@ -94,3 +94,12 @@ partial interface Range { [ChromeOnly, Throws] ClientRectsAndTexts getClientRectsAndTexts(); }; + +// ChromeOnly methods that allow setting Range boundaries to cross +// shadow boundary. +partial interface Range { + [ChromeOnly, Throws] + undefined setStartAllowCrossShadowBoundary(Node refNode, unsigned long offset); + [ChromeOnly, Throws] + undefined setEndAllowCrossShadowBoundary(Node refNode, unsigned long offset); +}; diff --git a/dom/webidl/Request.webidl b/dom/webidl/Request.webidl index 54e4e5ec95..2480d2b39a 100644 --- a/dom/webidl/Request.webidl +++ b/dom/webidl/Request.webidl @@ -7,20 +7,25 @@ * https://fetch.spec.whatwg.org/#request-class */ -typedef (Request or USVString) RequestInfo; +typedef (Request or UTF8String) RequestInfo; typedef unsigned long nsContentPolicyType; [Exposed=(Window,Worker)] interface Request { + /** + * Note that Requests created from system principal (ie "privileged"/chrome) + * code will default to omitting credentials. You can override this behaviour + * using the ``credentials`` member on the ``init`` dictionary. + */ [Throws] constructor(RequestInfo input, optional RequestInit init = {}); readonly attribute ByteString method; - readonly attribute USVString url; + readonly attribute UTF8String url; [SameObject, BinaryName="headers_"] readonly attribute Headers headers; readonly attribute RequestDestination destination; - readonly attribute USVString referrer; + readonly attribute UTF8String referrer; [BinaryName="referrerPolicy_"] readonly attribute ReferrerPolicy referrerPolicy; readonly attribute RequestMode mode; @@ -29,6 +34,9 @@ interface Request { readonly attribute RequestRedirect redirect; readonly attribute DOMString integrity; + [Pref="dom.fetchKeepalive.enabled"] + readonly attribute boolean keepalive; + // If a main-thread fetch() promise rejects, the error passed will be a // nsresult code. [ChromeOnly] @@ -51,14 +59,21 @@ dictionary RequestInit { ByteString method; HeadersInit headers; BodyInit? body; - USVString referrer; + UTF8String referrer; ReferrerPolicy referrerPolicy; RequestMode mode; + /** + * If not set, defaults to "same-origin", except for system principal (chrome) + * requests where the default is "omit". + */ RequestCredentials credentials; RequestCache cache; RequestRedirect redirect; DOMString integrity; + [Pref="dom.fetchKeepalive.enabled"] + boolean keepalive; + [ChromeOnly] boolean mozErrors; diff --git a/dom/webidl/Response.webidl b/dom/webidl/Response.webidl index d04453c37c..5257c34176 100644 --- a/dom/webidl/Response.webidl +++ b/dom/webidl/Response.webidl @@ -18,13 +18,13 @@ interface Response { [NewObject] static Response error(); [Throws, - NewObject] static Response redirect(USVString url, optional unsigned short status = 302); + NewObject] static Response redirect(UTF8String url, optional unsigned short status = 302); [BinaryName=CreateFromJson, Throws, NewObject] static Response json(any data, optional ResponseInit init = {}); readonly attribute ResponseType type; - readonly attribute USVString url; + readonly attribute UTF8String url; readonly attribute boolean redirected; readonly attribute unsigned short status; readonly attribute boolean ok; diff --git a/dom/webidl/Selection.webidl b/dom/webidl/Selection.webidl index 496ebc0813..263c50181e 100644 --- a/dom/webidl/Selection.webidl +++ b/dom/webidl/Selection.webidl @@ -26,6 +26,7 @@ interface Selection { */ readonly attribute unsigned long rangeCount; readonly attribute DOMString type; + readonly attribute DOMString direction; /** * Returns the range at the specified index. Throws if the index is * out of range. @@ -49,6 +50,10 @@ interface Selection { undefined removeAllRanges(); [Throws, BinaryName="RemoveAllRanges"] undefined empty(); + + [Pref="dom.shadowdom.selection_across_boundary_enabled"] + sequence<StaticRange> getComposedRanges(ShadowRoot... shadowRoots); + [Throws, BinaryName="collapseJS"] undefined collapse(Node? node, optional unsigned long offset = 0); [Throws, BinaryName="collapseJS"] diff --git a/dom/webidl/Storage.webidl b/dom/webidl/Storage.webidl index 5ee5e2c737..a9e0393929 100644 --- a/dom/webidl/Storage.webidl +++ b/dom/webidl/Storage.webidl @@ -30,9 +30,6 @@ interface Storage { [Throws, NeedsSubjectPrincipal] undefined clear(); - - [ChromeOnly] - readonly attribute boolean isSessionOnly; }; /** diff --git a/dom/webidl/TextEvent.webidl b/dom/webidl/TextEvent.webidl new file mode 100644 index 0000000000..5b79e0e1b8 --- /dev/null +++ b/dom/webidl/TextEvent.webidl @@ -0,0 +1,21 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * The origin of this IDL file is + * https://w3c.github.io/uievents/#textevent + */ + +[Pref="dom.events.textevent.enabled", Exposed=Window] +interface TextEvent : UIEvent +{ + [NeedsSubjectPrincipal] + readonly attribute DOMString data; + + undefined initTextEvent(DOMString type, + optional boolean bubbles = false, + optional boolean cancelable = false, + optional Window? view = null, + optional DOMString data = "undefined"); +}; diff --git a/dom/webidl/TrustedTypes.webidl b/dom/webidl/TrustedTypes.webidl index 3b7e35534f..c43faacb79 100644 --- a/dom/webidl/TrustedTypes.webidl +++ b/dom/webidl/TrustedTypes.webidl @@ -5,6 +5,7 @@ * * The origin of this IDL file is * <https://w3c.github.io/trusted-types/dist/spec/>. + * It is augmented with Gecko-specific annotations. */ [Exposed=(Window,Worker), Pref="dom.security.trusted_types.enabled"] @@ -28,9 +29,9 @@ interface TrustedScriptURL { [Exposed=(Window,Worker), Pref="dom.security.trusted_types.enabled"] interface TrustedTypePolicy { readonly attribute DOMString name; - [NewObject] TrustedHTML createHTML(DOMString input, any... arguments); - [NewObject] TrustedScript createScript(DOMString input, any... arguments); - [NewObject] TrustedScriptURL createScriptURL(DOMString input, any... arguments); + [NewObject, Throws] TrustedHTML createHTML(DOMString input, any... arguments); + [NewObject, Throws] TrustedScript createScript(DOMString input, any... arguments); + [NewObject, Throws] TrustedScriptURL createScriptURL(DOMString input, any... arguments); }; dictionary TrustedTypePolicyOptions { diff --git a/dom/webidl/URL.webidl b/dom/webidl/URL.webidl index 9cdd7f7aac..1f1d709b08 100644 --- a/dom/webidl/URL.webidl +++ b/dom/webidl/URL.webidl @@ -17,43 +17,44 @@ interface URI; LegacyWindowAlias=webkitURL] interface URL { [Throws] - constructor(USVString url, optional USVString base); + constructor(UTF8String url, optional UTF8String base); - static boolean canParse(USVString url, optional USVString base); + static URL? parse(UTF8String url, optional UTF8String base); + static boolean canParse(UTF8String url, optional UTF8String base); [SetterThrows] - stringifier attribute USVString href; - readonly attribute USVString origin; - attribute USVString protocol; - attribute USVString username; - attribute USVString password; - attribute USVString host; - attribute USVString hostname; - attribute USVString port; - attribute USVString pathname; - attribute USVString search; + stringifier attribute UTF8String href; + readonly attribute UTF8String origin; + attribute UTF8String protocol; + attribute UTF8String username; + attribute UTF8String password; + attribute UTF8String host; + attribute UTF8String hostname; + attribute UTF8String port; + attribute UTF8String pathname; + attribute UTF8String search; [SameObject] readonly attribute URLSearchParams searchParams; - attribute USVString hash; + attribute UTF8String hash; [ChromeOnly] readonly attribute URI URI; [ChromeOnly] static URL fromURI(URI uri); - USVString toJSON(); + UTF8String toJSON(); }; [Exposed=(Window,DedicatedWorker,SharedWorker)] partial interface URL { [Throws] - static DOMString createObjectURL(Blob blob); + static UTF8String createObjectURL(Blob blob); [Throws] - static undefined revokeObjectURL(DOMString url); + static undefined revokeObjectURL(UTF8String url); [ChromeOnly, Throws] - static boolean isValidObjectURL(DOMString url); + static boolean isValidObjectURL(UTF8String url); // https://dvcs.w3.org/hg/html-media/raw-file/default/media-source/media-source.html [Throws] - static DOMString createObjectURL(MediaSource source); + static UTF8String createObjectURL(MediaSource source); }; diff --git a/dom/webidl/URLSearchParams.webidl b/dom/webidl/URLSearchParams.webidl index de4922b33c..74b2e2729c 100644 --- a/dom/webidl/URLSearchParams.webidl +++ b/dom/webidl/URLSearchParams.webidl @@ -16,21 +16,21 @@ [Exposed=(Window,Worker,WorkerDebugger)] interface URLSearchParams { [Throws] - constructor(optional (sequence<sequence<USVString>> or - record<USVString, USVString> or USVString) init = ""); + constructor(optional (sequence<sequence<UTF8String>> or + record<UTF8String, UTF8String> or UTF8String) init = ""); readonly attribute unsigned long size; - undefined append(USVString name, USVString value); - undefined delete(USVString name, optional USVString value); - USVString? get(USVString name); - sequence<USVString> getAll(USVString name); - boolean has(USVString name, optional USVString value); - undefined set(USVString name, USVString value); + undefined append(UTF8String name, UTF8String value); + undefined delete(UTF8String name, optional UTF8String value); + UTF8String? get(UTF8String name); + sequence<UTF8String> getAll(UTF8String name); + boolean has(UTF8String name, optional UTF8String value); + undefined set(UTF8String name, UTF8String value); [Throws] undefined sort(); - iterable<USVString, USVString>; + iterable<UTF8String, UTF8String>; stringifier; }; diff --git a/dom/webidl/XMLHttpRequest.webidl b/dom/webidl/XMLHttpRequest.webidl index 66cd6cc79d..b1cc840faa 100644 --- a/dom/webidl/XMLHttpRequest.webidl +++ b/dom/webidl/XMLHttpRequest.webidl @@ -32,9 +32,12 @@ dictionary MozXMLHttpRequestParameters { /** * If true, the request will be sent without cookie and authentication - * headers. + * headers. Defaults to true for system/privileged/chrome requests, + * and to false otherwise. + * Note that even if set to true, for system/privileged/chrome requests, + * manually-set 'Cookie' headers are not removed. */ - boolean mozAnon = false; + boolean mozAnon; /** * If true, the same origin policy will not be enforced on the request. diff --git a/dom/webidl/moz.build b/dom/webidl/moz.build index 3880b727e7..9f8832d042 100644 --- a/dom/webidl/moz.build +++ b/dom/webidl/moz.build @@ -238,6 +238,9 @@ with Files("NavigationPreloadManager.webidl"): with Files("Net*"): BUG_COMPONENT = ("Core", "Networking") +with Files("Notification*"): + BUG_COMPONENT = ("Core", "DOM: Notifications") + with Files("OfflineAudio*"): BUG_COMPONENT = ("Core", "Web Audio") @@ -275,7 +278,7 @@ with Files("ProgressEvent.webidl"): BUG_COMPONENT = ("Core", "DOM: Events") with Files("Push*"): - BUG_COMPONENT = ("Core", "DOM: Notifications") + BUG_COMPONENT = ("Core", "DOM: Push Subscriptions") with Files("RTC*"): BUG_COMPONENT = ("Core", "WebRTC") @@ -322,6 +325,9 @@ with Files("SubtleCrypto.webidl"): with Files("TCP*"): BUG_COMPONENT = ("Core", "DOM: Networking") +with Files("TextEvent.webidl"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + with Files("TextTrack*"): BUG_COMPONENT = ("Core", "Audio/Video") @@ -419,6 +425,7 @@ WEBIDL_FILES = [ "AudioData.webidl", "AudioDecoder.webidl", "AudioDestinationNode.webidl", + "AudioEncoder.webidl", "AudioListener.webidl", "AudioNode.webidl", "AudioParam.webidl", @@ -492,6 +499,8 @@ WEBIDL_FILES = [ "CSSPseudoElement.webidl", "CSSRule.webidl", "CSSRuleList.webidl", + "CSSScopeRule.webidl", + "CSSStartingStyleRule.webidl", "CSSStyleDeclaration.webidl", "CSSStyleRule.webidl", "CSSStyleSheet.webidl", @@ -566,6 +575,7 @@ WEBIDL_FILES = [ "FontFaceSet.webidl", "FontFaceSource.webidl", "FormData.webidl", + "FragmentDirective.webidl", "Function.webidl", "GainNode.webidl", "Gamepad.webidl", @@ -754,6 +764,7 @@ WEBIDL_FILES = [ "NodeFilter.webidl", "NodeIterator.webidl", "NodeList.webidl", + "NonElementParentNode.webidl", "Notification.webidl", "NotificationEvent.webidl", "NotifyPaintEvent.webidl", @@ -961,6 +972,7 @@ WEBIDL_FILES = [ "TextDecoderStream.webidl", "TextEncoder.webidl", "TextEncoderStream.webidl", + "TextEvent.webidl", "TextTrack.webidl", "TextTrackCue.webidl", "TextTrackCueList.webidl", diff --git a/dom/webtransport/api/WebTransportDatagramDuplexStream.cpp b/dom/webtransport/api/WebTransportDatagramDuplexStream.cpp index 30d7451414..b40991c9a1 100644 --- a/dom/webtransport/api/WebTransportDatagramDuplexStream.cpp +++ b/dom/webtransport/api/WebTransportDatagramDuplexStream.cpp @@ -258,7 +258,7 @@ NS_IMPL_ADDREF_INHERITED(OutgoingDatagramStreamAlgorithms, NS_IMPL_RELEASE_INHERITED(OutgoingDatagramStreamAlgorithms, UnderlyingSinkAlgorithmsWrapper) -already_AddRefed<Promise> OutgoingDatagramStreamAlgorithms::WriteCallback( +already_AddRefed<Promise> OutgoingDatagramStreamAlgorithms::WriteCallbackImpl( JSContext* aCx, JS::Handle<JS::Value> aChunk, WritableStreamDefaultController& aController, ErrorResult& aError) { // https://w3c.github.io/webtransport/#writedatagrams @@ -269,9 +269,9 @@ already_AddRefed<Promise> OutgoingDatagramStreamAlgorithms::WriteCallback( // rejected with a TypeError. { BufferSource == ArrayBuffer/ArrayBufferView } ArrayBufferViewOrArrayBuffer arrayBuffer; if (!arrayBuffer.Init(aCx, aChunk)) { - return Promise::CreateRejectedWithTypeError( - mDatagrams->GetParentObject(), - "Wrong type for Datagram stream write"_ns, aError); + JS_ClearPendingException(aCx); + aError.ThrowTypeError("Wrong type for Datagram stream write"_ns); + return nullptr; } // Step 3: Let datagrams be transport.[[Datagrams]]. diff --git a/dom/webtransport/api/WebTransportDatagramDuplexStream.h b/dom/webtransport/api/WebTransportDatagramDuplexStream.h index 510cbcac71..a9f8ce7b56 100644 --- a/dom/webtransport/api/WebTransportDatagramDuplexStream.h +++ b/dom/webtransport/api/WebTransportDatagramDuplexStream.h @@ -71,7 +71,7 @@ class OutgoingDatagramStreamAlgorithms final // Streams algorithms - already_AddRefed<Promise> WriteCallback( + already_AddRefed<Promise> WriteCallbackImpl( JSContext* aCx, JS::Handle<JS::Value> aChunk, WritableStreamDefaultController& aController, ErrorResult& aError) override; diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp index 02efb12053..321895e700 100644 --- a/dom/workers/RuntimeService.cpp +++ b/dom/workers/RuntimeService.cpp @@ -372,6 +372,9 @@ void LoadJSGCMemoryOptions(const char* aPrefName, void* /* aClosure */) { PREF("gc_parallel_marking", JSGC_PARALLEL_MARKING_ENABLED), PREF("gc_parallel_marking_threshold_mb", JSGC_PARALLEL_MARKING_THRESHOLD_MB), +#ifdef NIGHTLY_BUILD + PREF("gc_experimental_semispace_nursery", JSGC_SEMISPACE_NURSERY_ENABLED), +#endif // Note: Workers do not currently trigger eager minor GC, but if that is // desired the following parameters should be added: // javascript.options.mem.nursery_eager_collection_threshold_kb @@ -426,6 +429,9 @@ void LoadJSGCMemoryOptions(const char* aPrefName, void* /* aClosure */) { } case JSGC_COMPACTING_ENABLED: case JSGC_PARALLEL_MARKING_ENABLED: +#ifdef NIGHTLY_BUILD + case JSGC_SEMISPACE_NURSERY_ENABLED: +#endif case JSGC_BALANCED_HEAP_LIMITS_ENABLED: { bool present; bool prefValue = GetPref(pref->fullName, false, &present); diff --git a/dom/workers/ScriptLoader.cpp b/dom/workers/ScriptLoader.cpp index 73997b2725..9dcb1d0c9a 100644 --- a/dom/workers/ScriptLoader.cpp +++ b/dom/workers/ScriptLoader.cpp @@ -517,9 +517,7 @@ already_AddRefed<WorkerScriptLoader> WorkerScriptLoader::Create( } // Set up the module loader, if it has not been initialzied yet. - if (!aWorkerPrivate->IsServiceWorker()) { - self->InitModuleLoader(); - } + self->InitModuleLoader(); return self.forget(); } @@ -1065,8 +1063,8 @@ nsresult WorkerScriptLoader::LoadScript( // This flag reflects the fact that if the worker is created under a // third-party context. nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); - loadInfo->SetIsThirdPartyContextToTopWindow( - mWorkerRef->Private()->IsThirdPartyContextToTopWindow()); + loadInfo->SetIsInThirdPartyContext( + mWorkerRef->Private()->IsThirdPartyContext()); Maybe<ClientInfo> clientInfo; clientInfo.emplace(loadContext->mClientInfo.ref()); @@ -1856,12 +1854,6 @@ void ReportLoadError(ErrorResult& aRv, nsresult aLoadResult, NS_ConvertUTF16toUTF8(aScriptURL).get()); switch (aLoadResult) { - case NS_ERROR_FILE_NOT_FOUND: - case NS_ERROR_NOT_AVAILABLE: - case NS_ERROR_CORRUPTED_CONTENT: - aRv.Throw(NS_ERROR_DOM_NETWORK_ERR); - break; - case NS_ERROR_MALFORMED_URI: case NS_ERROR_DOM_SYNTAX_ERR: aRv.ThrowSyntaxError(err); @@ -1877,7 +1869,7 @@ void ReportLoadError(ErrorResult& aRv, nsresult aLoadResult, // make it impossible for consumers to realize that our error was // NS_BINDING_ABORTED. aRv.Throw(aLoadResult); - return; + break; case NS_ERROR_DOM_BAD_URI: // This is actually a security error. @@ -1885,15 +1877,16 @@ void ReportLoadError(ErrorResult& aRv, nsresult aLoadResult, aRv.ThrowSecurityError(err); break; + case NS_ERROR_FILE_NOT_FOUND: + case NS_ERROR_NOT_AVAILABLE: + case NS_ERROR_CORRUPTED_CONTENT: + case NS_ERROR_DOM_NETWORK_ERR: + // For lack of anything better, go ahead and throw a NetworkError here. + // We don't want to throw a JS exception, because for toplevel script + // loads that would get squelched. default: - // For lack of anything better, go ahead and throw a NetworkError here. - // We don't want to throw a JS exception, because for toplevel script - // loads that would get squelched. - aRv.ThrowNetworkError(nsPrintfCString( - "Failed to load worker script at %s (nsresult = 0x%" PRIx32 ")", - NS_ConvertUTF16toUTF8(aScriptURL).get(), - static_cast<uint32_t>(aLoadResult))); - return; + aRv.Throw(NS_ERROR_DOM_NETWORK_ERR); + break; } } diff --git a/dom/workers/Worker.cpp b/dom/workers/Worker.cpp index 2348572e65..88df53b877 100644 --- a/dom/workers/Worker.cpp +++ b/dom/workers/Worker.cpp @@ -83,6 +83,12 @@ JSObject* Worker::WrapObject(JSContext* aCx, return wrapper; } +bool Worker::IsEligibleForMessaging() { + NS_ASSERT_OWNINGTHREAD(Worker); + + return mWorkerPrivate && mWorkerPrivate->ParentStatusProtected() <= Running; +} + void Worker::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, const Sequence<JSObject*>& aTransferable, ErrorResult& aRv) { diff --git a/dom/workers/Worker.h b/dom/workers/Worker.h index 14d0630f28..6a0e295fc2 100644 --- a/dom/workers/Worker.h +++ b/dom/workers/Worker.h @@ -42,6 +42,31 @@ class Worker : public DOMEventTargetHelper, public SupportsWeakPtr { return Some(EventCallbackDebuggerNotificationType::Worker); } + // True if the worker is not yet closing from the perspective of this, the + // owning thread, and therefore it's okay to post a message to the worker. + // This is not a guarantee that the worker will process the message. + // + // This method will return false if `globalThis.close()` is invoked on the + // worker before that method returns control to the caller and without waiting + // for any task to be queued on this thread and run; this biases us to avoid + // doing wasteful work but does mean if you are exposing something to content + // that is specified to only transition as the result of a task, then you + // should not use this method. + // + // The method name comes from + // https://html.spec.whatwg.org/multipage/web-messaging.html#eligible-for-messaging + // and is intended to convey whether it's okay to begin to take the steps to + // create an `EventWithOptionsRunnable` to pass to `PostEventWithOptions`. + // Note that early returning based on calling this method without performing + // the structured serialization steps that would otherwise run is potentially + // observable to content if content is in control of any of the payload in + // such a way that an object with getters or a proxy could be provided. + // + // There is an identically named method on nsIGlobalObject and the semantics + // are intentionally similar but please make sure you document your + // assumptions when calling either method. + bool IsEligibleForMessaging(); + void PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, const Sequence<JSObject*>& aTransferable, ErrorResult& aRv); @@ -49,6 +74,8 @@ class Worker : public DOMEventTargetHelper, public SupportsWeakPtr { const StructuredSerializeOptions& aOptions, ErrorResult& aRv); + // Callers must call `IsEligibleForMessaging` before constructing an + // `EventWithOptionsRunnable` subclass. void PostEventWithOptions(JSContext* aCx, JS::Handle<JS::Value> aOptions, const Sequence<JSObject*>& aTransferable, EventWithOptionsRunnable* aRunnable, diff --git a/dom/workers/WorkerLoadInfo.cpp b/dom/workers/WorkerLoadInfo.cpp index 0dec07a675..29fb69c212 100644 --- a/dom/workers/WorkerLoadInfo.cpp +++ b/dom/workers/WorkerLoadInfo.cpp @@ -101,7 +101,7 @@ WorkerLoadInfoData::WorkerLoadInfoData() mUsingStorageAccess(false), mServiceWorkersTestingInWindow(false), mShouldResistFingerprinting(false), - mIsThirdPartyContextToTopWindow(true), + mIsThirdPartyContext(true), mSecureContext(eNotSet) {} nsresult WorkerLoadInfo::SetPrincipalsAndCSPOnMainThread( diff --git a/dom/workers/WorkerLoadInfo.h b/dom/workers/WorkerLoadInfo.h index 722e71d6f3..c86538145c 100644 --- a/dom/workers/WorkerLoadInfo.h +++ b/dom/workers/WorkerLoadInfo.h @@ -147,7 +147,7 @@ struct WorkerLoadInfoData { bool mShouldResistFingerprinting; Maybe<RFPTarget> mOverriddenFingerprintingSettings; OriginAttributes mOriginAttributes; - bool mIsThirdPartyContextToTopWindow; + bool mIsThirdPartyContext; enum { eNotSet, diff --git a/dom/workers/WorkerPrivate.cpp b/dom/workers/WorkerPrivate.cpp index a8643981aa..df248acda4 100644 --- a/dom/workers/WorkerPrivate.cpp +++ b/dom/workers/WorkerPrivate.cpp @@ -1443,8 +1443,8 @@ nsresult WorkerPrivate::SetCSPFromHeaderValues( } MOZ_ASSERT(selfURI, "need a self URI for CSP"); - rv = csp->SetRequestContextWithPrincipal(mLoadInfo.mPrincipal, selfURI, - u""_ns, 0); + rv = csp->SetRequestContextWithPrincipal(mLoadInfo.mPrincipal, selfURI, ""_ns, + 0); NS_ENSURE_SUCCESS(rv, rv); csp->EnsureEventTarget(mMainThreadEventTarget); @@ -1574,8 +1574,6 @@ nsresult WorkerPrivate::DispatchLockHeld( MOZ_ASSERT_IF(aSyncLoopTarget, mThread); if (mStatus == Dead || (!aSyncLoopTarget && ParentStatus() > Canceling)) { - LOGV(("WorkerPrivate::DispatchLockHeld [%p] runnable %p, parent status: %u", - this, runnable.get(), (uint8_t)(ParentStatus()))); NS_WARNING( "A runnable was posted to a worker that is already shutting " "down!"); @@ -1624,6 +1622,7 @@ nsresult WorkerPrivate::DispatchLockHeld( } if (NS_WARN_IF(NS_FAILED(rv))) { + LOGV(("WorkerPrivate::Dispatch Failed [%p]", this)); return rv; } @@ -2792,8 +2791,7 @@ nsresult WorkerPrivate::GetLoadInfo( loadInfo.mOriginAttributes = aParent->GetOriginAttributes(); loadInfo.mServiceWorkersTestingInWindow = aParent->ServiceWorkersTestingInWindow(); - loadInfo.mIsThirdPartyContextToTopWindow = - aParent->IsThirdPartyContextToTopWindow(); + loadInfo.mIsThirdPartyContext = aParent->IsThirdPartyContext(); loadInfo.mShouldResistFingerprinting = aParent->ShouldResistFingerprinting( RFPTarget::IsAlwaysEnabledForPrecompute); loadInfo.mOverriddenFingerprintingSettings = @@ -2954,7 +2952,7 @@ nsresult WorkerPrivate::GetLoadInfo( StorageAllowedForDocument(document) != StorageAccess::eAllow) { loadInfo.mUsingStorageAccess = false; } - loadInfo.mIsThirdPartyContextToTopWindow = + loadInfo.mIsThirdPartyContext = AntiTrackingUtils::IsThirdPartyWindow(globalWindow, nullptr); loadInfo.mCookieJarSettings = document->CookieJarSettings(); if (loadInfo.mCookieJarSettings) { @@ -3022,7 +3020,7 @@ nsresult WorkerPrivate::GetLoadInfo( cookieJarSettings->Serialize(loadInfo.mCookieJarSettingsArgs); loadInfo.mOriginAttributes = OriginAttributes(); - loadInfo.mIsThirdPartyContextToTopWindow = false; + loadInfo.mIsThirdPartyContext = false; } MOZ_ASSERT(loadInfo.mLoadingPrincipal); @@ -3104,10 +3102,33 @@ void WorkerPrivate::OverrideLoadInfoLoadGroup(WorkerLoadInfo& aLoadInfo, void WorkerPrivate::RunLoopNeverRan() { LOG(WorkerLog(), ("WorkerPrivate::RunLoopNeverRan [%p]", this)); + // RunLoopNeverRan is only called in WorkerThreadPrimaryRunnable::Run() to + // handle cases + // 1. Fail to get BackgroundChild for the worker thread or + // 2. Fail to initialize the worker's JS context + // However, mPreStartRunnables had already dispatched in + // WorkerThread::SetWorkerPrivateInWorkerThread() where beforing above jobs + // start. So we need to clean up these dispatched runnables for the worker + // thread. + + auto data = mWorkerThreadAccessible.Access(); + RefPtr<WorkerThread> thread; { MutexAutoLock lock(mMutex); - + // WorkerPrivate::DoRunLoop() is never called, so CompileScriptRunnable + // should not execute yet. However, the Worker is going to "Dead", flip the + // mCancelBeforeWorkerScopeConstructed to true for the dispatched runnables + // to indicate runnables there is no valid WorkerGlobalScope for executing. + MOZ_ASSERT(!data->mCancelBeforeWorkerScopeConstructed); + data->mCancelBeforeWorkerScopeConstructed.Flip(); + // Switch State to Dead mStatus = Dead; + thread = mThread; + } + + // Clear the dispatched mPreStartRunnables. + if (thread && NS_HasPendingEvents(thread)) { + NS_ProcessPendingEvents(nullptr); } // After mStatus is set to Dead there can be no more @@ -3230,11 +3251,6 @@ void WorkerPrivate::DoRunLoop(JSContext* aCx) { { MutexAutoLock lock(mMutex); - - LOGV( - ("WorkerPrivate::DoRunLoop [%p] mStatus %u before getting events" - " to run", - this, (uint8_t)mStatus)); if (checkFinalGCCC && currentStatus != mStatus) { // Something moved our status while we were supposed to check for a // potentially needed GC/CC. Just check again. @@ -3270,6 +3286,25 @@ void WorkerPrivate::DoRunLoop(JSContext* aCx) { currentStatus = mStatus; } + // Status transitions to Closing/Canceling and there are no SyncLoops, + // set global start dying, disconnect EventTargetObjects and + // WebTaskScheduler. + // The Worker might switch to the "Killing" immediately then directly exits + // DoRunLoop(). Before exiting the DoRunLoop(), explicitly disconnecting the + // WorkerGlobalScope's EventTargetObject here would help to fail runnable + // dispatching when the Worker is in the status changing. + if (currentStatus >= Closing && + !data->mPerformedShutdownAfterLastContentTaskExecuted) { + data->mPerformedShutdownAfterLastContentTaskExecuted.Flip(); + if (data->mScope) { + data->mScope->NoteTerminating(); + data->mScope->DisconnectGlobalTeardownObservers(); + if (data->mScope->GetExistingScheduler()) { + data->mScope->GetExistingScheduler()->Disconnect(); + } + } + } + // Transition from Canceling to Killing and exit this loop when: // * All (non-weak) WorkerRefs have been released. // * There are no runnables pending. This is intended to let same-thread @@ -3335,21 +3370,6 @@ void WorkerPrivate::DoRunLoop(JSContext* aCx) { } } - // Status transitions to Closing/Canceling and there are no SyncLoops, - // set global start dying, disconnect EventTargetObjects and - // WebTaskScheduler. - if (currentStatus >= Closing && - !data->mPerformedShutdownAfterLastContentTaskExecuted) { - data->mPerformedShutdownAfterLastContentTaskExecuted.Flip(); - if (data->mScope) { - data->mScope->NoteTerminating(); - data->mScope->DisconnectGlobalTeardownObservers(); - if (data->mScope->GetExistingScheduler()) { - data->mScope->GetExistingScheduler()->Disconnect(); - } - } - } - if (debuggerRunnablesPending || normalRunnablesPending) { // Start the periodic GC timer if it is not already running. SetGCTimerMode(PeriodicTimer); @@ -4324,7 +4344,6 @@ void WorkerPrivate::AdjustNonblockingCCBackgroundActorCount(int32_t aCount) { } void WorkerPrivate::UpdateCCFlag(const CCFlag aFlag) { - LOGV(("WorkerPrivate::UpdateCCFlag [%p]", this)); AssertIsOnWorkerThread(); auto data = mWorkerThreadAccessible.Access(); @@ -4984,17 +5003,6 @@ bool WorkerPrivate::NotifyInternal(WorkerStatus aStatus) { MOZ_ASSERT_IF(aStatus == Killing, mStatus == Canceling && mParentStatus == Canceling); - if (aStatus >= Canceling) { - MutexAutoUnlock unlock(mMutex); - if (data->mScope) { - if (aStatus == Canceling) { - data->mScope->NoteTerminating(); - } else { - data->mScope->NoteShuttingDown(); - } - } - } - mStatus = aStatus; // Mark parent status as closing immediately to avoid new events being @@ -5005,8 +5013,20 @@ bool WorkerPrivate::NotifyInternal(WorkerStatus aStatus) { // Synchronize the mParentStatus with mStatus, such that event dispatching // will fail in proper after WorkerPrivate gets into Killing status. - if (aStatus == Killing) { - mParentStatus = Killing; + if (aStatus >= Killing) { + mParentStatus = aStatus; + } + } + + // Status transistion to "Canceling"/"Killing", mark the scope as dying when + // "Canceling," or shutdown the StorageManager when "Killing." + if (aStatus >= Canceling) { + if (data->mScope) { + if (aStatus == Canceling) { + data->mScope->NoteTerminating(); + } else { + data->mScope->NoteShuttingDown(); + } } } diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h index a670d00975..ce754ba9f6 100644 --- a/dom/workers/WorkerPrivate.h +++ b/dom/workers/WorkerPrivate.h @@ -972,9 +972,7 @@ class WorkerPrivate final } // Determine if the worker was created under a third-party context. - bool IsThirdPartyContextToTopWindow() const { - return mLoadInfo.mIsThirdPartyContextToTopWindow; - } + bool IsThirdPartyContext() const { return mLoadInfo.mIsThirdPartyContext; } bool IsWatchedByDevTools() const { return mLoadInfo.mWatchedByDevTools; } diff --git a/dom/workers/WorkerRunnable.cpp b/dom/workers/WorkerRunnable.cpp index ff2178d16e..14a3e5e3f9 100644 --- a/dom/workers/WorkerRunnable.cpp +++ b/dom/workers/WorkerRunnable.cpp @@ -226,22 +226,6 @@ WorkerRunnable::Run() { LOG(("WorkerRunnable::Run [%p]", this)); bool targetIsWorkerThread = mTarget == WorkerThread; - if (targetIsWorkerThread) { - // On a worker thread, a WorkerRunnable should only run when there is an - // underlying WorkerThreadPrimaryRunnable active, which means we should - // find a CycleCollectedJSContext. - if (!CycleCollectedJSContext::Get()) { -#if (defined(MOZ_COLLECTING_RUNNABLE_TELEMETRY) && defined(NIGHTLY_BUILD)) - // We will only leak the static name string of the WorkerRunnable type - // we are trying to execute. - MOZ_CRASH_UNSAFE_PRINTF( - "Runnable '%s' executed after WorkerThreadPrimaryRunnable ended.", - this->mName); -#endif - return NS_OK; - } - } - #ifdef DEBUG if (targetIsWorkerThread) { mWorkerPrivate->AssertIsOnWorkerThread(); diff --git a/dom/workers/WorkerScope.cpp b/dom/workers/WorkerScope.cpp index 92d6c89dca..2121a99cb3 100644 --- a/dom/workers/WorkerScope.cpp +++ b/dom/workers/WorkerScope.cpp @@ -721,7 +721,7 @@ void WorkerGlobalScope::GetJSTestingFunctions( } already_AddRefed<Promise> WorkerGlobalScope::Fetch( - const RequestOrUSVString& aInput, const RequestInit& aInit, + const RequestOrUTF8String& aInput, const RequestInit& aInit, CallerType aCallerType, ErrorResult& aRv) { return FetchRequest(this, aInput, aInit, aCallerType, aRv); } diff --git a/dom/workers/WorkerScope.h b/dom/workers/WorkerScope.h index 12a97e12c3..668755ce4a 100644 --- a/dom/workers/WorkerScope.h +++ b/dom/workers/WorkerScope.h @@ -68,7 +68,7 @@ template <typename T> class Optional; class Performance; class Promise; -class RequestOrUSVString; +class RequestOrUTF8String; template <typename T> class Sequence; class ServiceWorkerDescriptor; @@ -326,7 +326,7 @@ class WorkerGlobalScope : public WorkerGlobalScopeBase { JS::MutableHandle<JS::Value> aRetval, ErrorResult& aError); - already_AddRefed<Promise> Fetch(const RequestOrUSVString& aInput, + already_AddRefed<Promise> Fetch(const RequestOrUTF8String& aInput, const RequestInit& aInit, CallerType aCallerType, ErrorResult& aRv); diff --git a/dom/workers/WorkerThread.cpp b/dom/workers/WorkerThread.cpp index 19cf9cb364..14d944e4d3 100644 --- a/dom/workers/WorkerThread.cpp +++ b/dom/workers/WorkerThread.cpp @@ -13,6 +13,7 @@ #include "mozilla/Atomics.h" #include "mozilla/CycleCollectedJSContext.h" #include "mozilla/EventQueue.h" +#include "mozilla/Logging.h" #include "mozilla/MacroForEach.h" #include "mozilla/NotNull.h" #include "mozilla/ThreadEventQueue.h" @@ -25,8 +26,15 @@ #include "nsIRunnable.h" #include "nsIThreadInternal.h" #include "nsString.h" +#include "nsThreadUtils.h" #include "prthread.h" +static mozilla::LazyLogModule gWorkerThread("WorkerThread"); +#ifdef LOGV +# undef LOGV +#endif +#define LOGV(msg) MOZ_LOG(gWorkerThread, LogLevel::Verbose, msg); + namespace mozilla { using namespace ipc; @@ -143,7 +151,11 @@ void WorkerThread::SetWorker(const WorkerThreadFriendKey& /* aKey */, while (mOtherThreadsDispatchingViaEventTarget) { mWorkerPrivateCondVar.Wait(); } - + // Need to clean up the dispatched runnables if + // mOtherThreadsDispatchingViaEventTarget was non-zero. + if (NS_HasPendingEvents(nullptr)) { + NS_ProcessPendingEvents(nullptr); + } #ifdef DEBUG mAcceptingNonWorkerRunnables = true; #endif @@ -223,6 +235,8 @@ WorkerThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable, // May be called on any thread! nsCOMPtr<nsIRunnable> runnable(aRunnable); // in case we exit early + LOGV(("WorkerThread::Dispatch [%p] runnable: %p", this, runnable.get())); + // Workers only support asynchronous dispatch. if (NS_WARN_IF(aFlags != NS_DISPATCH_NORMAL)) { return NS_ERROR_UNEXPECTED; @@ -282,6 +296,8 @@ WorkerThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable, } if (NS_WARN_IF(NS_FAILED(rv))) { + LOGV(("WorkerThread::Dispatch [%p] failed, runnable: %p", this, + runnable.get())); return rv; } diff --git a/dom/workers/loader/CacheLoadHandler.cpp b/dom/workers/loader/CacheLoadHandler.cpp index 16f992e837..7059249602 100644 --- a/dom/workers/loader/CacheLoadHandler.cpp +++ b/dom/workers/loader/CacheLoadHandler.cpp @@ -305,18 +305,15 @@ void CacheLoadHandler::Load(Cache* aCache) { return; } - nsAutoCString spec; - rv = uri->GetSpec(spec); + MOZ_ASSERT(loadContext->mFullURL.IsEmpty()); + rv = uri->GetSpec(loadContext->mFullURL); if (NS_WARN_IF(NS_FAILED(rv))) { Fail(rv); return; } - MOZ_ASSERT(loadContext->mFullURL.IsEmpty()); - CopyUTF8toUTF16(spec, loadContext->mFullURL); - - mozilla::dom::RequestOrUSVString request; - request.SetAsUSVString().ShareOrDependUpon(loadContext->mFullURL); + mozilla::dom::RequestOrUTF8String request; + request.SetAsUTF8String().ShareOrDependUpon(loadContext->mFullURL); mozilla::dom::CacheQueryOptions params; diff --git a/dom/workers/loader/NetworkLoadHandler.cpp b/dom/workers/loader/NetworkLoadHandler.cpp index 9c2c243066..07398dac0a 100644 --- a/dom/workers/loader/NetworkLoadHandler.cpp +++ b/dom/workers/loader/NetworkLoadHandler.cpp @@ -358,10 +358,10 @@ nsresult NetworkLoadHandler::PrepareForRequest(nsIRequest* aRequest) { RefPtr<mozilla::dom::Response> response = new mozilla::dom::Response( mRequestHandle->GetCacheCreator()->Global(), std::move(ir), nullptr); - mozilla::dom::RequestOrUSVString request; + mozilla::dom::RequestOrUTF8String request; MOZ_ASSERT(!loadContext->mFullURL.IsEmpty()); - request.SetAsUSVString().ShareOrDependUpon(loadContext->mFullURL); + request.SetAsUTF8String().ShareOrDependUpon(loadContext->mFullURL); // This JSContext will not end up executing JS code because here there are // no ReadableStreams involved. diff --git a/dom/workers/loader/WorkerLoadContext.h b/dom/workers/loader/WorkerLoadContext.h index 97362f2871..f5b107a610 100644 --- a/dom/workers/loader/WorkerLoadContext.h +++ b/dom/workers/loader/WorkerLoadContext.h @@ -130,7 +130,7 @@ class WorkerLoadContext : public JS::loader::LoadContextBase { /* TODO: Split out a ServiceWorkerLoadContext */ // This full URL string is populated only if this object is used in a // ServiceWorker. - nsString mFullURL; + nsCString mFullURL; // This promise is set only when the script is for a ServiceWorker but // it's not in the cache yet. The promise is resolved when the full body is diff --git a/dom/workers/loader/WorkerModuleLoader.cpp b/dom/workers/loader/WorkerModuleLoader.cpp index da340c89bd..d76c61c31f 100644 --- a/dom/workers/loader/WorkerModuleLoader.cpp +++ b/dom/workers/loader/WorkerModuleLoader.cpp @@ -147,6 +147,15 @@ already_AddRefed<ModuleLoadRequest> WorkerModuleLoader::CreateDynamicImport( return request.forget(); } +bool WorkerModuleLoader::IsDynamicImportSupported() { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + if (workerPrivate->IsServiceWorker()) { + return false; + } + + return true; +} + bool WorkerModuleLoader::CanStartLoad(ModuleLoadRequest* aRequest, nsresult* aRvOut) { return true; diff --git a/dom/workers/loader/WorkerModuleLoader.h b/dom/workers/loader/WorkerModuleLoader.h index 6ad45b42a0..f49d08743c 100644 --- a/dom/workers/loader/WorkerModuleLoader.h +++ b/dom/workers/loader/WorkerModuleLoader.h @@ -63,6 +63,8 @@ class WorkerModuleLoader : public JS::loader::ModuleLoaderBase { JS::Handle<JSString*> aSpecifier, JS::Handle<JSObject*> aPromise) override; + bool IsDynamicImportSupported() override; + bool CanStartLoad(ModuleLoadRequest* aRequest, nsresult* aRvOut) override; // StartFetch is special for worker modules, as we need to move back to the diff --git a/dom/workers/nsIWorkerDebugger.idl b/dom/workers/nsIWorkerDebugger.idl index 931d01a4ac..573e7027b0 100644 --- a/dom/workers/nsIWorkerDebugger.idl +++ b/dom/workers/nsIWorkerDebugger.idl @@ -25,11 +25,11 @@ interface nsIWorkerDebugger : nsISupports const unsigned long TYPE_SHARED = 1; const unsigned long TYPE_SERVICE = 2; - readonly attribute bool isClosed; + readonly attribute boolean isClosed; - readonly attribute bool isChrome; + readonly attribute boolean isChrome; - readonly attribute bool isInitialized; + readonly attribute boolean isInitialized; readonly attribute nsIWorkerDebugger parent; diff --git a/dom/workers/remoteworkers/RemoteWorkerChild.cpp b/dom/workers/remoteworkers/RemoteWorkerChild.cpp index feb294f3fc..bf63c1729c 100644 --- a/dom/workers/remoteworkers/RemoteWorkerChild.cpp +++ b/dom/workers/remoteworkers/RemoteWorkerChild.cpp @@ -286,7 +286,7 @@ nsresult RemoteWorkerChild::ExecWorkerOnMainThread(RemoteWorkerData&& aData) { info.mStorageAccess = aData.storageAccess(); info.mUseRegularPrincipal = aData.useRegularPrincipal(); info.mUsingStorageAccess = aData.usingStorageAccess(); - info.mIsThirdPartyContextToTopWindow = aData.isThirdPartyContextToTopWindow(); + info.mIsThirdPartyContext = aData.isThirdPartyContext(); info.mOriginAttributes = BasePrincipal::Cast(principal)->OriginAttributesRef(); info.mShouldResistFingerprinting = aData.shouldResistFingerprinting(); diff --git a/dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh b/dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh index 8894450b72..08fb5700fe 100644 --- a/dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh +++ b/dom/workers/remoteworkers/RemoteWorkerTypes.ipdlh @@ -76,7 +76,7 @@ struct RemoteWorkerData StorageAccess storageAccess; - bool isThirdPartyContextToTopWindow; + bool isThirdPartyContext; bool shouldResistFingerprinting; diff --git a/dom/workers/test/call_throws.js b/dom/workers/test/call_throws.js new file mode 100644 index 0000000000..dfde155961 --- /dev/null +++ b/dom/workers/test/call_throws.js @@ -0,0 +1,4 @@ +function workerMethod() { + console.log("workerMethod about to throw..."); + throw new Error("Method-Throw-Payload"); +} diff --git a/dom/workers/test/chrome.toml b/dom/workers/test/chrome.toml index 0b2d68da39..239394d2ea 100644 --- a/dom/workers/test/chrome.toml +++ b/dom/workers/test/chrome.toml @@ -89,10 +89,6 @@ support-files = [ ["test_chromeWorkerJSM.xhtml"] ["test_file.xhtml"] -skip-if = [ - "os == 'linux' && bits == 64 && debug", # Bug 1765445 - "apple_catalina && !debug", # Bug 1765445 -] ["test_fileBlobPosting.xhtml"] diff --git a/dom/workers/test/crashtests/1858809.html b/dom/workers/test/crashtests/1858809.html new file mode 100644 index 0000000000..3a5190c300 --- /dev/null +++ b/dom/workers/test/crashtests/1858809.html @@ -0,0 +1,14 @@ +<!DOCTYPE> +<html> +<head> +<meta charset="UTF-8"> +<script> +document.addEventListener("DOMContentLoaded", () => { + const a = new Worker("", {}) + const b = new Blob([""], {}) + a.terminate() + new RTCRtpScriptTransform(a, b, [{}]) +}) +</script> +</head> +</html> diff --git a/dom/workers/test/crashtests/crashtests.list b/dom/workers/test/crashtests/crashtests.list index 528f4c8a10..26a1fbbf80 100644 --- a/dom/workers/test/crashtests/crashtests.list +++ b/dom/workers/test/crashtests/crashtests.list @@ -6,3 +6,4 @@ load 1228456.html load 1348882.html load 1821066.html load 1819146.html +load 1858809.html diff --git a/dom/workers/test/importScripts_3rdParty_worker.js b/dom/workers/test/importScripts_3rdParty_worker.js index 326d48f77a..e55fdc514b 100644 --- a/dom/workers/test/importScripts_3rdParty_worker.js +++ b/dom/workers/test/importScripts_3rdParty_worker.js @@ -1,18 +1,113 @@ const workerURL = "http://mochi.test:8888/tests/dom/workers/test/importScripts_3rdParty_worker.js"; +/** + * An Error can be a JS Error or a DOMException. The primary difference is that + * JS Errors have a SpiderMonkey specific `fileName` for the filename and + * DOMEXCEPTION uses `filename`. + */ +function normalizeError(err) { + if (!err) { + return null; + } + + const isDOMException = "filename" in err; + + return { + message: err.message, + name: err.name, + isDOMException, + code: err.code, + // normalize to fileName + fileName: isDOMException ? err.filename : err.fileName, + hasFileName: !!err.fileName, + hasFilename: !!err.filename, + lineNumber: err.lineNumber, + columnNumber: err.columnNumber, + stack: err.stack, + stringified: err.toString(), + }; +} + +function normalizeErrorEvent(event) { + if (!event) { + return null; + } + + return { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + error: normalizeError(event.error), + stringified: event.toString(), + }; +} + +/** + * Normalize the `OnErrorEventHandlerNonNull onerror` invocation. The + * special handling in JSEventHandler::HandleEvent ends up spreading out the + * contents of the ErrorEvent into discrete arguments. The one thing lost is + * we can't toString the ScriptEvent itself, but this should be the same as the + * message anyways. + * + * The spec for the invocation is the "special error event handling" logic + * described in step 4 at: + * https://html.spec.whatwg.org/multipage/webappapis.html#the-event-handler-processing-algorithm + * noting that the step somewhat glosses over that it's only "onerror" that is + * OnErrorEventHandlerNonNull and capable of processing 5 arguments and that's + * why an addEventListener "error" listener doesn't get this handling. + * + * Argument names here are made to match the call-site in JSEventHandler. + */ +function normalizeOnError( + msgOrEvent, + fileName, + lineNumber, + columnNumber, + error +) { + return { + message: msgOrEvent, + filename: fileName, + lineno: lineNumber, + colno: columnNumber, + error: normalizeError(error), + stringified: null, + }; +} + +/** + * Helper to postMessage the provided data after a setTimeout(0) so that any + * error event currently being dispatched that will bubble to our parent will + * be delivered before our postMessage. + */ +function delayedPostMessage(data) { + setTimeout(() => { + postMessage(data); + }, 0); +} + onmessage = function (a) { + const args = a.data; + // Messages are either nested (forward to a nested worker) or should be + // processed locally. if (a.data.nested) { - var worker = new Worker(workerURL); + const worker = new Worker(workerURL); + let firstErrorEvent; + + // When the test mode is "catch" + worker.onmessage = function (event) { - postMessage(event.data); + delayedPostMessage({ + nestedMessage: event.data, + errorEvent: firstErrorEvent, + }); }; worker.onerror = function (event) { + firstErrorEvent = normalizeErrorEvent(event); event.preventDefault(); - postMessage({ - error: event instanceof ErrorEvent && event.filename == workerURL, - }); }; a.data.nested = false; @@ -20,69 +115,47 @@ onmessage = function (a) { return; } - // This first URL will use the same origin of this script. - var sameOriginURL = new URL(a.data.url); - var fileName1 = 42; - - // This is cross-origin URL. - var crossOriginURL = new URL(a.data.url); - crossOriginURL.host = "example.com"; - crossOriginURL.port = 80; - var fileName2 = 42; - - if (a.data.test == "none") { - importScripts(crossOriginURL.href); - return; - } - - try { - importScripts(sameOriginURL.href); - } catch (e) { - if (!(e instanceof SyntaxError)) { - postMessage({ result: false }); - return; - } - - fileName1 = e.fileName; - } - - if (fileName1 != sameOriginURL.href || !fileName1) { - postMessage({ result: false }); - return; - } - - if (a.data.test == "try") { - var exception; + // Local test. + if (a.data.mode === "catch") { try { - importScripts(crossOriginURL.href); - } catch (e) { - fileName2 = e.filename; - exception = e; + importScripts(a.data.url); + workerMethod(); + } catch (ex) { + delayedPostMessage({ + args, + error: normalizeError(ex), + }); } - - postMessage({ - result: - fileName2 == workerURL && - exception.name == "NetworkError" && - exception.code == DOMException.NETWORK_ERR, + } else if (a.data.mode === "uncaught") { + const onerrorPromise = new Promise(resolve => { + self.onerror = (...onerrorArgs) => { + resolve(normalizeOnError(...onerrorArgs)); + }; }); - return; - } - - if (a.data.test == "eventListener") { - addEventListener("error", function (event) { - event.preventDefault(); - postMessage({ - result: event instanceof ErrorEvent && event.filename == workerURL, + const listenerPromise = new Promise(resolve => { + self.addEventListener("error", evt => { + resolve(normalizeErrorEvent(evt)); }); }); - } - if (a.data.test == "onerror") { - onerror = function (...args) { - postMessage({ result: args[1] == workerURL }); - }; - } + Promise.all([onerrorPromise, listenerPromise]).then( + ([onerrorEvent, listenerEvent]) => { + delayedPostMessage({ + args, + onerrorEvent, + listenerEvent, + }); + } + ); - importScripts(crossOriginURL.href); + importScripts(a.data.url); + workerMethod(); + // we will have thrown by this point, which will trigger an "error" event + // on our global and then will propagate to our parent (which could be a + // window or a worker, if nested). + // + // To avoid hangs, throw a different error here that will fail equivalence + // tests. + throw new Error("We expected an error and this is a failsafe for hangs."); + } }; diff --git a/dom/workers/test/mime_type_is_csv.js b/dom/workers/test/mime_type_is_csv.js new file mode 100644 index 0000000000..54d3b70689 --- /dev/null +++ b/dom/workers/test/mime_type_is_csv.js @@ -0,0 +1 @@ +throw new Error("This file has a CSV mime type and should not load."); diff --git a/dom/workers/test/mime_type_is_csv.js^headers^ b/dom/workers/test/mime_type_is_csv.js^headers^ new file mode 100644 index 0000000000..0d848b02c2 --- /dev/null +++ b/dom/workers/test/mime_type_is_csv.js^headers^ @@ -0,0 +1 @@ +Content-Type: text/csv diff --git a/dom/workers/test/mochitest.toml b/dom/workers/test/mochitest.toml index 5ae8094b58..a32dbf3bf6 100644 --- a/dom/workers/test/mochitest.toml +++ b/dom/workers/test/mochitest.toml @@ -12,6 +12,7 @@ support-files = [ "bug998474_worker.js", "bug1063538_worker.js", "bug1063538.sjs", + "call_throws.js", "clearTimeouts_worker.js", "clearTimeoutsImplicit_worker.js", "content_worker.js", @@ -42,6 +43,8 @@ support-files = [ "loadEncoding_worker.js", "location_worker.js", "longThread_worker.js", + "mime_type_is_csv.js", + "mime_type_is_csv.js^headers^", "multi_sharedWorker_frame.html", "multi_sharedWorker_sharedWorker.js", "navigator_languages_worker.js", @@ -58,6 +61,7 @@ support-files = [ "recursion_worker.js", "recursiveOnerror_worker.js", "redirect_to_foreign.sjs", + "redirect_with_query_args.sjs", "rvals_worker.js", "sharedWorker_sharedWorker.js", "simpleThread_worker.js", @@ -66,6 +70,7 @@ support-files = [ "terminate_worker.js", "test_csp.html^headers^", "test_csp.js", + "toplevel_throws.js", "referrer_worker.html", "sourcemap_header_iframe.html", "sourcemap_header_worker.js", diff --git a/dom/workers/test/redirect_with_query_args.sjs b/dom/workers/test/redirect_with_query_args.sjs new file mode 100644 index 0000000000..3359367ee0 --- /dev/null +++ b/dom/workers/test/redirect_with_query_args.sjs @@ -0,0 +1,22 @@ +/** + * This file expects a query string that's the upper-cased version of a file to + * be redirected to in the same directory. The redirect will also include + * added "secret data" as a query string. + * + * So if the request is `/path/redirect_with_query_args.sjs?FOO.JS` the redirect + * will be to `/path/foo.js?SECRET_DATA`. + **/ + +function handleRequest(request, response) { + // The secret data to include in the redirect to make the redirect URL + // easily detectable. + const secretData = "SECRET_DATA"; + + let pathBase = request.path.split("/").slice(0, -1).join("/"); + let targetFile = request.queryString.toLowerCase(); + let newUrl = `${pathBase}/${targetFile}?${secretData}`; + + response.setStatusLine(request.httpVersion, 302, "Found"); + response.setHeader("Cache-Control", "no-cache"); + response.setHeader("Location", newUrl, false); +} diff --git a/dom/workers/test/test_importScripts_3rdparty.html b/dom/workers/test/test_importScripts_3rdparty.html index 7f10f23faf..b1e4913eea 100644 --- a/dom/workers/test/test_importScripts_3rdparty.html +++ b/dom/workers/test/test_importScripts_3rdparty.html @@ -14,123 +14,576 @@ const workerURL = 'http://mochi.test:8888/tests/dom/workers/test/importScripts_3rdParty_worker.js'; -const sameOriginURL = 'http://mochi.test:8888/tests/dom/workers/test/invalid.js' +const sameOriginBaseURL = 'http://mochi.test:8888/tests/dom/workers/test'; +const crossOriginBaseURL = "https://example.com/tests/dom/workers/test"; -var tests = [ - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data!"); - next(); - }; +const workerRelativeUrl = 'importScripts_3rdParty_worker.js'; +const workerAbsoluteUrl = `${sameOriginBaseURL}/${workerRelativeUrl}` - worker.postMessage({ url: sameOriginURL, test: 'try', nested: false }); - }, +/** + * This file tests cross-origin error muting in importScripts for workers. In + * particular, we want to test: + * - The errors thrown by the parsing phase of importScripts(). + * - The errors thrown by the top-level evaluation phase of importScripts(). + * - If the error is reported to the parent's Worker binding, including through + * nested workers, as well as the contents of the error. + * - For errors: + * - What type of exception is reported? + * - What fileName is reported on the exception? + * - What are the contents of the stack on the exception? + * + * Relevant specs: + * - https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-classic-worker-imported-script + * - https://html.spec.whatwg.org/multipage/webappapis.html#creating-a-classic-script + * + * The situation and motivation for error muting is: + * - JS scripts are allowed to be loaded cross-origin without CORS for legacy + * reasons. If a script is cross-origin, its "muted errors" is set to true. + * - The fetch will set the "use-URL-credentials" flag + * https://fetch.spec.whatwg.org/#concept-request-use-url-credentials-flag + * but will have the default "credentials" mode of "omit" + * https://fetch.spec.whatwg.org/#concept-request-credentials-mode which + * means that username/password will be propagated. + * - For legacy reasons, JS scripts aren't required to have an explicit JS MIME + * type which allows attacks that attempt to load a known-non JS file as JS + * in order to derive information from the errors or from side-effects to the + * global for code that does parse and evaluate as legal JS. + **/ - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data in nested workers!"); - next(); - }; - worker.postMessage({ url: sameOriginURL, test: 'try', nested: true }); + /** + * - `sameOrigin`: Describes the exception we expect to see for a same-origin + * import. + * - `crossOrigin`: Describes the exception we expect to see for a cross-origin + * import (from example.com while the worker is the mochitest origin). + * + * The exception fields are: + * - `exceptionName`: The `name` of the Error object. + * - `thrownFile`: Describes the filename we expect to see on the error: + * - `importing-worker-script`: The worker script that's doing the importing + * will be the source of the exception, not the imported script. + * - `imported-script-no-redirect`: The (absolute-ified) script as passed to + * importScript(s), regardless of any redirects that occur. + * - `post-redirect-imported-script`: The name of the actual URL that was + * loaded following any redirects. + */ +const scriptPermutations = [ + { + name: 'Invalid script that generates a syntax error', + script: 'invalid.js', + sameOrigin: { + exceptionName: 'SyntaxError', + thrownFile: 'post-redirect-imported-script', + isDOMException: false, + message: "expected expression, got end of script" + }, + crossOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + } }, + { + name: 'Non-JS MIME Type', + script: 'mime_type_is_csv.js', + sameOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + }, + crossOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + } + }, + { + // What happens if the script is a 404? + name: 'Nonexistent script', + script: 'script_does_not_exist.js', + sameOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + }, + crossOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + } + }, + { + name: 'Script that throws during toplevel execution', + script: 'toplevel_throws.js', + sameOrigin: { + exceptionName: 'Error', + thrownFile: 'post-redirect-imported-script', + isDOMException: false, + message: "Toplevel-Throw-Payload", + }, + crossOrigin: { + exceptionName: 'NetworkError', + thrownFile: 'importing-worker-script', + isDOMException: true, + code: DOMException.NETWORK_ERR, + message: "A network error occurred." + } + }, + { + name: 'Script that exposes a method that throws', + script: 'call_throws.js', + sameOrigin: { + exceptionName: 'Error', + thrownFile: 'post-redirect-imported-script', + isDOMException: false, + message: "Method-Throw-Payload" + }, + crossOrigin: { + exceptionName: 'Error', + thrownFile: 'imported-script-no-redirect', + isDOMException: false, + message: "Method-Throw-Payload" + } + }, +]; - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data via eventListener!"); - next(); - }; - - worker.postMessage({ url: sameOriginURL, test: 'eventListener', nested: false }); +/** + * Special fields: + * - `transformScriptImport`: A function that takes the script name as input and + * produces the actual path to use for import purposes, allowing the addition + * of a redirect. + * - `expectedURLAfterRedirect`: A function that takes the script name as + * input and produces the expected script name post-redirect (if there is a + * redirect). In particular, our `redirect_with_query_args.sjs` helper will + * perform a same-origin redirect and append "?SECRET_DATA" onto the end of + * the redirected URL at this time. + * - `partOfTheURLToNotExposeToJS`: A string snippet that is present in the + * post-redirect contents that should absolutely not show up in the error's + * stack if the redirect isn't exposed. This is a secondary check to the + * result of expectedURLAfterRedirect. + */ +const urlPermutations = [ + { + name: 'No Redirect', + transformScriptImport: x => x, + expectedURLAfterRedirect: x => x, + // No redirect means nothing to be paranoid about. + partOfTheURLToNotExposeToJS: null, }, + { + name: 'Same-Origin Redirect With Query Args', + // We mangle the script into uppercase and the redirector undoes this in + // order to minimize the similarity of the pre-redirect and post-redirect + // strings. + transformScriptImport: x => `redirect_with_query_args.sjs?${x.toUpperCase()}`, + expectedURLAfterRedirect: x => `${x}?SECRET_DATA`, + // The redirect will add this when it formulates the redirected URL, and the + // test wants to make sure this doesn't show up in filenames or stacks + // unless the thrownFile is set to 'post-redirect-imported-script'. + partOfTheURLToNotExposeToJS: 'SECRET_DATA', + } +]; +const nestedPermutations = [ + { + name: 'Window Parent', + nested: false, + }, + { + name: 'Worker Parent', + nested: true, + } +]; - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data in nested workers via eventListener!"); - next(); - }; + // NOTE: These implementations are copied from importScripts_3rdParty_worker.js + // for reasons of minimizing the number of calls to importScripts for + // debugging. + function normalizeError(err) { + if (!err) { + return null; + } - worker.postMessage({ url: sameOriginURL, test: 'eventListener', nested: true }); - }, + const isDOMException = "filename" in err; - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onmessage = function(event) { - ok("result" in event.data && event.data.result, "It seems we don't share data via onerror!"); - next(); - }; - worker.onerror = function(event) { - event.preventDefault(); + return { + message: err.message, + name: err.name, + isDOMException, + code: err.code, + // normalize to fileName + fileName: isDOMException ? err.filename : err.fileName, + hasFileName: !!err.fileName, + hasFilename: !!err.filename, + lineNumber: err.lineNumber, + columnNumber: err.columnNumber, + stack: err.stack, + stringified: err.toString(), + }; +} + +function normalizeErrorEvent(event) { + if (!event) { + return null; + } + + return { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + error: normalizeError(event.error), + stringified: event.toString(), + }; +} +// End duplicated code. + + +/** + * Validate the received error against our expectations and provided context. + * + * For `expectation`, see the `scriptPermutations` doc-block which documents + * its `sameOrigin` and `crossOrigin` properties which are what we expect here. + * + * The `context` should include: + * - `workerUrl`: The absolute URL of the toplevel worker script that the worker + * is running which is the code that calls `importScripts`. + * - `importUrl`: The absolute URL provided to the call to `importScripts`. + * This is the pre-redirect URL if a redirect is involved. + * - `postRedirectUrl`: The same as `importUrl` unless a redirect is involved, + * in which case this will be a different URL. + * - `isRedirected`: Boolean indicating whether a redirect was involved. This + * is a convenience variable that's derived from the above 2 URL's for now. + * - `shouldNotInclude`: Provided by the URL permutation, this is used to check + * that post-redirect data does not creep into the exception unless the + * expected `thrownFile` is `post-redirect-imported-script`. + */ +function checkError(label, expectation, context, err) { + info(`## Checking error: ${JSON.stringify(err)}`); + is(err.name, expectation.exceptionName, + `${label}: Error name matches "${expectation.exceptionName}"?`); + is(err.isDOMException, expectation.isDOMException, + `${label}: Is a DOM Exception == ${expectation.isDOMException}?`); + if (expectation.code) { + is(err.code, expectation.code, + `${label}: Code matches ${expectation.code}?`); + } + + let expectedFile; + switch (expectation.thrownFile) { + case 'importing-worker-script': + expectedFile = context.workerUrl; + break; + case 'imported-script-no-redirect': + expectedFile = context.importUrl; + break; + case 'post-redirect-imported-script': + expectedFile = context.postRedirectUrl; + break; + default: + ok(false, `Unexpected thrownFile parameter: ${expectation.thrownFile}`); + return; + } + + is(err.fileName, expectedFile, + `${label}: Filename from ${expectation.thrownFile} is ${expectedFile}`); + + + let expMessage = expectation.message; + if (typeof(expMessage) === "function") { + expMessage = expectation.message(context); + } + is(err.message, expMessage, + `${label}: Message is ${expMessage}`); + + // If this is a redirect and we expect the error to not be surfacing any + // post-redirect information and there's a `shouldNotInclude` string, then + // check to make sure it's not present. + if (context.isRedirected && context.shouldNotInclude) { + if (expectation.thrownFile !== 'post-redirect-imported-script') { + ok(!err.stack.includes(context.shouldNotInclude), + `${label}: Stack should not include ${context.shouldNotInclude}:\n${err.stack}`); + ok(!err.stringified.includes(context.shouldNotInclude), + `${label}: Stringified error should not include ${context.shouldNotInclude}:\n${err.stringified}`); + } else if (expectation.exceptionName !== 'SyntaxError') { + // We do expect the shouldNotInclude to be present for + // 'post-redirect-imported-script' as long as the exception isn't a + // SyntaxError. SyntaxError stacks inherently do not include the filename + // of the file with the syntax problem as a stack frame. + ok(err.stack.includes(context.shouldNotInclude), + `${label}: Stack should include ${context.shouldNotInclude}:\n${err.stack}`); } + } + let expStringified = `${err.name}: ${expMessage}`; + is(err.stringified, expStringified, + `${label}: Stringified error should be: ${expStringified}`); - worker.postMessage({ url: sameOriginURL, test: 'onerror', nested: false }); - }, + // Add some whitespace in our output. + info(""); +} - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onerror = function(event) { - event.preventDefault(); - ok(event instanceof ErrorEvent, "ErrorEvent received."); - is(event.filename, workerURL, "ErrorEvent.filename is correct"); - next(); - }; +function checkErrorEvent(label, expectation, context, event, viaTask=false) { + info(`## Checking error event: ${JSON.stringify(event)}`); - worker.postMessage({ url: sameOriginURL, test: 'none', nested: false }); - }, + let expectedFile; + switch (expectation.thrownFile) { + case 'importing-worker-script': + expectedFile = context.workerUrl; + break; + case 'imported-script-no-redirect': + expectedFile = context.importUrl; + break; + case 'post-redirect-imported-script': + expectedFile = context.postRedirectUrl; + break; + default: + ok(false, `Unexpected thrownFile parameter: ${expectation.thrownFile}`); + return; + } - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.addEventListener("error", function(event) { - event.preventDefault(); - ok(event instanceof ErrorEvent, "ErrorEvent received."); - is(event.filename, workerURL, "ErrorEvent.filename is correct"); - next(); - }); + is(event.filename, expectedFile, + `${label}: Filename from ${expectation.thrownFile} is ${expectedFile}`); - worker.postMessage({ url: sameOriginURL, test: 'none', nested: false }); - }, + let expMessage = expectation.message; + if (typeof(expMessage) === "function") { + expMessage = expectation.message(context); + } + // The error event message prepends the exception name to the Error's message. + expMessage = `${expectation.exceptionName}: ${expMessage}`; - function() { - var worker = new Worker("importScripts_3rdParty_worker.js"); - worker.onerror = function(event) { - ok(false, "No error should be received!"); - }; + is(event.message, expMessage, + `${label}: Message is ${expMessage}`); + + // If this is a redirect and we expect the error to not be surfacing any + // post-redirect information and there's a `shouldNotInclude` string, then + // check to make sure it's not present. + // + // Note that `stringified` may not be present for the "onerror" case. + if (context.isRedirected && + expectation.thrownFile !== 'post-redirect-imported-script' && + context.shouldNotInclude && + event.stringified) { + ok(!event.stringified.includes(context.shouldNotInclude), + `${label}: Stringified error should not include ${context.shouldNotInclude}:\n${event.stringified}`); + } + if (event.stringified) { + is(event.stringified, "[object ErrorEvent]", + `${label}: Stringified event should be "[object ErrorEvent]"`); + } + + // If we received the error via a task queued because it was not handled in + // the worker, then per + // https://html.spec.whatwg.org/multipage/workers.html#runtime-script-errors-2 + // the error will be null. + if (viaTask) { + is(event.error, null, + `${label}: Error is null because it came from an HTML 10.2.5 task.`); + } else { + checkError(label, expectation, context, event.error); + } +} + +/** + * Helper to spawn a worker, postMessage it the given args, and return the + * worker's response payload and the first "error" received on the Worker + * binding by the time the message handler resolves. The worker logic makes + * sure to delay its postMessage using setTimeout(0) so error events will always + * arrive before any message that is sent. + * + * If args includes a truthy `nested` value, then the `message` and + * `bindingErrorEvent` are as perceived by the parent worker. + */ +function asyncWorkerImport(args) { + const worker = new Worker(workerRelativeUrl); + const promise = new Promise((resolve, reject) => { + // The first "error" received on the Worker binding. + let firstErrorEvent = null; worker.onmessage = function(event) { - ok("error" in event.data && event.data.error, "The error has been fully received from a nested worker"); - next(); - }; - worker.postMessage({ url: sameOriginURL, test: 'none', nested: true }); - }, + let message = event.data; + // For the nested case, unwrap and normalize things. + if (args.nested) { + firstErrorEvent = message.errorEvent; + message = message.nestedMessage; + // We need to re-set the argument to be nested because it was set to + // false so that only a single level of nesting occurred. + message.args.nested = true; + } + + // Make sure the args we receive from the worker are the same as the ones + // we sent. + is(JSON.stringify(message.args), JSON.stringify(args), + "Worker re-transmitted args match sent args."); - function() { - var url = URL.createObjectURL(new Blob(["%&%^&%^"])); - var worker = new Worker(url); + resolve({ + message, + bindingErrorEvent: firstErrorEvent + }); + worker.terminate(); + }; worker.onerror = function(event) { + // We don't want this to bubble to the window and cause a test failure. event.preventDefault(); - ok(event instanceof Event, "Event received."); - next(); - }; - } -]; -function next() { - if (!tests.length) { - SimpleTest.finish(); - return; - } + if (firstErrorEvent) { + ok(false, "Worker binding received more than one error"); + reject(new Error("multiple error events received")); + return; + } + firstErrorEvent = normalizeErrorEvent(event); + } + }); + info("Sending args to worker: " + JSON.stringify(args)); + worker.postMessage(args); - var test = tests.shift(); - test(); + return promise; } -SimpleTest.waitForExplicitFinish(); -next(); +function makeTestPermutations() { + for (const urlPerm of urlPermutations) { + for (const scriptPerm of scriptPermutations) { + for (const nestedPerm of nestedPermutations) { + const testName = + `${nestedPerm.name}: ${urlPerm.name}: ${scriptPerm.name}`; + const caseFunc = async () => { + // Make the test name much more obvious when viewing logs. + info(`#############################################################`); + info(`### ${testName}`); + let result, errorEvent; + + const scriptName = urlPerm.transformScriptImport(scriptPerm.script); + const redirectedUrl = urlPerm.expectedURLAfterRedirect(scriptPerm.script); + + // ### Same-Origin Import + // ## What does the error look like when caught? + ({ message, bindingErrorEvent } = await asyncWorkerImport( + { + url: `${sameOriginBaseURL}/${scriptName}`, + mode: "catch", + nested: nestedPerm.nested, + })); + + const sameOriginContext = { + workerUrl: workerAbsoluteUrl, + importUrl: message.args.url, + postRedirectUrl: `${sameOriginBaseURL}/${redirectedUrl}`, + isRedirected: message.args.url !== redirectedUrl, + shouldNotInclude: urlPerm.partOfTheURLToNotExposeToJS, + }; + checkError( + `${testName}: Same-Origin Thrown`, + scriptPerm.sameOrigin, + sameOriginContext, + message.error); + + // ## What does the error events look like when not caught? + ({ message, bindingErrorEvent } = await asyncWorkerImport( + { + url: `${sameOriginBaseURL}/${scriptName}`, + mode: "uncaught", + nested: nestedPerm.nested, + })); + + // The worker will have captured the error event twice, once via + // onerror and once via an "error" event listener. It will have not + // invoked preventDefault(), so the worker's parent will also have + // received a copy of the error event as well. + checkErrorEvent( + `${testName}: Same-Origin Worker global onerror handler`, + scriptPerm.sameOrigin, + sameOriginContext, + message.onerrorEvent); + checkErrorEvent( + `${testName}: Same-Origin Worker global error listener`, + scriptPerm.sameOrigin, + sameOriginContext, + message.listenerEvent); + // Binding events + checkErrorEvent( + `${testName}: Same-Origin Parent binding onerror`, + scriptPerm.sameOrigin, + sameOriginContext, + bindingErrorEvent, "via-task"); + + // ### Cross-Origin Import + // ## What does the error look like when caught? + ({ message, bindingErrorEvent } = await asyncWorkerImport( + { + url: `${crossOriginBaseURL}/${scriptName}`, + mode: "catch", + nested: nestedPerm.nested, + })); + + const crossOriginContext = { + workerUrl: workerAbsoluteUrl, + importUrl: message.args.url, + postRedirectUrl: `${crossOriginBaseURL}/${redirectedUrl}`, + isRedirected: message.args.url !== redirectedUrl, + shouldNotInclude: urlPerm.partOfTheURLToNotExposeToJS, + }; + + checkError( + `${testName}: Cross-Origin Thrown`, + scriptPerm.crossOrigin, + crossOriginContext, + message.error); + + // ## What does the error events look like when not caught? + ({ message, bindingErrorEvent } = await asyncWorkerImport( + { + url: `${crossOriginBaseURL}/${scriptName}`, + mode: "uncaught", + nested: nestedPerm.nested, + })); + + // The worker will have captured the error event twice, once via + // onerror and once via an "error" event listener. It will have not + // invoked preventDefault(), so the worker's parent will also have + // received a copy of the error event as well. + checkErrorEvent( + `${testName}: Cross-Origin Worker global onerror handler`, + scriptPerm.crossOrigin, + crossOriginContext, + message.onerrorEvent); + checkErrorEvent( + `${testName}: Cross-Origin Worker global error listener`, + scriptPerm.crossOrigin, + crossOriginContext, + message.listenerEvent); + // Binding events + checkErrorEvent( + `${testName}: Cross-Origin Parent binding onerror`, + scriptPerm.crossOrigin, + crossOriginContext, + bindingErrorEvent, "via-task"); + }; + + // The mochitest framework uses the name of the caseFunc, which by default + // will be inferred and set on the configurable `name` property. It's not + // writable though, so we need to clobber the property. Devtools will + // xray through this name but this works for the test framework. + Object.defineProperty( + caseFunc, + 'name', + { + value: testName, + writable: false + }); + add_task(caseFunc); + } + } + } +} +makeTestPermutations(); </script> </body> </html> diff --git a/dom/workers/test/test_worker_interfaces.js b/dom/workers/test/test_worker_interfaces.js index efd108f85c..c53c0b2b0f 100644 --- a/dom/workers/test/test_worker_interfaces.js +++ b/dom/workers/test/test_worker_interfaces.js @@ -138,6 +138,8 @@ let interfaceNamesInGlobalScope = [ // IMPORTANT: Do not change this list without review from a DOM peer! { name: "AudioDecoder", nightly: true }, // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "AudioEncoder", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! { name: "Blob", insecureContext: true }, // IMPORTANT: Do not change this list without review from a DOM peer! { name: "BroadcastChannel", insecureContext: true }, diff --git a/dom/workers/test/toplevel_throws.js b/dom/workers/test/toplevel_throws.js new file mode 100644 index 0000000000..3efe29d5af --- /dev/null +++ b/dom/workers/test/toplevel_throws.js @@ -0,0 +1 @@ +throw new Error("Toplevel-Throw-Payload"); diff --git a/dom/worklet/WorkletFetchHandler.cpp b/dom/worklet/WorkletFetchHandler.cpp index b259d6a357..b0c9ec1cfa 100644 --- a/dom/worklet/WorkletFetchHandler.cpp +++ b/dom/worklet/WorkletFetchHandler.cpp @@ -458,11 +458,8 @@ nsresult WorkletFetchHandler::StartFetch(JSContext* aCx, nsIURI* aURI, return NS_ERROR_FAILURE; } - RequestOrUSVString requestInput; - - nsAutoString url; - CopyUTF8toUTF16(spec, url); - requestInput.SetAsUSVString().ShareOrDependUpon(url); + RequestOrUTF8String requestInput; + requestInput.SetAsUTF8String().ShareOrDependUpon(spec); RootedDictionary<RequestInit> requestInit(aCx); requestInit.mCredentials.Construct(mCredentials); @@ -472,30 +469,33 @@ nsresult WorkletFetchHandler::StartFetch(JSContext* aCx, nsIURI* aURI, requestInit.mMode.Construct(RequestMode::Cors); if (aReferrer) { - nsAutoString referrer; - res = aReferrer->GetSpec(spec); + res = aReferrer->GetSpec(requestInit.mReferrer.Construct()); if (NS_WARN_IF(NS_FAILED(res))) { return NS_ERROR_FAILURE; } - - CopyUTF8toUTF16(spec, referrer); - requestInit.mReferrer.Construct(referrer); } nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWorklet->GetParentObject()); MOZ_ASSERT(global); + // Note: added to infer a default credentials mode in the Request setup, + // but we always pass an explicit credentials value in requestInit, so + // this has no effect right now. Bug 1887862 covers fixing worklets to behave + // the same as "normal" fetch calls. + nsIPrincipal* p = global->PrincipalOrNull(); + CallerType callerType = (p && p->IsSystemPrincipal() ? CallerType::System + : CallerType::NonSystem); IgnoredErrorResult rv; - SafeRefPtr<Request> request = - Request::Constructor(global, aCx, requestInput, requestInit, rv); + SafeRefPtr<Request> request = Request::Constructor( + global, aCx, requestInput, requestInit, callerType, rv); if (rv.Failed()) { return NS_ERROR_FAILURE; } request->OverrideContentPolicyType(mWorklet->Impl()->ContentPolicyType()); - RequestOrUSVString finalRequestInput; + RequestOrUTF8String finalRequestInput; finalRequestInput.SetAsRequest() = request.unsafeGetRawPtr(); RefPtr<Promise> fetchPromise = FetchRequest( diff --git a/dom/xhr/XMLHttpRequest.cpp b/dom/xhr/XMLHttpRequest.cpp index be94267d63..8807c5515a 100644 --- a/dom/xhr/XMLHttpRequest.cpp +++ b/dom/xhr/XMLHttpRequest.cpp @@ -7,7 +7,9 @@ #include "XMLHttpRequest.h" #include "XMLHttpRequestMainThread.h" #include "XMLHttpRequestWorker.h" +#include "mozilla/BasePrincipal.h" #include "mozilla/Logging.h" +#include "mozilla/StaticPrefs_network.h" #include "mozilla/net/CookieJarSettings.h" mozilla::LazyLogModule gXMLHttpRequestLog("XMLHttpRequest"); @@ -21,15 +23,16 @@ already_AddRefed<XMLHttpRequest> XMLHttpRequest::Constructor( if (NS_IsMainThread()) { nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); - nsCOMPtr<nsIScriptObjectPrincipal> principal = + nsCOMPtr<nsIScriptObjectPrincipal> scriptPrincipal = do_QueryInterface(aGlobal.GetAsSupports()); - if (!global || !principal) { + if (!global || !scriptPrincipal) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } nsCOMPtr<nsICookieJarSettings> cookieJarSettings; nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global); + nsCOMPtr<nsIPrincipal> principal = scriptPrincipal->GetPrincipal(); if (window) { Document* document = window->GetExtantDoc(); if (NS_WARN_IF(!document)) { @@ -40,13 +43,21 @@ already_AddRefed<XMLHttpRequest> XMLHttpRequest::Constructor( cookieJarSettings = document->CookieJarSettings(); } else { // We are here because this is a sandbox. - cookieJarSettings = - net::CookieJarSettings::Create(principal->GetPrincipal()); + cookieJarSettings = net::CookieJarSettings::Create(principal); } RefPtr<XMLHttpRequestMainThread> req = new XMLHttpRequestMainThread(global); - req->Construct(principal->GetPrincipal(), cookieJarSettings, false); - req->InitParameters(aParams.mMozAnon, aParams.mMozSystem); + req->Construct(principal, cookieJarSettings, false); + + bool isAnon = false; + if (aParams.mMozAnon.WasPassed()) { + isAnon = aParams.mMozAnon.Value(); + } else { + isAnon = + StaticPrefs::network_fetch_systemDefaultsToOmittingCredentials() && + (aParams.mMozSystem || principal->IsSystemPrincipal()); + } + req->InitParameters(isAnon, aParams.mMozSystem); return req.forget(); } diff --git a/dom/xhr/XMLHttpRequestMainThread.cpp b/dom/xhr/XMLHttpRequestMainThread.cpp index ace26f296f..e18800e24a 100644 --- a/dom/xhr/XMLHttpRequestMainThread.cpp +++ b/dom/xhr/XMLHttpRequestMainThread.cpp @@ -228,12 +228,19 @@ struct DebugWorkerRefs { (((const std::ostringstream&)(std::ostringstream() << stuff)) \ .str()) // NOLINT -# define DEBUG_WORKERREFS \ - DebugWorkerRefs MOZ_UNIQUE_VAR(debugWR__)(*this, __func__) +# if 1 // Disabling because bug 1855699 +# define DEBUG_WORKERREFS void() +# define DEBUG_WORKERREFS1(x) void() +# else -# define DEBUG_WORKERREFS1(x) \ - DebugWorkerRefs MOZ_UNIQUE_VAR(debugWR__)( \ - *this, STREAM_STRING(__func__ << ": " << x)) // NOLINT +# define DEBUG_WORKERREFS \ + DebugWorkerRefs MOZ_UNIQUE_VAR(debugWR__)(*this, __func__) + +# define DEBUG_WORKERREFS1(x) \ + DebugWorkerRefs MOZ_UNIQUE_VAR(debugWR__)( \ + *this, STREAM_STRING(__func__ << ": " << x)) // NOLINT + +# endif #else # define DEBUG_WORKERREFS void() diff --git a/dom/xhr/XMLHttpRequestWorker.cpp b/dom/xhr/XMLHttpRequestWorker.cpp index 7fdfa8fee9..67cba876db 100644 --- a/dom/xhr/XMLHttpRequestWorker.cpp +++ b/dom/xhr/XMLHttpRequestWorker.cpp @@ -1388,10 +1388,12 @@ already_AddRefed<XMLHttpRequest> XMLHttpRequestWorker::Construct( new XMLHttpRequestWorker(workerPrivate, global); if (workerPrivate->XHRParamsAllowed()) { - if (aParams.mMozSystem) + if (aParams.mMozSystem) { xhr->mMozAnon = true; - else - xhr->mMozAnon = aParams.mMozAnon; + } else { + xhr->mMozAnon = + aParams.mMozAnon.WasPassed() ? aParams.mMozAnon.Value() : false; + } xhr->mMozSystem = aParams.mMozSystem; } diff --git a/dom/xml/resources/XMLPrettyPrint.css b/dom/xml/resources/XMLPrettyPrint.css index 845833c46f..4e2088bd91 100644 --- a/dom/xml/resources/XMLPrettyPrint.css +++ b/dom/xml/resources/XMLPrettyPrint.css @@ -56,3 +56,11 @@ font-family: monospace; white-space: pre; } + +.space-default { + white-space: initial; +} + +.space-preserve { + white-space: pre-wrap; +} diff --git a/dom/xml/resources/XMLPrettyPrint.xsl b/dom/xml/resources/XMLPrettyPrint.xsl index b23296cf0d..7b9397cde7 100644 --- a/dom/xml/resources/XMLPrettyPrint.xsl +++ b/dom/xml/resources/XMLPrettyPrint.xsl @@ -33,7 +33,7 @@ </xsl:template> <xsl:template match="*[node()]"> - <div> + <div><xsl:apply-templates mode="space" select="@xml:space"/> <xsl:text><</xsl:text> <span class="start-tag"><xsl:value-of select="name(.)"/></span> <xsl:apply-templates select="@*"/> @@ -48,7 +48,7 @@ </xsl:template> <xsl:template match="*[* or processing-instruction() or comment() or string-length(.) > 50]"> - <div> + <div><xsl:apply-templates mode="space" select="@xml:space"/> <details open="" class="expandable-body"> <summary class="expandable-opening"> <xsl:text><</xsl:text> @@ -68,6 +68,13 @@ </div> </xsl:template> + <xsl:template match="@xml:space[string() = 'default']" mode="space"> + <xsl:attribute name="class">space-default</xsl:attribute> + </xsl:template> + <xsl:template match="@xml:space[string() = 'preserve']" mode="space"> + <xsl:attribute name="class">space-preserve</xsl:attribute> + </xsl:template> + <xsl:template match="@*"> <xsl:text> </xsl:text> <span class="attribute-name"><xsl:value-of select="name(.)"/></span> diff --git a/dom/xul/ChromeObserver.cpp b/dom/xul/ChromeObserver.cpp index 9ad66348d4..73f712e30c 100644 --- a/dom/xul/ChromeObserver.cpp +++ b/dom/xul/ChromeObserver.cpp @@ -137,10 +137,6 @@ void ChromeObserver::AttributeChanged(dom::Element* aElement, // if the localedir changed on the root element, reset the document // direction mDocument->ResetDocumentDirection(); - } else if (aName == nsGkAtoms::lwtheme) { - // if the lwtheme changed, make sure to reset the document lwtheme - // cache - mDocument->ResetDocumentLWTheme(); } } else { if (aName == nsGkAtoms::hidechrome) { @@ -151,9 +147,6 @@ void ChromeObserver::AttributeChanged(dom::Element* aElement, // if the localedir changed on the root element, reset the document // direction mDocument->ResetDocumentDirection(); - } else if (aName == nsGkAtoms::lwtheme) { - // if the lwtheme changed, make sure to restyle appropriately - mDocument->ResetDocumentLWTheme(); } else if (aName == nsGkAtoms::drawtitle) { SetDrawsTitle(false); } |