From 4510bf92a26c811557d0cfd8bc2728595ab4438b Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sat, 9 May 2026 12:44:44 -0400 Subject: [PATCH 01/30] Floor fractional border widths --- layout/style/nsStyleStruct.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/layout/style/nsStyleStruct.h b/layout/style/nsStyleStruct.h index edecefdf08..6135c78ea3 100644 --- a/layout/style/nsStyleStruct.h +++ b/layout/style/nsStyleStruct.h @@ -1145,10 +1145,10 @@ private: nsCSSShadowItem mArray[1]; // This MUST be the last item }; -// Border widths are rounded to the nearest integer number of pixels, but values -// between zero and one device pixels are always rounded up to one device pixel. +// Border widths are rounded down to integer pixels, but values between zero and +// one device pixel are always rounded up to one device pixel. #define NS_ROUND_BORDER_TO_PIXELS(l,tpp) \ - ((l) == 0) ? 0 : std::max((tpp), ((l) + ((tpp) / 2)) / (tpp) * (tpp)) + ((l) == 0) ? 0 : std::max((tpp), (l) / (tpp) * (tpp)) // Caret widths are rounded to the nearest-below integer number of pixels, but values // between zero and one device pixels are always rounded up to one device pixel. #define NS_ROUND_CARET_TO_PIXELS(l,tpp) \ From 794f3fe2e7c11554cca6301fd8f32fb2f1f66c90 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sat, 9 May 2026 13:14:04 -0400 Subject: [PATCH 02/30] Test fractional border width rounding --- layout/style/test/mochitest.ini | 1 + .../test/test_border_width_rounding.html | 51 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 layout/style/test/test_border_width_rounding.html diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini index f9874a52af..acf09d9c03 100644 --- a/layout/style/test/mochitest.ini +++ b/layout/style/test/mochitest.ini @@ -75,6 +75,7 @@ support-files = file_animations_with_disabled_properties.html [test_nesting_flattening_parser_edges.html] [test_nesting_flattening_recovery.html] [test_box_size_keywords.html] +[test_border_width_rounding.html] [test_bug73586.html] [test_bug74880.html] [test_bug98997.html] diff --git a/layout/style/test/test_border_width_rounding.html b/layout/style/test/test_border_width_rounding.html new file mode 100644 index 0000000000..f7c2faabda --- /dev/null +++ b/layout/style/test/test_border_width_rounding.html @@ -0,0 +1,51 @@ + + + + Test fractional border and outline width rounding + + + + +
+
+
+
+ + From c2896414281eeee957acdfec6098d7e420bf31f5 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sat, 9 May 2026 14:27:59 -0400 Subject: [PATCH 03/30] Fix CSS border rounding and currentcolor clipping --- layout/base/nsCSSRendering.cpp | 19 +++--- .../currentcolor-border-radius-ref.html | 18 ++++++ .../currentcolor-border-radius.html | 18 ++++++ layout/reftests/border-radius/reftest.list | 2 + layout/style/nsComputedDOMStyle.cpp | 61 ++++++++++++++++++- layout/style/nsComputedDOMStyle.h | 1 + layout/style/nsComputedDOMStylePropertyList.h | 2 +- layout/style/nsRuleNode.cpp | 4 +- layout/style/nsStyleStruct.h | 12 ++-- .../test/test_border_width_rounding.html | 29 +++++++++ 10 files changed, 145 insertions(+), 21 deletions(-) create mode 100644 layout/reftests/border-radius/currentcolor-border-radius-ref.html create mode 100644 layout/reftests/border-radius/currentcolor-border-radius.html diff --git a/layout/base/nsCSSRendering.cpp b/layout/base/nsCSSRendering.cpp index 54f965e9d2..0bca63885b 100644 --- a/layout/base/nsCSSRendering.cpp +++ b/layout/base/nsCSSRendering.cpp @@ -1743,7 +1743,9 @@ nsCSSRendering::PaintBackground(const PaintBGParams& aParams) } static bool -IsOpaqueBorderEdge(const nsStyleBorder& aBorder, mozilla::Side aSide) +IsOpaqueBorderEdge(const nsStyleBorder& aBorder, + const nsStyleColor& aColor, + mozilla::Side aSide) { if (aBorder.GetComputedBorder().Side(aSide) == 0) return true; @@ -1765,25 +1767,20 @@ IsOpaqueBorderEdge(const nsStyleBorder& aBorder, mozilla::Side aSide) if (aBorder.mBorderImageSource.GetType() != eStyleImageType_Null) return false; - StyleComplexColor color = aBorder.mBorderColor[aSide]; - // We don't know the foreground color here, so if it's being used - // we must assume it might be transparent. - if (!color.IsNumericColor()) { - return false; - } - return NS_GET_A(color.mColor) == 255; + nscolor color = aColor.CalcComplexColor(aBorder.mBorderColor[aSide]); + return NS_GET_A(color) == 255; } /** * Returns true if all border edges are either missing or opaque. */ static bool -IsOpaqueBorder(const nsStyleBorder& aBorder) +IsOpaqueBorder(const nsStyleBorder& aBorder, const nsStyleColor& aColor) { if (aBorder.mBorderColors) return false; NS_FOR_CSS_SIDES(i) { - if (!IsOpaqueBorderEdge(aBorder, i)) + if (!IsOpaqueBorderEdge(aBorder, aColor, i)) return false; } return true; @@ -1916,7 +1913,7 @@ nsCSSRendering::GetImageLayerClip(const nsStyleImageLayers::Layer& aLayer, } bool isSolidBorder = - aWillPaintBorder && IsOpaqueBorder(aBorder); + aWillPaintBorder && IsOpaqueBorder(aBorder, *aForFrame->StyleColor()); if (isSolidBorder && layerClip == StyleGeometryBox::Border) { // If we have rounded corners, we need to inflate the background // drawing area a bit to avoid seams between the border and diff --git a/layout/reftests/border-radius/currentcolor-border-radius-ref.html b/layout/reftests/border-radius/currentcolor-border-radius-ref.html new file mode 100644 index 0000000000..94f7b98702 --- /dev/null +++ b/layout/reftests/border-radius/currentcolor-border-radius-ref.html @@ -0,0 +1,18 @@ + + + +
diff --git a/layout/reftests/border-radius/currentcolor-border-radius.html b/layout/reftests/border-radius/currentcolor-border-radius.html new file mode 100644 index 0000000000..ae93c104b9 --- /dev/null +++ b/layout/reftests/border-radius/currentcolor-border-radius.html @@ -0,0 +1,18 @@ + + + +
diff --git a/layout/reftests/border-radius/reftest.list b/layout/reftests/border-radius/reftest.list index 4b2d2f4ee6..c9de171ba0 100644 --- a/layout/reftests/border-radius/reftest.list +++ b/layout/reftests/border-radius/reftest.list @@ -89,6 +89,8 @@ fuzzy-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)||/^Windows\x20NT\x206\.2/.te # Test for antialiasing gaps between background and border fuzzy-if(gtkWidget,1,9) fuzzy-if(winWidget&&!d2d,1,9) fuzzy-if(d2d,5,40) fuzzy-if(skiaContent,1,9) == curved-border-background-nogap.html curved-border-background-nogap-ref.html +== currentcolor-border-radius.html currentcolor-border-radius-ref.html + == color-layer-1a.html color-layer-1-ref.html == corner-split.html corner-split-ref.svg # bug 1185636 diff --git a/layout/style/nsComputedDOMStyle.cpp b/layout/style/nsComputedDOMStyle.cpp index 686ea6a1f5..3915b21707 100644 --- a/layout/style/nsComputedDOMStyle.cpp +++ b/layout/style/nsComputedDOMStyle.cpp @@ -3193,6 +3193,66 @@ nsComputedDOMStyle::DoGetBorderRightWidth() return GetBorderWidthFor(eSideRight); } +already_AddRefed +nsComputedDOMStyle::DoGetBorderWidth() +{ + nscoord widths[4]; + if (mInnerFrame) { + AssertFlushedPendingReflows(); + const nsMargin& usedBorder = mInnerFrame->GetUsedBorder(); + NS_FOR_CSS_SIDES(side) { + widths[side] = usedBorder.Side(side); + } + } else { + const nsStyleBorder* border = StyleBorder(); + NS_FOR_CSS_SIDES(side) { + widths[side] = border->GetComputedBorderWidth(side); + } + } + + RefPtr valueList = GetROCSSValueList(false); + + RefPtr top = new nsROCSSPrimitiveValue; + top->SetAppUnits(widths[eSideTop]); + valueList->AppendCSSValue(top.forget()); + + if (widths[eSideRight] == widths[eSideLeft]) { + if (widths[eSideTop] == widths[eSideBottom]) { + if (widths[eSideTop] == widths[eSideRight]) { + return valueList.forget(); + } + + RefPtr right = new nsROCSSPrimitiveValue; + right->SetAppUnits(widths[eSideRight]); + valueList->AppendCSSValue(right.forget()); + return valueList.forget(); + } + + RefPtr right = new nsROCSSPrimitiveValue; + right->SetAppUnits(widths[eSideRight]); + valueList->AppendCSSValue(right.forget()); + + RefPtr bottom = new nsROCSSPrimitiveValue; + bottom->SetAppUnits(widths[eSideBottom]); + valueList->AppendCSSValue(bottom.forget()); + return valueList.forget(); + } + + RefPtr right = new nsROCSSPrimitiveValue; + right->SetAppUnits(widths[eSideRight]); + valueList->AppendCSSValue(right.forget()); + + RefPtr bottom = new nsROCSSPrimitiveValue; + bottom->SetAppUnits(widths[eSideBottom]); + valueList->AppendCSSValue(bottom.forget()); + + RefPtr left = new nsROCSSPrimitiveValue; + left->SetAppUnits(widths[eSideLeft]); + valueList->AppendCSSValue(left.forget()); + + return valueList.forget(); +} + already_AddRefed nsComputedDOMStyle::DoGetBorderTopColor() { @@ -6843,4 +6903,3 @@ nsComputedDOMStyle::DoGetOverflowBlockEnd() return DoGetOverflowBlock(); } - diff --git a/layout/style/nsComputedDOMStyle.h b/layout/style/nsComputedDOMStyle.h index 718a38ff47..cf168554b8 100644 --- a/layout/style/nsComputedDOMStyle.h +++ b/layout/style/nsComputedDOMStyle.h @@ -344,6 +344,7 @@ private: already_AddRefed DoGetBorderBottomWidth(); already_AddRefed DoGetBorderLeftWidth(); already_AddRefed DoGetBorderRightWidth(); + already_AddRefed DoGetBorderWidth(); already_AddRefed DoGetBorderTopColor(); already_AddRefed DoGetBorderBottomColor(); already_AddRefed DoGetBorderLeftColor(); diff --git a/layout/style/nsComputedDOMStylePropertyList.h b/layout/style/nsComputedDOMStylePropertyList.h index db0a68c660..51a1ed67f7 100644 --- a/layout/style/nsComputedDOMStylePropertyList.h +++ b/layout/style/nsComputedDOMStylePropertyList.h @@ -95,7 +95,7 @@ COMPUTED_STYLE_PROP(border_top_left_radius, BorderTopLeftRadius) COMPUTED_STYLE_PROP(border_top_right_radius, BorderTopRightRadius) COMPUTED_STYLE_PROP(border_top_style, BorderTopStyle) COMPUTED_STYLE_PROP(border_top_width, BorderTopWidth) -//// COMPUTED_STYLE_PROP(border_width, BorderWidth) +COMPUTED_STYLE_PROP(border_width, BorderWidth) COMPUTED_STYLE_PROP(bottom, Bottom) COMPUTED_STYLE_PROP(box_decoration_break, BoxDecorationBreak) COMPUTED_STYLE_PROP(box_shadow, BoxShadow) diff --git a/layout/style/nsRuleNode.cpp b/layout/style/nsRuleNode.cpp index baaa4081d6..eb645badda 100644 --- a/layout/style/nsRuleNode.cpp +++ b/layout/style/nsRuleNode.cpp @@ -8322,7 +8322,9 @@ nsRuleNode::ComputeOutlineData(void* aStartStruct, SETCOORD_LH | SETCOORD_INITIAL_ZERO | SETCOORD_CALC_LENGTH_ONLY | SETCOORD_UNSET_INITIAL, aContext, mPresContext, conditions)) { - outline->mOutlineOffset = tempCoord.GetCoordValue(); + outline->mOutlineOffset = + NS_ROUND_OFFSET_TO_PIXELS(tempCoord.GetCoordValue(), + mPresContext->AppUnitsPerDevPixel()); } else { NS_ASSERTION(outlineOffsetValue->GetUnit() == eCSSUnit_Null, "unexpected unit"); diff --git a/layout/style/nsStyleStruct.h b/layout/style/nsStyleStruct.h index 6135c78ea3..7770e2815f 100644 --- a/layout/style/nsStyleStruct.h +++ b/layout/style/nsStyleStruct.h @@ -1148,18 +1148,16 @@ private: // Border widths are rounded down to integer pixels, but values between zero and // one device pixel are always rounded up to one device pixel. #define NS_ROUND_BORDER_TO_PIXELS(l,tpp) \ - ((l) == 0) ? 0 : std::max((tpp), (l) / (tpp) * (tpp)) + (((l) == 0) ? 0 : std::max((tpp), (l) / (tpp) * (tpp))) // Caret widths are rounded to the nearest-below integer number of pixels, but values // between zero and one device pixels are always rounded up to one device pixel. #define NS_ROUND_CARET_TO_PIXELS(l,tpp) \ - ((l) == 0) ? 0 : std::max((tpp), (l) / (tpp) * (tpp)) -// Outline offset is rounded to the nearest integer number of pixels, but values -// between zero and one device pixels are always rounded up to one device pixel. -// Note that the offset can be negative. + NS_ROUND_BORDER_TO_PIXELS(l,tpp) +// Outline offset is snapped like border widths, preserving the sign. #define NS_ROUND_OFFSET_TO_PIXELS(l,tpp) \ (((l) == 0) ? 0 : \ - ((l) > 0) ? std::max( (tpp), ((l) + ((tpp) / 2)) / (tpp) * (tpp)) : \ - std::min(-(tpp), ((l) - ((tpp) / 2)) / (tpp) * (tpp))) + (((l) > 0) ? NS_ROUND_BORDER_TO_PIXELS((l), (tpp)) : \ + std::min(-(tpp), (l) / (tpp) * (tpp)))) // Returns if the given border style type is visible or not static bool IsVisibleBorderStyle(uint8_t aStyle) diff --git a/layout/style/test/test_border_width_rounding.html b/layout/style/test/test_border_width_rounding.html index f7c2faabda..2de866b200 100644 --- a/layout/style/test/test_border_width_rounding.html +++ b/layout/style/test/test_border_width_rounding.html @@ -42,8 +42,37 @@ function checkWidth(property, setupProperty, setupValue) { }); } +function checkOffset(property) { + tests.forEach(function(test) { + var specified = test[0]; + var expected = test[1]; + var div = document.createElement("div"); + div.style[property] = specified; + display.appendChild(div); + + is(document.defaultView.getComputedStyle(div, "")[property], expected, + property + " computes " + specified + " as " + expected); + + display.removeChild(div); + }); + + tests.forEach(function(test) { + var specified = test[0]; + var expected = test[0] == "0px" ? "0px" : "-" + test[1]; + var div = document.createElement("div"); + div.style[property] = "-" + specified; + display.appendChild(div); + + is(document.defaultView.getComputedStyle(div, "")[property], expected, + property + " computes -" + specified + " as " + expected); + + display.removeChild(div); + }); +} + checkWidth("borderWidth", "borderStyle", "solid"); checkWidth("outlineWidth", "outlineStyle", "solid"); +checkOffset("outlineOffset"); From 75136dff381efc31063feceb86f2c470fad8cb87 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sat, 9 May 2026 15:24:56 -0400 Subject: [PATCH 04/30] Flush layout for computed border-width shorthand --- layout/style/nsCSSPropList.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/layout/style/nsCSSPropList.h b/layout/style/nsCSSPropList.h index b420c5542f..018f71b8d9 100644 --- a/layout/style/nsCSSPropList.h +++ b/layout/style/nsCSSPropList.h @@ -1273,7 +1273,8 @@ CSS_PROP_SHORTHAND( border_width, BorderWidth, CSS_PROPERTY_PARSE_FUNCTION | - CSS_PROPERTY_UNITLESS_LENGTH_QUIRK, + CSS_PROPERTY_UNITLESS_LENGTH_QUIRK | + CSS_PROPERTY_GETCS_NEEDS_LAYOUT_FLUSH, "") CSS_PROP_POSITION( bottom, From 50a581840ea0a3515b5d2849cffdd2da92496f71 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sat, 9 May 2026 18:30:47 -0400 Subject: [PATCH 05/30] Fix app-unit rounding for border width edges --- layout/style/nsRuleNode.cpp | 146 +++++++++++++++--- .../test/test_border_width_rounding.html | 2 + 2 files changed, 127 insertions(+), 21 deletions(-) diff --git a/layout/style/nsRuleNode.cpp b/layout/style/nsRuleNode.cpp index eb645badda..93a754d4e7 100644 --- a/layout/style/nsRuleNode.cpp +++ b/layout/style/nsRuleNode.cpp @@ -319,6 +319,11 @@ nsRuleNode::EnsureInlineDisplay(StyleDisplay& display) } } +enum class AppUnitRounding { + Default, + TowardZero +}; + static nscoord CalcLengthWith(const nsCSSValue& aValue, nscoord aFontSize, const nsStyleFont* aStyleFont, @@ -326,7 +331,9 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue, nsPresContext* aPresContext, bool aUseProvidedRootEmSize, bool aUseUserFontSet, - RuleNodeCacheConditions& aConditions); + RuleNodeCacheConditions& aConditions, + AppUnitRounding aRounding = + AppUnitRounding::Default); struct CalcLengthCalcOps : public css::BasicCoordCalcOps, public css::NumbersAlreadyNormalizedOps @@ -339,18 +346,21 @@ struct CalcLengthCalcOps : public css::BasicCoordCalcOps, const bool mUseProvidedRootEmSize; const bool mUseUserFontSet; RuleNodeCacheConditions& mConditions; + const AppUnitRounding mRounding; CalcLengthCalcOps(nscoord aFontSize, const nsStyleFont* aStyleFont, nsStyleContext* aStyleContext, nsPresContext* aPresContext, bool aUseProvidedRootEmSize, bool aUseUserFontSet, - RuleNodeCacheConditions& aConditions) + RuleNodeCacheConditions& aConditions, + AppUnitRounding aRounding) : mFontSize(aFontSize), mStyleFont(aStyleFont), mStyleContext(aStyleContext), mPresContext(aPresContext), mUseProvidedRootEmSize(aUseProvidedRootEmSize), mUseUserFontSet(aUseUserFontSet), - mConditions(aConditions) + mConditions(aConditions), + mRounding(aRounding) { } @@ -358,15 +368,84 @@ struct CalcLengthCalcOps : public css::BasicCoordCalcOps, { return CalcLengthWith(aValue, mFontSize, mStyleFont, mStyleContext, mPresContext, mUseProvidedRootEmSize, - mUseUserFontSet, mConditions); + mUseUserFontSet, mConditions, mRounding); } }; -static inline nscoord ScaleCoordRound(const nsCSSValue& aValue, float aFactor) +static inline nscoord +ScaleCoordWithRounding(const nsCSSValue& aValue, float aFactor, + AppUnitRounding aRounding) { + if (aRounding == AppUnitRounding::TowardZero) { + return NSToCoordTruncClamped(aValue.GetFloatValue() * aFactor); + } return NSToCoordRoundWithClamp(aValue.GetFloatValue() * aFactor); } +static inline nscoord +CSSPixelsToAppUnitsWithRounding(float aPixels, AppUnitRounding aRounding) +{ + if (aRounding == AppUnitRounding::TowardZero) { + return NSToCoordTruncClamped(double(aPixels) * + nsPresContext::AppUnitsPerCSSPixel()); + } + return nsPresContext::CSSPixelsToAppUnits(aPixels); +} + +static nscoord +GetFixedLengthWithRounding(const nsCSSValue& aValue, + nsPresContext* aPresContext, + AppUnitRounding aRounding) +{ + if (aRounding == AppUnitRounding::Default) { + return aValue.GetFixedLength(aPresContext); + } + + MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_PhysicalMillimeter, + "not a fixed length unit"); + double inches = double(aValue.GetFloatValue()) / MM_PER_INCH_FLOAT; + return NSToCoordTruncClamped( + inches * aPresContext->DeviceContext()->AppUnitsPerPhysicalInch()); +} + +static nscoord +GetPixelLengthWithRounding(const nsCSSValue& aValue, + AppUnitRounding aRounding) +{ + MOZ_ASSERT(aValue.IsPixelLengthUnit(), "not a pixel length unit"); + + double scaleFactor; + switch (aValue.GetUnit()) { + case eCSSUnit_Pixel: + return CSSPixelsToAppUnitsWithRounding(aValue.GetFloatValue(), + aRounding); + case eCSSUnit_Pica: + scaleFactor = 16.0; + break; + case eCSSUnit_Point: + scaleFactor = 4.0 / 3.0; + break; + case eCSSUnit_Inch: + scaleFactor = 96.0; + break; + case eCSSUnit_Millimeter: + scaleFactor = 96.0 / 25.4; + break; + case eCSSUnit_Centimeter: + scaleFactor = 96.0 / 2.54; + break; + case eCSSUnit_Quarter: + scaleFactor = 96.0 / 101.6; + break; + default: + NS_ERROR("should never get here"); + return 0; + } + + return CSSPixelsToAppUnitsWithRounding( + float(double(aValue.GetFloatValue()) * scaleFactor), aRounding); +} + static inline nscoord ScaleViewportCoordTrunc(const nsCSSValue& aValue, nscoord aViewportSize) { @@ -465,7 +544,8 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue, // except when called from // CalcLengthWithInitialFont. bool aUseUserFontSet, - RuleNodeCacheConditions& aConditions) + RuleNodeCacheConditions& aConditions, + AppUnitRounding aRounding) { NS_ASSERTION(aValue.IsLengthUnit() || aValue.IsCalcUnit(), "not a length or calc unit"); @@ -476,10 +556,10 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue, NS_ASSERTION(aPresContext, "Must have prescontext"); if (aValue.IsFixedLengthUnit()) { - return aValue.GetFixedLength(aPresContext); + return GetFixedLengthWithRounding(aValue, aPresContext, aRounding); } if (aValue.IsPixelLengthUnit()) { - return aValue.GetPixelLength(); + return GetPixelLengthWithRounding(aValue, aRounding); } if (aValue.IsCalcUnit()) { // For properties for which lengths are the *only* units accepted in @@ -490,7 +570,7 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue, CalcLengthCalcOps ops(aFontSize, aStyleFont, aStyleContext, aPresContext, aUseProvidedRootEmSize, aUseUserFontSet, - aConditions); + aConditions, aRounding); return css::ComputeCalc(aValue, ops); } switch (aValue.GetUnit()) { @@ -623,7 +703,7 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue, rootFontSize = rootStyleFont->mFont.size; } - return ScaleCoordRound(aValue, float(rootFontSize)); + return ScaleCoordWithRounding(aValue, float(rootFontSize), aRounding); } default: // Fall through to the code for units that can't be stored in the @@ -648,7 +728,7 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue, // CSS2.1 specifies that this unit scales to the computed font // size, not the em-width in the font metrics, despite the name. aConditions.SetFontSizeDependency(aFontSize); - return ScaleCoordRound(aValue, float(aFontSize)); + return ScaleCoordWithRounding(aValue, float(aFontSize), aRounding); } case eCSSUnit_XHeight: { aPresContext->SetUsesExChUnits(true); @@ -656,7 +736,7 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue, GetMetricsFor(aPresContext, aStyleContext, styleFont, aFontSize, aUseUserFontSet); aConditions.SetUncacheable(); - return ScaleCoordRound(aValue, float(fm->XHeight())); + return ScaleCoordWithRounding(aValue, float(fm->XHeight()), aRounding); } case eCSSUnit_Char: { aPresContext->SetUsesExChUnits(true); @@ -668,8 +748,9 @@ static nscoord CalcLengthWith(const nsCSSValue& aValue, GetMetrics(fm->Orientation()).zeroOrAveCharWidth; aConditions.SetUncacheable(); - return ScaleCoordRound(aValue, ceil(aPresContext->AppUnitsPerDevPixel() * - zeroWidth)); + return ScaleCoordWithRounding( + aValue, ceil(aPresContext->AppUnitsPerDevPixel() * zeroWidth), + aRounding); } default: NS_NOTREACHED("unexpected unit"); @@ -701,6 +782,20 @@ static inline nscoord CalcLength(const nsCSSValue& aValue, aPresContext, aConditions); } +static inline nscoord +CalcLengthTowardZero(const nsCSSValue& aValue, + nsStyleContext* aStyleContext, + nsPresContext* aPresContext, + RuleNodeCacheConditions& aConditions) +{ + NS_ASSERTION(aStyleContext, "Must have style data"); + + return CalcLengthWith(aValue, -1, nullptr, + aStyleContext, aPresContext, + false, true, aConditions, + AppUnitRounding::TowardZero); +} + /* static */ nscoord nsRuleNode::CalcLengthWithInitialFont(nsPresContext* aPresContext, const nsCSSValue& aValue) @@ -8022,13 +8117,11 @@ nsRuleNode::ComputeBorderData(void* aStartStruct, "Unexpected enum value"); border->SetBorderWidth(side, (mPresContext->GetBorderWidthTable())[value.GetIntValue()]); - // OK to pass bad aParentCoord since we're not passing SETCOORD_INHERIT - } else if (SetCoord(value, coord, nsStyleCoord(), - SETCOORD_LENGTH | SETCOORD_CALC_LENGTH_ONLY, - aContext, mPresContext, conditions)) { - NS_ASSERTION(coord.GetUnit() == eStyleUnit_Coord, "unexpected unit"); + } else if (value.IsLengthUnit() || value.IsCalcUnit()) { // clamp negative calc() to 0. - border->SetBorderWidth(side, std::max(coord.GetCoordValue(), 0)); + border->SetBorderWidth(side, + std::max(CalcLengthTowardZero(value, aContext, mPresContext, + conditions), 0)); } else if (eCSSUnit_Inherit == value.GetUnit()) { conditions.SetUncacheable(); border->SetBorderWidth(side, @@ -8306,6 +8399,11 @@ nsRuleNode::ComputeOutlineData(void* aStartStruct, eCSSUnit_Revert == outlineWidthValue->GetUnit()) { outline->mOutlineWidth = nsStyleCoord(NS_STYLE_BORDER_WIDTH_MEDIUM, eStyleUnit_Enumerated); + } else if (outlineWidthValue->IsLengthUnit() || + outlineWidthValue->IsCalcUnit()) { + outline->mOutlineWidth.SetCoordValue( + CalcLengthTowardZero(*outlineWidthValue, aContext, mPresContext, + conditions)); } else { SetCoord(*outlineWidthValue, outline->mOutlineWidth, parentOutline->mOutlineWidth, @@ -8316,7 +8414,13 @@ nsRuleNode::ComputeOutlineData(void* aStartStruct, // outline-offset: length, inherit nsStyleCoord tempCoord; const nsCSSValue* outlineOffsetValue = aRuleData->ValueForOutlineOffset(); - if (SetCoord(*outlineOffsetValue, tempCoord, + if (outlineOffsetValue->IsLengthUnit() || outlineOffsetValue->IsCalcUnit()) { + outline->mOutlineOffset = + NS_ROUND_OFFSET_TO_PIXELS( + CalcLengthTowardZero(*outlineOffsetValue, aContext, mPresContext, + conditions), + mPresContext->AppUnitsPerDevPixel()); + } else if (SetCoord(*outlineOffsetValue, tempCoord, nsStyleCoord(parentOutline->mOutlineOffset, nsStyleCoord::CoordConstructor), SETCOORD_LH | SETCOORD_INITIAL_ZERO | SETCOORD_CALC_LENGTH_ONLY | diff --git a/layout/style/test/test_border_width_rounding.html b/layout/style/test/test_border_width_rounding.html index 2de866b200..16c75ed14a 100644 --- a/layout/style/test/test_border_width_rounding.html +++ b/layout/style/test/test_border_width_rounding.html @@ -24,6 +24,8 @@ var tests = [ ["2px", "2px"], ["2.75px", "2px"], ["2.999px", "2px"], + ["3px", "3px"], + ["3.001px", "3px"], ]; function checkWidth(property, setupProperty, setupValue) { From 29f5ff07d87b8cbd3dad6ac0a3f3fccd369f8b1d Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Tue, 5 May 2026 21:00:15 -0400 Subject: [PATCH 06/30] Issue #1826 - Implement broader CSS calc() parsing --- layout/style/nsCSSParser.cpp | 177 +++++++++++++++++- layout/style/test/mochitest.ini | 2 + layout/style/test/property_database.js | 28 ++- .../style/test/test_calc_numeric_types.html | 93 +++++++++ 4 files changed, 284 insertions(+), 16 deletions(-) create mode 100644 layout/style/test/test_calc_numeric_types.html diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index 528076b8f1..037d92c588 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -170,6 +170,153 @@ struct ReducePercentageCalcOps : ReduceNumberCalcOps } }; +struct ReduceDimensionCalcOps : public mozilla::css::BasicFloatCalcOps, + public mozilla::css::CSSValueInputCalcOps, + public mozilla::css::NumbersAlreadyNormalizedOps +{ + enum class DimensionType { + Angle, + Time, + Frequency, + }; + + explicit ReduceDimensionCalcOps(DimensionType aDimensionType) + : mDimensionType(aDimensionType) + { + } + + result_type ComputeLeafValue(const nsCSSValue& aValue) + { + switch (mDimensionType) { + case DimensionType::Angle: + MOZ_ASSERT(aValue.IsAngularUnit(), "unexpected unit"); + return aValue.GetAngleValueInDegrees(); + + case DimensionType::Time: + MOZ_ASSERT(aValue.IsTimeUnit(), "unexpected unit"); + return aValue.GetUnit() == eCSSUnit_Seconds + ? aValue.GetFloatValue() + : aValue.GetFloatValue() / float(PR_MSEC_PER_SEC); + + case DimensionType::Frequency: + MOZ_ASSERT(aValue.IsFrequencyUnit(), "unexpected unit"); + return aValue.GetUnit() == eCSSUnit_Kilohertz + ? aValue.GetFloatValue() * 1000.0f + : aValue.GetFloatValue(); + } + + MOZ_ASSERT_UNREACHABLE("unexpected dimension type"); + return 0.0f; + } + + DimensionType mDimensionType; +}; + +static uint32_t +GetCalcParseVariantMask(uint32_t aVariantMask) +{ + uint32_t calcVariantMask = 0; + + if (aVariantMask & VARIANT_LENGTH) { + calcVariantMask |= VARIANT_LENGTH | (aVariantMask & VARIANT_ABSOLUTE_DIMENSION); + } + if (aVariantMask & VARIANT_PERCENT) { + calcVariantMask |= VARIANT_PERCENT; + } + if (aVariantMask & VARIANT_ANGLE) { + calcVariantMask |= VARIANT_ANGLE; + } + if (aVariantMask & VARIANT_TIME) { + calcVariantMask |= VARIANT_TIME; + } + if (aVariantMask & VARIANT_FREQUENCY) { + calcVariantMask |= VARIANT_FREQUENCY; + } + if (aVariantMask & (VARIANT_NUMBER | VARIANT_INTEGER)) { + calcVariantMask |= VARIANT_NUMBER; + } + if (aVariantMask & VARIANT_OPACITY) { + calcVariantMask |= VARIANT_PN; + } + + return calcVariantMask; +} + +static bool +ShouldPreserveCalcValue(uint32_t aVariantMask) +{ + return (aVariantMask & VARIANT_LENGTH) != 0 || + ((aVariantMask & VARIANT_PERCENT) != 0 && + (aVariantMask & (VARIANT_NUMBER | VARIANT_INTEGER | + VARIANT_OPACITY)) == 0); +} + +static int32_t +RoundFloatToCSSInteger(float aValue) +{ + double rounded = std::floor(double(aValue) + 0.5); + rounded = mozilla::clamped( + rounded, + double(std::numeric_limits::min()), + double(std::numeric_limits::max())); + return int32_t(rounded); +} + +static bool +NormalizeCalcForVariant(nsCSSValue& aValue, + uint32_t aPropertyVariantMask, + uint32_t aResultVariantMask) +{ + if (ShouldPreserveCalcValue(aPropertyVariantMask)) { + return true; + } + + if (aResultVariantMask == VARIANT_NUMBER) { + ReduceNumberCalcOps ops; + float value = mozilla::css::ComputeCalc(aValue, ops); + + if (aPropertyVariantMask & VARIANT_INTEGER) { + aValue.SetIntValue(RoundFloatToCSSInteger(value), eCSSUnit_Integer); + } else { + aValue.SetFloatValue(value, eCSSUnit_Number); + } + return true; + } + + if (aResultVariantMask & VARIANT_PERCENT) { + ReducePercentageCalcOps ops; + float value = mozilla::css::ComputeCalc(aValue, ops); + + if (aPropertyVariantMask & VARIANT_OPACITY) { + aValue.SetFloatValue(value, eCSSUnit_Number); + } else { + aValue.SetPercentValue(value); + } + return true; + } + + if (aResultVariantMask & VARIANT_ANGLE) { + ReduceDimensionCalcOps ops(ReduceDimensionCalcOps::DimensionType::Angle); + aValue.SetFloatValue(mozilla::css::ComputeCalc(aValue, ops), eCSSUnit_Degree); + return true; + } + + if (aResultVariantMask & VARIANT_TIME) { + ReduceDimensionCalcOps ops(ReduceDimensionCalcOps::DimensionType::Time); + aValue.SetFloatValue(mozilla::css::ComputeCalc(aValue, ops), eCSSUnit_Seconds); + return true; + } + + if (aResultVariantMask & VARIANT_FREQUENCY) { + ReduceDimensionCalcOps ops(ReduceDimensionCalcOps::DimensionType::Frequency); + aValue.SetFloatValue(mozilla::css::ComputeCalc(aValue, ops), eCSSUnit_Hertz); + return true; + } + + MOZ_ASSERT_UNREACHABLE("unsupported calc result type"); + return false; +} + static_assert(css::eAuthorSheetFeatures == 0 && css::eUserSheetFeatures == 1 && css::eAgentSheetFeatures == 2, @@ -980,7 +1127,8 @@ protected: bool ParseBorderStyle(); bool ParseBorderWidth(); - bool ParseCalc(nsCSSValue &aValue, uint32_t aVariantMask); + bool ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask, + uint32_t* aResultVariantMask = nullptr); bool ParseCalcAdditiveExpression(nsCSSValue& aValue, uint32_t& aVariantMask); bool ParseCalcMultiplicativeExpression(nsCSSValue& aValue, @@ -8584,14 +8732,18 @@ CSSParserImpl::ParseNonNegativeVariant(nsCSSValue& aValue, VARIANT_NUMBER | VARIANT_LENGTH | VARIANT_PERCENT | + VARIANT_FREQUENCY | VARIANT_OPACITY | + VARIANT_TIME | VARIANT_INTEGER)) == 0, "need to update code below to handle additional variants"); CSSParseResult result = ParseVariant(aValue, aVariantMask, aKeywordTable); if (result == CSSParseResult::Ok) { if (eCSSUnit_Number == aValue.GetUnit() || - aValue.IsLengthUnit()){ + aValue.IsLengthUnit() || + aValue.IsFrequencyUnit() || + aValue.IsTimeUnit()) { if (aValue.GetFloatValue() < 0) { UngetToken(); return CSSParseResult::NotFound; @@ -8953,12 +9105,14 @@ CSSParserImpl::ParseVariant(nsCSSValue& aValue, return CSSParseResult::Ok; } } - if ((aVariantMask & VARIANT_CALC) && + uint32_t calcVariantMask = GetCalcParseVariantMask(aVariantMask); + if (calcVariantMask && + (((aVariantMask & VARIANT_CALC) != 0) || + !ShouldPreserveCalcValue(calcVariantMask)) && IsCalcFunctionToken(*tk)) { - // calc() currently allows only lengths and percents and number inside it. - // And note that in current implementation, number cannot be mixed with - // length and percent. - if (!ParseCalc(aValue, aVariantMask & VARIANT_LPN)) { + uint32_t calcResultVariantMask = calcVariantMask; + if (!ParseCalc(aValue, calcVariantMask, &calcResultVariantMask) || + !NormalizeCalcForVariant(aValue, aVariantMask, calcResultVariantMask)) { return CSSParseResult::Error; } return CSSParseResult::Ok; @@ -14663,7 +14817,8 @@ CSSParserImpl::ParseBorderColors(nsCSSPropertyID aProperty) // Parse the top level of a calc() expression. bool -CSSParserImpl::ParseCalc(nsCSSValue &aValue, uint32_t aVariantMask) +CSSParserImpl::ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask, + uint32_t* aResultVariantMask) { // Parsing calc expressions requires, in a number of cases, looking // for a token that is *either* a value of the property or a number. @@ -14678,14 +14833,18 @@ CSSParserImpl::ParseCalc(nsCSSValue &aValue, uint32_t aVariantMask) do { // The toplevel of a calc() is always an nsCSSValue::Array of length 1. RefPtr arr = nsCSSValue::Array::Create(1); + uint32_t resultVariantMask = aVariantMask; - if (!ParseCalcAdditiveExpression(arr->Item(0), aVariantMask)) + if (!ParseCalcAdditiveExpression(arr->Item(0), resultVariantMask)) break; if (!ExpectSymbol(')', true)) break; aValue.SetArrayValue(arr, eCSSUnit_Calc); + if (aResultVariantMask) { + *aResultVariantMask = resultVariantMask; + } mUnitlessLengthQuirk = oldUnitlessLengthQuirk; return true; } while (false); diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini index acf09d9c03..45e5823343 100644 --- a/layout/style/test/mochitest.ini +++ b/layout/style/test/mochitest.ini @@ -157,6 +157,8 @@ support-files = file_bug1089417_iframe.html [test_clip-path_polygon.html] [test_compute_data_with_start_struct.html] [test_computed_style.html] +[test_calc_numeric_types.html] +prefs = layout.css.filters.enabled=true [test_computed_style_min_size_auto.html] [test_computed_style_no_pseudo.html] [test_computed_style_prefs.html] diff --git a/layout/style/test/property_database.js b/layout/style/test/property_database.js index 3e7ee12a3c..ef2e39d792 100644 --- a/layout/style/test/property_database.js +++ b/layout/style/test/property_database.js @@ -122,6 +122,8 @@ var validGradientAndElementValues = [ "linear-gradient(10deg, red, blue)", "linear-gradient(1turn, red, blue)", "linear-gradient(.414rad, red, blue)", + "linear-gradient(calc(90deg / 2), red, blue)", + "linear-gradient(calc(calc(0.25turn) + 45deg), red, blue)", "linear-gradient(90deg in srgb, yellow, purple)", "linear-gradient(90deg in hsl, yellow, purple)", "linear-gradient(90deg in lch, yellow, purple)", @@ -1005,7 +1007,8 @@ var gCSSProperties = { inherited: false, type: CSS_TYPE_LONGHAND, initial_values: ["0s", "0ms"], - other_values: ["1s", "250ms", "-100ms", "-1s", "1s, 250ms, 2.3s"], + other_values: ["1s", "250ms", "-100ms", "-1s", "1s, 250ms, 2.3s", + "calc(250ms - 0.5s)", "calc(calc(500ms) - 250ms)"], invalid_values: ["0", "0px"], }, "animation-direction": { @@ -1030,7 +1033,8 @@ var gCSSProperties = { inherited: false, type: CSS_TYPE_LONGHAND, initial_values: ["0s", "0ms"], - other_values: ["1s", "250ms", "1s, 250ms, 2.3s"], + other_values: ["1s", "250ms", "1s, 250ms, 2.3s", + "calc(calc(0.25s) + 250ms)"], invalid_values: ["0", "0px", "-1ms", "-2s"], }, "animation-fill-mode": { @@ -2854,6 +2858,9 @@ var gCSSProperties = { "translate(calc(5px - 10% * 3))", "translate(calc(5px - 3 * 10%), 50px)", "translate(-50px, calc(5px - 10% * 3))", + "rotate(calc(45deg + 45deg))", + "rotate(calc(calc(0.125turn) + 45deg))", + "scale(calc(1 + 0.5))", "translatez(1px)", "translatez(4em)", "translatez(-4px)", @@ -5572,7 +5579,8 @@ var gCSSProperties = { "3e+0", "3e-0", ], - other_values: ["0", "0.4", "0.0000", "-3", "3e-1", "-100%", "50%"], + other_values: ["0", "0.4", "0.0000", "-3", "3e-1", "-100%", "50%", + "calc(25% * 2)", "calc(calc(0.25) + 0.25)"], invalid_values: ["0px", "1px"], }, "-moz-orient": { @@ -6401,7 +6409,8 @@ var gCSSProperties = { inherited: false, type: CSS_TYPE_LONGHAND, initial_values: ["0s", "0ms"], - other_values: ["1s", "250ms", "-100ms", "-1s", "1s, 250ms, 2.3s"], + other_values: ["1s", "250ms", "-100ms", "-1s", "1s, 250ms, 2.3s", + "calc(250ms - 0.5s)", "calc(calc(500ms) - 250ms)"], invalid_values: ["0", "0px"], }, "transition-duration": { @@ -6409,7 +6418,8 @@ var gCSSProperties = { inherited: false, type: CSS_TYPE_LONGHAND, initial_values: ["0s", "0ms"], - other_values: ["1s", "250ms", "1s, 250ms, 2.3s"], + other_values: ["1s", "250ms", "1s, 250ms, 2.3s", + "calc(calc(0.25s) + 250ms)"], invalid_values: ["0", "0px", "-1ms", "-2s"], }, "transition-property": { @@ -6773,7 +6783,7 @@ var gCSSProperties = { type: CSS_TYPE_LONGHAND, /* XXX requires position */ initial_values: ["auto"], - other_values: ["0", "3", "-7000", "12000"], + other_values: ["0", "3", "-7000", "12000", "calc(2.5)"], invalid_values: ["3.0", "17.5", "3e1"], }, "clip-path": { @@ -7633,7 +7643,8 @@ var gCSSProperties = { inherited: false, type: CSS_TYPE_LONGHAND, initial_values: ["0"], - other_values: ["1", "99999", "-1", "-50"], + other_values: ["1", "99999", "-1", "-50", "calc(1.5)", + "calc(calc(-2) + 1)"], invalid_values: ["0px", "1.0", "1.", "1%", "0.2", "3em", "stretch"], }, @@ -9293,6 +9304,8 @@ if (IsCSSPropertyPrefEnabled("layout.css.filters.enabled")) { "brightness(2)", "brightness(350%)", "brightness(4.567)", + "brightness(calc(25% * 2))", + "brightness(calc(calc(0.25) + 0.25))", "contrast(0)", "contrast(50%)", @@ -9337,6 +9350,7 @@ if (IsCSSPropertyPrefEnabled("layout.css.filters.enabled")) { "hue-rotate(-1.6rad)", "hue-rotate(0.5turn)", "hue-rotate(-2turn)", + "hue-rotate(calc(90deg + 0.125turn))", "invert(0)", "invert(50%)", diff --git a/layout/style/test/test_calc_numeric_types.html b/layout/style/test/test_calc_numeric_types.html new file mode 100644 index 0000000000..eb84cd986e --- /dev/null +++ b/layout/style/test/test_calc_numeric_types.html @@ -0,0 +1,93 @@ + + + + + Test calc() numeric type support + + + + +
+
+
+    
+ + From f9a90b9bb26d15dfbcb7cb6bf056f3dd634d6f91 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Tue, 5 May 2026 21:18:24 -0400 Subject: [PATCH 07/30] Issue #1826 - Support calc() in media queries --- layout/style/CSSStyleSheet.cpp | 6 ++++-- layout/style/nsCSSParser.cpp | 2 +- layout/style/test/test_media_queries.html | 11 +++++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/layout/style/CSSStyleSheet.cpp b/layout/style/CSSStyleSheet.cpp index 05bb84018f..4acbc408a4 100644 --- a/layout/style/CSSStyleSheet.cpp +++ b/layout/style/CSSStyleSheet.cpp @@ -161,7 +161,8 @@ nsMediaExpression::Matches(nsPresContext *aPresContext, case nsMediaFeature::eLength: { NS_ASSERTION(actual.IsLengthUnit(), "bad actual value"); - NS_ASSERTION(required.IsLengthUnit(), "bad required value"); + NS_ASSERTION(required.IsLengthUnit() || required.IsCalcUnit(), + "bad required value"); nscoord actualCoord = nsRuleNode::CalcLengthWithInitialFont( aPresContext, actual); nscoord requiredCoord = nsRuleNode::CalcLengthWithInitialFont( @@ -490,7 +491,8 @@ nsMediaQuery::AppendToString(nsAString& aString) const aString.AppendLiteral(": "); switch (feature->mValueType) { case nsMediaFeature::eLength: - NS_ASSERTION(expr.mValue.IsLengthUnit(), "bad unit"); + NS_ASSERTION(expr.mValue.IsLengthUnit() || expr.mValue.IsCalcUnit(), + "bad unit"); // Use 'width' as a property that takes length values // written in the normal way. expr.mValue.AppendToString(eCSSProperty_width, aString, diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index 037d92c588..2733abbeff 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -3999,7 +3999,7 @@ CSSParserImpl::ParseMediaQueryExpression(nsMediaQuery* aQuery) bool rv = false; switch (feature->mValueType) { case nsMediaFeature::eLength: - rv = ParseSingleTokenNonNegativeVariant(expr->mValue, VARIANT_LENGTH, + rv = ParseSingleTokenNonNegativeVariant(expr->mValue, VARIANT_LCALC, nullptr); break; case nsMediaFeature::eInteger: diff --git a/layout/style/test/test_media_queries.html b/layout/style/test/test_media_queries.html index 911ee6f434..a6d10e3fd5 100644 --- a/layout/style/test/test_media_queries.html +++ b/layout/style/test/test_media_queries.html @@ -237,6 +237,7 @@ function run() { expression_should_be_parseable(feature + ": 0"); expression_should_be_parseable(feature + ": 0px"); expression_should_be_parseable(feature + ": 0em"); + expression_should_be_parseable(feature + ": calc(0px + 0em)"); expression_should_be_parseable(feature + ": -0"); expression_should_be_parseable("min-" + feature + ": -0"); expression_should_be_parseable("max-" + feature + ": -0"); @@ -244,6 +245,7 @@ function run() { expression_should_be_parseable(feature + ": 1px"); expression_should_be_parseable(feature + ": 0.001mm"); expression_should_be_parseable(feature + ": 100000px"); + expression_should_not_be_parseable(feature + ": calc(1px + 1%)"); expression_should_not_be_parseable(feature + ": -1px"); expression_should_not_be_parseable("min-" + feature + ": -1px"); expression_should_not_be_parseable("max-" + feature + ": -1px"); @@ -263,8 +265,9 @@ function run() { var content_div = document.getElementById("content"); content_div.style.font = "initial"; - var em_size = - getComputedStyle(content_div, "").fontSize.match(/^(\d+)px$/)[1]; + var em_size = parseFloat( + getComputedStyle(content_div, "").fontSize.match(/^(\d+)px$/)[1] + ); // in this test, assume the common underlying implementation is correct var width_val = 117; // pick two not-too-round numbers @@ -282,7 +285,11 @@ function run() { for (feature in features) { var value = features[feature]; should_apply("all and (" + feature + ": " + value + "px)"); + should_apply("all and (" + feature + ": calc(" + + (value - em_size) + "px + 1em))"); should_not_apply("all and (" + feature + ": " + (value + 1) + "px)"); + should_not_apply("all and (" + feature + ": calc(" + + (value - em_size + 1) + "px + 1em))"); should_not_apply("all and (" + feature + ": " + (value - 1) + "px)"); should_apply("all and (min-" + feature + ": " + value + "px)"); should_not_apply("all and (min-" + feature + ": " + (value + 1) + "px)"); From f94a63864cfc4afa33730d8430349f9c96a8f814 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Tue, 5 May 2026 21:18:31 -0400 Subject: [PATCH 08/30] Issue #1826 - Canonicalize nested calc() serialization --- layout/style/nsCSSValue.cpp | 210 +++++++++++++++++- .../style/test/test_calc_numeric_types.html | 19 ++ 2 files changed, 227 insertions(+), 2 deletions(-) diff --git a/layout/style/nsCSSValue.cpp b/layout/style/nsCSSValue.cpp index 1e43377460..046f7cb108 100644 --- a/layout/style/nsCSSValue.cpp +++ b/layout/style/nsCSSValue.cpp @@ -1018,6 +1018,209 @@ private: nsCSSValue::Serialization mValueSerialization; }; +struct CSSValueReduceNumberCalcOps : public BasicFloatCalcOps, + public CSSValueInputCalcOps +{ + result_type ComputeLeafValue(const nsCSSValue& aValue) + { + MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_Number, "unexpected unit"); + return aValue.GetFloatValue(); + } + + float ComputeNumber(const nsCSSValue& aValue) + { + return css::ComputeCalc(aValue, *this); + } +}; + +struct CSSValueCalcLengthTerm { + nsCSSUnit mUnit; + float mValue; +}; + +struct CSSValueLengthPercentCalcAccumulator { + AutoTArray mLengths; + float mPercent = 0.0f; + bool mSawPercent = false; + + void AddLength(nsCSSUnit aUnit, float aValue) + { + for (CSSValueCalcLengthTerm& term : mLengths) { + if (term.mUnit == aUnit) { + term.mValue += aValue; + return; + } + } + + CSSValueCalcLengthTerm& term = *mLengths.AppendElement(); + term.mUnit = aUnit; + term.mValue = aValue; + } +}; + +static bool +IsNearlyZero(float aValue) +{ + return aValue > -0.000001f && aValue < 0.000001f; +} + +static bool +ComputeCalcNumberValue(const nsCSSValue& aValue, float& aResult) +{ + if (aValue.GetUnit() == eCSSUnit_Number) { + aResult = aValue.GetFloatValue(); + return true; + } + + if (!aValue.IsCalcUnit()) { + return false; + } + + CSSValueReduceNumberCalcOps ops; + aResult = css::ComputeCalc(aValue, ops); + return true; +} + +static bool +AccumulateCalcLengthPercentTerms( + const nsCSSValue& aValue, + float aScale, + CSSValueLengthPercentCalcAccumulator& aAccumulator) +{ + switch (aValue.GetUnit()) { + case eCSSUnit_Calc: { + const nsCSSValue::Array* array = aValue.GetArrayValue(); + MOZ_ASSERT(array->Count() == 1, "unexpected length"); + return AccumulateCalcLengthPercentTerms(array->Item(0), aScale, + aAccumulator); + } + + case eCSSUnit_Calc_Plus: + case eCSSUnit_Calc_Minus: { + const nsCSSValue::Array* array = aValue.GetArrayValue(); + MOZ_ASSERT(array->Count() == 2, "unexpected length"); + if (!AccumulateCalcLengthPercentTerms(array->Item(0), aScale, + aAccumulator)) { + return false; + } + float rhsScale = aValue.GetUnit() == eCSSUnit_Calc_Plus + ? aScale + : -aScale; + return AccumulateCalcLengthPercentTerms(array->Item(1), rhsScale, + aAccumulator); + } + + case eCSSUnit_Calc_Times_L: + case eCSSUnit_Calc_Times_R: + case eCSSUnit_Calc_Divided: { + const nsCSSValue::Array* array = aValue.GetArrayValue(); + MOZ_ASSERT(array->Count() == 2, "unexpected length"); + + const nsCSSValue* value = &array->Item(0); + const nsCSSValue* factorValue = &array->Item(1); + if (aValue.GetUnit() == eCSSUnit_Calc_Times_L) { + value = &array->Item(1); + factorValue = &array->Item(0); + } + + float factor; + if (!ComputeCalcNumberValue(*factorValue, factor)) { + return false; + } + if (aValue.GetUnit() == eCSSUnit_Calc_Divided) { + factor = 1.0f / factor; + } + return AccumulateCalcLengthPercentTerms(*value, aScale * factor, + aAccumulator); + } + + case eCSSUnit_Percent: + aAccumulator.mSawPercent = true; + aAccumulator.mPercent += aScale * aValue.GetPercentValue(); + return true; + + default: + break; + } + + if (aValue.IsLengthUnit()) { + aAccumulator.AddLength(aValue.GetUnit(), aScale * aValue.GetFloatValue()); + return true; + } + + return false; +} + +static void +AppendSerializedCalcLeafValue(nsCSSPropertyID aProperty, + nsAString& aResult, + nsCSSValue::Serialization aSerialization, + nsCSSUnit aUnit, + float aValue) +{ + nsCSSValue value; + if (aUnit == eCSSUnit_Percent) { + value.SetPercentValue(aValue); + } else { + value.SetFloatValue(aValue, aUnit); + } + value.AppendToString(aProperty, aResult, aSerialization); +} + +static bool +AppendNormalizedLengthPercentCalcToString( + const nsCSSValue& aValue, + nsCSSPropertyID aProperty, + nsAString& aResult, + nsCSSValue::Serialization aSerialization) +{ + CSSValueLengthPercentCalcAccumulator accumulator; + if (!AccumulateCalcLengthPercentTerms(aValue, 1.0f, accumulator)) { + return false; + } + + AutoTArray serializedTerms; + if (!IsNearlyZero(accumulator.mPercent) || accumulator.mSawPercent) { + CSSValueCalcLengthTerm& term = *serializedTerms.AppendElement(); + term.mUnit = eCSSUnit_Percent; + term.mValue = accumulator.mPercent; + } + + for (const CSSValueCalcLengthTerm& term : accumulator.mLengths) { + if (IsNearlyZero(term.mValue)) { + continue; + } + serializedTerms.AppendElement(term); + } + + if (serializedTerms.IsEmpty()) { + CSSValueCalcLengthTerm& term = *serializedTerms.AppendElement(); + term.mUnit = accumulator.mSawPercent ? eCSSUnit_Percent : eCSSUnit_Pixel; + term.mValue = 0.0f; + } + + aResult.AppendLiteral("calc("); + AppendSerializedCalcLeafValue(aProperty, aResult, aSerialization, + serializedTerms[0].mUnit, + serializedTerms[0].mValue); + + for (uint32_t i = 1; i < serializedTerms.Length(); ++i) { + const CSSValueCalcLengthTerm& term = serializedTerms[i]; + if (term.mValue < 0.0f) { + aResult.AppendLiteral(" - "); + AppendSerializedCalcLeafValue(aProperty, aResult, aSerialization, + term.mUnit, -term.mValue); + } else { + aResult.AppendLiteral(" + "); + AppendSerializedCalcLeafValue(aProperty, aResult, aSerialization, + term.mUnit, term.mValue); + } + } + + aResult.Append(')'); + return true; +} + } // namespace void @@ -1474,8 +1677,11 @@ nsCSSValue::AppendToString(nsCSSPropertyID aProperty, nsAString& aResult, } else if (IsCalcUnit()) { MOZ_ASSERT(GetUnit() == eCSSUnit_Calc, "unexpected unit"); - CSSValueSerializeCalcOps ops(aProperty, aResult, aSerialization); - css::SerializeCalc(*this, ops); + if (!AppendNormalizedLengthPercentCalcToString(*this, aProperty, aResult, + aSerialization)) { + CSSValueSerializeCalcOps ops(aProperty, aResult, aSerialization); + css::SerializeCalc(*this, ops); + } } else if (eCSSUnit_Integer == unit) { aResult.AppendInt(GetIntValue(), 10); diff --git a/layout/style/test/test_calc_numeric_types.html b/layout/style/test/test_calc_numeric_types.html index eb84cd986e..267136dfe2 100644 --- a/layout/style/test/test_calc_numeric_types.html +++ b/layout/style/test/test_calc_numeric_types.html @@ -87,6 +87,25 @@ function parse2dMatrix(transformValue) { div.remove(); })(); + +(function testNestedCalcSpecifiedSerialization() { + const div = appendTestNode(); + const cases = [ + ["left", "calc(calc(20px) + calc(80px))", "calc(100px)"], + ["left", "calc(calc(2) * calc(50px))", "calc(100px)"], + ["left", "calc(calc(150px * 2 / 3))", "calc(100px)"], + ["left", "calc(50px + calc(40%))", "calc(40% + 50px)"], + ["border", "calc(calc(10px)) solid pink", "calc(10px) solid pink"], + ]; + + for (const [property, input, expected] of cases) { + div.style.setProperty(property, input, ""); + is(div.style.getPropertyValue(property), expected, + `${property} should canonicalize nested calc() serialization`); + } + + div.remove(); +})(); From 12b120db2456cdb23a70b66faa7cbdd600d0f48f Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Wed, 6 May 2026 07:58:56 -0400 Subject: [PATCH 09/30] Issue #1826 - Add typed calc() arithmetic for media queries --- layout/style/CSSCalc.h | 12 +-- layout/style/CSSStyleSheet.cpp | 107 +++++++++++++++++++- layout/style/nsCSSParser.cpp | 117 +++++++++++++++++++++- layout/style/test/test_media_queries.html | 15 +++ 4 files changed, 234 insertions(+), 17 deletions(-) diff --git a/layout/style/CSSCalc.h b/layout/style/CSSCalc.h index 06a570c731..665fcd3613 100644 --- a/layout/style/CSSCalc.h +++ b/layout/style/CSSCalc.h @@ -318,11 +318,7 @@ SerializeCalcInternal(const typename CalcOps::input_type& aValue, CalcOps &aOps) if (needParens) { aOps.Append("("); } - if (unit == eCSSUnit_Calc_Times_L) { - aOps.AppendNumber(array->Item(0)); - } else { - SerializeCalcInternal(array->Item(0), aOps); - } + SerializeCalcInternal(array->Item(0), aOps); if (needParens) { aOps.Append(")"); } @@ -340,11 +336,7 @@ SerializeCalcInternal(const typename CalcOps::input_type& aValue, CalcOps &aOps) if (needParens) { aOps.Append("("); } - if (unit == eCSSUnit_Calc_Times_L) { - SerializeCalcInternal(array->Item(1), aOps); - } else { - aOps.AppendNumber(array->Item(1)); - } + SerializeCalcInternal(array->Item(1), aOps); if (needParens) { aOps.Append(")"); } diff --git a/layout/style/CSSStyleSheet.cpp b/layout/style/CSSStyleSheet.cpp index 4acbc408a4..ff044f3a77 100644 --- a/layout/style/CSSStyleSheet.cpp +++ b/layout/style/CSSStyleSheet.cpp @@ -130,6 +130,106 @@ int32_t DoCompare(Numeric a, Numeric b) return 1; } +struct MediaQueryTypedCalcLengthResult +{ + double mValue; + int32_t mExponent; +}; + +static bool +EvaluateMediaQueryTypedCalcLength(nsPresContext* aPresContext, + const nsCSSValue& aValue, + MediaQueryTypedCalcLengthResult& aResult) +{ + switch (aValue.GetUnit()) { + case eCSSUnit_Calc: { + nsCSSValue::Array* array = aValue.GetArrayValue(); + MOZ_ASSERT(array->Count() == 1, "unexpected length"); + return EvaluateMediaQueryTypedCalcLength(aPresContext, array->Item(0), + aResult); + } + + case eCSSUnit_Calc_Plus: + case eCSSUnit_Calc_Minus: { + nsCSSValue::Array* array = aValue.GetArrayValue(); + MOZ_ASSERT(array->Count() == 2, "unexpected length"); + MediaQueryTypedCalcLengthResult lhs; + MediaQueryTypedCalcLengthResult rhs; + if (!EvaluateMediaQueryTypedCalcLength(aPresContext, array->Item(0), lhs) || + !EvaluateMediaQueryTypedCalcLength(aPresContext, array->Item(1), rhs) || + lhs.mExponent != rhs.mExponent) { + return false; + } + aResult.mExponent = lhs.mExponent; + aResult.mValue = aValue.GetUnit() == eCSSUnit_Calc_Plus + ? lhs.mValue + rhs.mValue + : lhs.mValue - rhs.mValue; + return true; + } + + case eCSSUnit_Calc_Times_L: + case eCSSUnit_Calc_Times_R: + case eCSSUnit_Calc_Divided: { + nsCSSValue::Array* array = aValue.GetArrayValue(); + MOZ_ASSERT(array->Count() == 2, "unexpected length"); + MediaQueryTypedCalcLengthResult lhs; + MediaQueryTypedCalcLengthResult rhs; + if (!EvaluateMediaQueryTypedCalcLength(aPresContext, array->Item(0), lhs) || + !EvaluateMediaQueryTypedCalcLength(aPresContext, array->Item(1), rhs)) { + return false; + } + if (aValue.GetUnit() == eCSSUnit_Calc_Divided) { + if (rhs.mValue == 0.0) { + return false; + } + aResult.mValue = lhs.mValue / rhs.mValue; + aResult.mExponent = lhs.mExponent - rhs.mExponent; + } else { + aResult.mValue = lhs.mValue * rhs.mValue; + aResult.mExponent = lhs.mExponent + rhs.mExponent; + } + return true; + } + + case eCSSUnit_Number: + aResult.mValue = aValue.GetFloatValue(); + aResult.mExponent = 0; + return true; + + default: + break; + } + + if (!aValue.IsLengthUnit()) { + return false; + } + + aResult.mValue = double(nsRuleNode::CalcLengthWithInitialFont(aPresContext, + aValue)); + aResult.mExponent = 1; + return true; +} + +static bool +ComputeMediaQueryTypedCalcLength(nsPresContext* aPresContext, + const nsCSSValue& aValue, + nscoord& aResult) +{ + if (aValue.IsLengthUnit()) { + aResult = nsRuleNode::CalcLengthWithInitialFont(aPresContext, aValue); + return true; + } + + MediaQueryTypedCalcLengthResult result; + if (!EvaluateMediaQueryTypedCalcLength(aPresContext, aValue, result) || + result.mExponent != 1) { + return false; + } + + aResult = NSToCoordFloorClamped(result.mValue); + return true; +} + bool nsMediaExpression::Matches(nsPresContext *aPresContext, const nsCSSValue& aActualValue) const @@ -165,8 +265,11 @@ nsMediaExpression::Matches(nsPresContext *aPresContext, "bad required value"); nscoord actualCoord = nsRuleNode::CalcLengthWithInitialFont( aPresContext, actual); - nscoord requiredCoord = nsRuleNode::CalcLengthWithInitialFont( - aPresContext, required); + nscoord requiredCoord; + if (!ComputeMediaQueryTypedCalcLength(aPresContext, required, + requiredCoord)) { + return false; + } cmp = DoCompare(actualCoord, requiredCoord); } break; diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index 2733abbeff..a75d21ab3f 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -262,6 +262,65 @@ RoundFloatToCSSInteger(float aValue) return int32_t(rounded); } +static bool +GetCalcLengthTypedArithmeticExponent(const nsCSSValue& aValue, + int32_t& aExponent) +{ + switch (aValue.GetUnit()) { + case eCSSUnit_Calc: { + nsCSSValue::Array* array = aValue.GetArrayValue(); + MOZ_ASSERT(array->Count() == 1, "unexpected length"); + return GetCalcLengthTypedArithmeticExponent(array->Item(0), aExponent); + } + + case eCSSUnit_Calc_Plus: + case eCSSUnit_Calc_Minus: { + nsCSSValue::Array* array = aValue.GetArrayValue(); + MOZ_ASSERT(array->Count() == 2, "unexpected length"); + int32_t lhsExponent; + int32_t rhsExponent; + if (!GetCalcLengthTypedArithmeticExponent(array->Item(0), lhsExponent) || + !GetCalcLengthTypedArithmeticExponent(array->Item(1), rhsExponent) || + lhsExponent != rhsExponent) { + return false; + } + aExponent = lhsExponent; + return true; + } + + case eCSSUnit_Calc_Times_L: + case eCSSUnit_Calc_Times_R: + case eCSSUnit_Calc_Divided: { + nsCSSValue::Array* array = aValue.GetArrayValue(); + MOZ_ASSERT(array->Count() == 2, "unexpected length"); + int32_t lhsExponent; + int32_t rhsExponent; + if (!GetCalcLengthTypedArithmeticExponent(array->Item(0), lhsExponent) || + !GetCalcLengthTypedArithmeticExponent(array->Item(1), rhsExponent)) { + return false; + } + aExponent = aValue.GetUnit() == eCSSUnit_Calc_Divided + ? lhsExponent - rhsExponent + : lhsExponent + rhsExponent; + return true; + } + + case eCSSUnit_Number: + aExponent = 0; + return true; + + default: + break; + } + + if (aValue.IsLengthUnit()) { + aExponent = 1; + return true; + } + + return false; +} + static bool NormalizeCalcForVariant(nsCSSValue& aValue, uint32_t aPropertyVariantMask, @@ -1728,6 +1787,7 @@ protected: // not be set to false if any nsCSSValues created during parsing can escape // out of the parser. bool mSheetPrincipalRequired; + bool mCalcAllowsTypedArithmetic; // This enum helps us track whether we've unprefixed "display: -webkit-box" // (treating it as "display: flex") in an earlier declaration within a series @@ -1832,6 +1892,7 @@ CSSParserImpl::CSSParserImpl() mInFailingSupportsRule(false), mSuppressErrors(false), mSheetPrincipalRequired(true), + mCalcAllowsTypedArithmetic(false), mWebkitBoxUnprefixState(eNotParsingDecls), mNextFree(nullptr) { @@ -3999,8 +4060,12 @@ CSSParserImpl::ParseMediaQueryExpression(nsMediaQuery* aQuery) bool rv = false; switch (feature->mValueType) { case nsMediaFeature::eLength: - rv = ParseSingleTokenNonNegativeVariant(expr->mValue, VARIANT_LCALC, - nullptr); + { + AutoRestore autoRestore(mCalcAllowsTypedArithmetic); + mCalcAllowsTypedArithmetic = true; + rv = ParseSingleTokenNonNegativeVariant(expr->mValue, VARIANT_LCALC, + nullptr); + } break; case nsMediaFeature::eInteger: case nsMediaFeature::eBoolInteger: @@ -14838,6 +14903,25 @@ CSSParserImpl::ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask, if (!ParseCalcAdditiveExpression(arr->Item(0), resultVariantMask)) break; + if (mCalcAllowsTypedArithmetic) { + int32_t exponent; + if (!GetCalcLengthTypedArithmeticExponent(arr->Item(0), exponent)) { + break; + } + + if (aVariantMask & VARIANT_NUMBER) { + if (exponent == 0) { + resultVariantMask = VARIANT_NUMBER; + } else if (exponent == 1) { + resultVariantMask &= ~int32_t(VARIANT_NUMBER); + } else { + break; + } + } else if (exponent != 1) { + break; + } + } + if (!ExpectSymbol(')', true)) break; @@ -14918,13 +15002,16 @@ CSSParserImpl::ParseCalcMultiplicativeExpression(nsCSSValue& aValue, bool *aHadFinalWS) { MOZ_ASSERT(aVariantMask != 0, "unexpected variant mask"); + bool allowTypedArithmetic = mCalcAllowsTypedArithmetic; bool gotValue = false; // already got the part with the unit bool afterDivision = false; nsCSSValue *storage = &aValue; for (;;) { uint32_t variantMask; - if (afterDivision || gotValue) { + if (allowTypedArithmetic) { + variantMask = aVariantMask | VARIANT_NUMBER; + } else if (afterDivision || gotValue) { variantMask = VARIANT_NUMBER; } else { variantMask = aVariantMask | VARIANT_NUMBER; @@ -14945,7 +15032,7 @@ CSSParserImpl::ParseCalcMultiplicativeExpression(nsCSSValue& aValue, if (number == 0.0 && afterDivision) return false; storage->SetFloatValue(number, eCSSUnit_Number); - } else { + } else if (!allowTypedArithmetic) { gotValue = true; if (storage != &aValue) { @@ -14967,7 +15054,10 @@ CSSParserImpl::ParseCalcMultiplicativeExpression(nsCSSValue& aValue, } nsCSSUnit unit; if (mToken.IsSymbol('*')) { - unit = gotValue ? eCSSUnit_Calc_Times_R : eCSSUnit_Calc_Times_L; + unit = allowTypedArithmetic + ? eCSSUnit_Calc_Times_R + : (gotValue ? eCSSUnit_Calc_Times_R + : eCSSUnit_Calc_Times_L); afterDivision = false; } else if (mToken.IsSymbol('/')) { unit = eCSSUnit_Calc_Divided; @@ -14986,6 +15076,23 @@ CSSParserImpl::ParseCalcMultiplicativeExpression(nsCSSValue& aValue, // Adjust aVariantMask (see comments above function) to reflect which // option we took. + if (allowTypedArithmetic) { + int32_t exponent; + if (!GetCalcLengthTypedArithmeticExponent(aValue, exponent)) { + return false; + } + + if (aVariantMask & VARIANT_NUMBER) { + if (exponent == 0) { + aVariantMask = VARIANT_NUMBER; + } else { + aVariantMask &= ~int32_t(VARIANT_NUMBER); + } + } + + return true; + } + if (aVariantMask & VARIANT_NUMBER) { if (gotValue) { aVariantMask &= ~int32_t(VARIANT_NUMBER); diff --git a/layout/style/test/test_media_queries.html b/layout/style/test/test_media_queries.html index a6d10e3fd5..fc5da98f9d 100644 --- a/layout/style/test/test_media_queries.html +++ b/layout/style/test/test_media_queries.html @@ -315,6 +315,21 @@ function run() { (Math.floor(value/em_size) - 1) + "rem)"); } + change_state(function() { + iframe_style.width = "100px"; + iframe_style.height = "10px"; + }); + should_apply("all and (width: calc(100px / 1em * 1em))"); + should_apply("all and (width: calc((50vh * 5em) / 4px))"); + should_apply("all and (width: calc(10vw / 10px * 1000vh))"); + should_apply("all and (width: calc(8000vh * 1vw / 1em * 8px / 40vh))"); + should_not_apply("all and (width: calc(100px / 1em * 2em))"); + + change_state(function() { + iframe_style.width = width_val + "px"; + iframe_style.height = height_val + "px"; + }); + change_state(function() { iframe_style.width = "0"; }); From e85f7787085b966c8d0a161f8b4002cdd49beccb Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Wed, 6 May 2026 08:15:05 -0400 Subject: [PATCH 10/30] Issue #1826 - Serialize special calc() number values --- layout/style/nsCSSParser.cpp | 181 +++++++++++++++++- layout/style/nsCSSValue.cpp | 28 ++- layout/style/nsCSSValue.h | 2 - layout/style/nsRuleNode.cpp | 38 ++++ .../style/test/test_calc_numeric_types.html | 38 ++++ 5 files changed, 273 insertions(+), 14 deletions(-) diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index a75d21ab3f..94d61daf65 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -262,6 +262,49 @@ RoundFloatToCSSInteger(float aValue) return int32_t(rounded); } +static bool +IsCalcSpecialNumberIdent(const nsAString& aIdent, float& aValue) +{ + if (aIdent.LowerCaseEqualsLiteral("nan")) { + aValue = std::numeric_limits::quiet_NaN(); + return true; + } + + if (aIdent.LowerCaseEqualsLiteral("infinity")) { + aValue = std::numeric_limits::infinity(); + return true; + } + + if (aIdent.LowerCaseEqualsLiteral("-infinity")) { + aValue = -std::numeric_limits::infinity(); + return true; + } + + return false; +} + +static bool +IsCalcNumberFunctionName(const nsAString& aIdent) +{ + return aIdent.LowerCaseEqualsLiteral("min") || + aIdent.LowerCaseEqualsLiteral("max") || + aIdent.LowerCaseEqualsLiteral("clamp"); +} + +static void +WrapCalcNumberValue(nsCSSValue& aValue, float aNumber) +{ + RefPtr arr = nsCSSValue::Array::Create(1); + arr->Item(0).SetFloatValue(aNumber, eCSSUnit_Number); + aValue.SetArrayValue(arr, eCSSUnit_Calc); +} + +static bool +IsFiniteCalcNumber(float aValue) +{ + return !std::isnan(aValue) && !std::isinf(aValue); +} + static bool GetCalcLengthTypedArithmeticExponent(const nsCSSValue& aValue, int32_t& aExponent) @@ -324,7 +367,8 @@ GetCalcLengthTypedArithmeticExponent(const nsCSSValue& aValue, static bool NormalizeCalcForVariant(nsCSSValue& aValue, uint32_t aPropertyVariantMask, - uint32_t aResultVariantMask) + uint32_t aResultVariantMask, + bool aSawSpecialNumericValues = false) { if (ShouldPreserveCalcValue(aPropertyVariantMask)) { return true; @@ -335,7 +379,13 @@ NormalizeCalcForVariant(nsCSSValue& aValue, float value = mozilla::css::ComputeCalc(aValue, ops); if (aPropertyVariantMask & VARIANT_INTEGER) { + if (!IsFiniteCalcNumber(value)) { + return false; + } aValue.SetIntValue(RoundFloatToCSSInteger(value), eCSSUnit_Integer); + } else if ((aPropertyVariantMask & VARIANT_OPACITY) && + aSawSpecialNumericValues) { + WrapCalcNumberValue(aValue, value); } else { aValue.SetFloatValue(value, eCSSUnit_Number); } @@ -347,7 +397,11 @@ NormalizeCalcForVariant(nsCSSValue& aValue, float value = mozilla::css::ComputeCalc(aValue, ops); if (aPropertyVariantMask & VARIANT_OPACITY) { - aValue.SetFloatValue(value, eCSSUnit_Number); + if (aSawSpecialNumericValues) { + WrapCalcNumberValue(aValue, value); + } else { + aValue.SetFloatValue(value, eCSSUnit_Number); + } } else { aValue.SetPercentValue(value); } @@ -1187,13 +1241,16 @@ protected: bool ParseBorderWidth(); bool ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask, - uint32_t* aResultVariantMask = nullptr); + uint32_t* aResultVariantMask = nullptr, + bool* aSawSpecialNumericValues = nullptr); bool ParseCalcAdditiveExpression(nsCSSValue& aValue, uint32_t& aVariantMask); bool ParseCalcMultiplicativeExpression(nsCSSValue& aValue, uint32_t& aVariantMask, bool *aHadFinalWS); bool ParseCalcTerm(nsCSSValue& aValue, uint32_t& aVariantMask); + bool ParseCalcNumberExpressionValue(float& aValue); + bool ParseCalcNumberFunction(nsCSSValue& aValue, uint32_t& aVariantMask); bool RequireWhitespace(); // For "flex" shorthand property, defined in CSS Flexbox spec @@ -1788,6 +1845,7 @@ protected: // out of the parser. bool mSheetPrincipalRequired; bool mCalcAllowsTypedArithmetic; + bool mCalcHasSpecialNumericValues; // This enum helps us track whether we've unprefixed "display: -webkit-box" // (treating it as "display: flex") in an earlier declaration within a series @@ -1893,6 +1951,7 @@ CSSParserImpl::CSSParserImpl() mSuppressErrors(false), mSheetPrincipalRequired(true), mCalcAllowsTypedArithmetic(false), + mCalcHasSpecialNumericValues(false), mWebkitBoxUnprefixState(eNotParsingDecls), mNextFree(nullptr) { @@ -9176,8 +9235,11 @@ CSSParserImpl::ParseVariant(nsCSSValue& aValue, !ShouldPreserveCalcValue(calcVariantMask)) && IsCalcFunctionToken(*tk)) { uint32_t calcResultVariantMask = calcVariantMask; - if (!ParseCalc(aValue, calcVariantMask, &calcResultVariantMask) || - !NormalizeCalcForVariant(aValue, aVariantMask, calcResultVariantMask)) { + bool sawSpecialNumericValues = false; + if (!ParseCalc(aValue, calcVariantMask, &calcResultVariantMask, + &sawSpecialNumericValues) || + !NormalizeCalcForVariant(aValue, aVariantMask, calcResultVariantMask, + sawSpecialNumericValues)) { return CSSParseResult::Error; } return CSSParseResult::Ok; @@ -14883,7 +14945,8 @@ CSSParserImpl::ParseBorderColors(nsCSSPropertyID aProperty) // Parse the top level of a calc() expression. bool CSSParserImpl::ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask, - uint32_t* aResultVariantMask) + uint32_t* aResultVariantMask, + bool* aSawSpecialNumericValues) { // Parsing calc expressions requires, in a number of cases, looking // for a token that is *either* a value of the property or a number. @@ -14892,7 +14955,9 @@ CSSParserImpl::ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask, MOZ_ASSERT(aVariantMask != 0, "unexpected variant mask"); bool oldUnitlessLengthQuirk = mUnitlessLengthQuirk; + bool oldCalcHasSpecialNumericValues = mCalcHasSpecialNumericValues; mUnitlessLengthQuirk = false; + mCalcHasSpecialNumericValues = false; // One-iteration loop so we can break to the error-handling case. do { @@ -14929,11 +14994,21 @@ CSSParserImpl::ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask, if (aResultVariantMask) { *aResultVariantMask = resultVariantMask; } + if (aSawSpecialNumericValues) { + *aSawSpecialNumericValues = mCalcHasSpecialNumericValues; + } + mCalcHasSpecialNumericValues = + oldCalcHasSpecialNumericValues || mCalcHasSpecialNumericValues; mUnitlessLengthQuirk = oldUnitlessLengthQuirk; return true; } while (false); SkipUntil(')'); + if (aSawSpecialNumericValues) { + *aSawSpecialNumericValues = mCalcHasSpecialNumericValues; + } + mCalcHasSpecialNumericValues = + oldCalcHasSpecialNumericValues || mCalcHasSpecialNumericValues; mUnitlessLengthQuirk = oldUnitlessLengthQuirk; return false; } @@ -15134,6 +15209,24 @@ CSSParserImpl::ParseCalcTerm(nsCSSValue& aValue, uint32_t& aVariantMask) } return true; } + if (mToken.mType == eCSSToken_Function && + IsCalcNumberFunctionName(mToken.mIdent)) { + if (!ParseCalcNumberFunction(aValue, aVariantMask)) { + SkipUntil(')'); + return false; + } + return true; + } + if ((aVariantMask & VARIANT_NUMBER) != 0 && + mToken.mType == eCSSToken_Ident) { + float specialValue; + if (IsCalcSpecialNumberIdent(mToken.mIdent, specialValue)) { + mCalcHasSpecialNumericValues = true; + aValue.SetFloatValue(specialValue, eCSSUnit_Number); + aVariantMask = VARIANT_NUMBER; + return true; + } + } // ... or just a value UngetToken(); // Always pass VARIANT_NUMBER to ParseVariant so that unitless zero @@ -15158,6 +15251,82 @@ CSSParserImpl::ParseCalcTerm(nsCSSValue& aValue, uint32_t& aVariantMask) return true; } +bool +CSSParserImpl::ParseCalcNumberExpressionValue(float& aValue) +{ + nsCSSValue expression; + uint32_t variantMask = VARIANT_NUMBER; + if (!ParseCalcAdditiveExpression(expression, variantMask) || + variantMask != VARIANT_NUMBER) { + return false; + } + + ReduceNumberCalcOps ops; + aValue = mozilla::css::ComputeCalc(expression, ops); + return true; +} + +bool +CSSParserImpl::ParseCalcNumberFunction(nsCSSValue& aValue, + uint32_t& aVariantMask) +{ + MOZ_ASSERT(mToken.mType == eCSSToken_Function, "expected function token"); + MOZ_ASSERT(IsCalcNumberFunctionName(mToken.mIdent), + "unexpected calc() number function"); + + float result; + + if (mToken.mIdent.LowerCaseEqualsLiteral("clamp")) { + float minValue; + float centerValue; + float maxValue; + if (!ParseCalcNumberExpressionValue(minValue) || + !ExpectSymbol(',', true) || + !ParseCalcNumberExpressionValue(centerValue) || + !ExpectSymbol(',', true) || + !ParseCalcNumberExpressionValue(maxValue) || + !ExpectSymbol(')', true)) { + return false; + } + + if (std::isnan(minValue) || std::isnan(centerValue) || + std::isnan(maxValue)) { + result = std::numeric_limits::quiet_NaN(); + } else { + result = std::max(minValue, std::min(centerValue, maxValue)); + } + } else { + const bool isMax = mToken.mIdent.LowerCaseEqualsLiteral("max"); + bool sawComma = false; + + if (!ParseCalcNumberExpressionValue(result)) { + return false; + } + + while (ExpectSymbol(',', true)) { + sawComma = true; + float candidate; + if (!ParseCalcNumberExpressionValue(candidate)) { + return false; + } + if (std::isnan(result) || std::isnan(candidate)) { + result = std::numeric_limits::quiet_NaN(); + } else { + result = isMax ? std::max(result, candidate) + : std::min(result, candidate); + } + } + + if (!sawComma || !ExpectSymbol(')', true)) { + return false; + } + } + + aValue.SetFloatValue(result, eCSSUnit_Number); + aVariantMask = VARIANT_NUMBER; + return true; +} + // This function consumes all consecutive whitespace and returns whether // there was any. bool diff --git a/layout/style/nsCSSValue.cpp b/layout/style/nsCSSValue.cpp index 046f7cb108..0ae9720522 100644 --- a/layout/style/nsCSSValue.cpp +++ b/layout/style/nsCSSValue.cpp @@ -6,6 +6,7 @@ /* representation of simple property values within CSS declarations */ #include "mozilla/ArrayUtils.h" +#include "mozilla/FloatingPoint.h" #include "nsCSSValue.h" @@ -29,6 +30,25 @@ using namespace mozilla; using namespace mozilla::css; +static void +AppendSerializedCSSFloat(float aValue, nsAString& aResult) +{ + if (mozilla::IsNaN(aValue)) { + aResult.AppendLiteral("NaN"); + return; + } + + if (mozilla::IsInfinite(aValue)) { + if (aValue < 0.0f) { + aResult.Append('-'); + } + aResult.AppendLiteral("infinity"); + return; + } + + aResult.AppendFloat(aValue); +} + static bool IsLocalRefURL(nsStringBuffer* aString) { @@ -67,7 +87,6 @@ nsCSSValue::nsCSSValue(float aValue, nsCSSUnit aUnit) MOZ_ASSERT(eCSSUnit_Percent <= aUnit, "not a float value"); if (eCSSUnit_Percent <= aUnit) { mValue.mFloat = aValue; - MOZ_ASSERT(!mozilla::IsNaN(mValue.mFloat)); } else { mUnit = eCSSUnit_Null; @@ -146,7 +165,6 @@ nsCSSValue::nsCSSValue(const nsCSSValue& aCopy) } else if (eCSSUnit_Percent <= mUnit) { mValue.mFloat = aCopy.mValue.mFloat; - MOZ_ASSERT(!mozilla::IsNaN(mValue.mFloat)); } else if (UnitHasStringValue()) { mValue.mString = aCopy.mValue.mString; @@ -483,7 +501,6 @@ void nsCSSValue::SetPercentValue(float aValue) Reset(); mUnit = eCSSUnit_Percent; mValue.mFloat = aValue; - MOZ_ASSERT(!mozilla::IsNaN(mValue.mFloat)); } void nsCSSValue::SetFloatValue(float aValue, nsCSSUnit aUnit) @@ -493,7 +510,6 @@ void nsCSSValue::SetFloatValue(float aValue, nsCSSUnit aUnit) if (IsFloatUnit(aUnit)) { mUnit = aUnit; mValue.mFloat = aValue; - MOZ_ASSERT(!mozilla::IsNaN(mValue.mFloat)); } } @@ -1973,10 +1989,10 @@ nsCSSValue::AppendToString(nsCSSPropertyID aProperty, nsAString& aResult, aResult.Append(')'); } else if (eCSSUnit_Percent == unit) { - aResult.AppendFloat(GetPercentValue() * 100.0f); + AppendSerializedCSSFloat(GetPercentValue() * 100.0f, aResult); } else if (eCSSUnit_Percent < unit) { // length unit - aResult.AppendFloat(GetFloatValue()); + AppendSerializedCSSFloat(GetFloatValue(), aResult); } else if (eCSSUnit_Gradient == unit) { nsCSSValueGradient* gradient = GetGradientValue(); diff --git a/layout/style/nsCSSValue.h b/layout/style/nsCSSValue.h index ae50355147..a6a118b113 100644 --- a/layout/style/nsCSSValue.h +++ b/layout/style/nsCSSValue.h @@ -773,7 +773,6 @@ public: float GetFloatValue() const { MOZ_ASSERT(eCSSUnit_Number <= mUnit, "not a float value"); - MOZ_ASSERT(!mozilla::IsNaN(mValue.mFloat)); return mValue.mFloat; } @@ -2026,4 +2025,3 @@ private: } // namespace mozilla #endif /* nsCSSValue_h___ */ - diff --git a/layout/style/nsRuleNode.cpp b/layout/style/nsRuleNode.cpp index 93a754d4e7..8826c8420e 100644 --- a/layout/style/nsRuleNode.cpp +++ b/layout/style/nsRuleNode.cpp @@ -13,6 +13,7 @@ #include "mozilla/ArrayUtils.h" #include "mozilla/Assertions.h" #include "mozilla/DebugOnly.h" +#include "mozilla/FloatingPoint.h" #include "mozilla/Function.h" #include "mozilla/dom/AnimationEffectReadOnlyBinding.h" // for PlaybackDirection #include "mozilla/Likely.h" @@ -1873,6 +1874,17 @@ SetValue(const nsCSSValue& aValue, FieldT& aField, #define SETFCT_UNSET_INHERIT 0x00400000 #define SETFCT_UNSET_INITIAL 0x00800000 +struct RuleNodeReduceNumberCalcOps : public css::BasicFloatCalcOps, + public css::CSSValueInputCalcOps, + public css::NumbersAlreadyNormalizedOps +{ + result_type ComputeLeafValue(const nsCSSValue& aValue) + { + MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_Number, "unexpected unit"); + return aValue.GetFloatValue(); + } +}; + static void SetFactor(const nsCSSValue& aValue, float& aField, RuleNodeCacheConditions& aConditions, float aParentValue, float aInitialValue, uint32_t aFlags = 0) @@ -1883,6 +1895,9 @@ SetFactor(const nsCSSValue& aValue, float& aField, RuleNodeCacheConditions& aCon case eCSSUnit_Number: aField = aValue.GetFloatValue(); + if (mozilla::IsNaN(aField)) { + aField = 0.0f; + } if (aFlags & SETFCT_POSITIVE) { NS_ASSERTION(aField >= 0.0f, "negative value for positive-only property"); if (aField < 0.0f) { @@ -1899,6 +1914,29 @@ SetFactor(const nsCSSValue& aValue, float& aField, RuleNodeCacheConditions& aCon } return; + case eCSSUnit_Calc: { + RuleNodeReduceNumberCalcOps ops; + aField = css::ComputeCalc(aValue, ops); + if (mozilla::IsNaN(aField)) { + aField = 0.0f; + } + if (aFlags & SETFCT_POSITIVE) { + NS_ASSERTION(aField >= 0.0f, "negative value for positive-only property"); + if (aField < 0.0f) { + aField = 0.0f; + } + } + if (aFlags & SETFCT_OPACITY) { + if (aField < 0.0f) { + aField = 0.0f; + } + if (aField > 1.0f) { + aField = 1.0f; + } + } + return; + } + case eCSSUnit_Inherit: aConditions.SetUncacheable(); aField = aParentValue; diff --git a/layout/style/test/test_calc_numeric_types.html b/layout/style/test/test_calc_numeric_types.html index 267136dfe2..3d27cee31b 100644 --- a/layout/style/test/test_calc_numeric_types.html +++ b/layout/style/test/test_calc_numeric_types.html @@ -106,6 +106,44 @@ function parse2dMatrix(transformValue) { div.remove(); })(); + +(function testSpecialNumberCalcSpecifiedSerialization() { + const div = appendTestNode(); + const cases = [ + ["calc(NaN)", "calc(NaN)"], + ["calc(infinity)", "calc(infinity)"], + ["calc(-infinity)", "calc(-infinity)"], + ["calc(1 * NaN)", "calc(NaN)"], + ["calc(1 * infinity / infinity)", "calc(NaN)"], + ["calc(1 * 0 * infinity)", "calc(NaN)"], + ["calc(1 * (infinity + -infinity))", "calc(NaN)"], + ["calc(1 * (infinity - infinity))", "calc(NaN)"], + ["calc(1 * infinity)", "calc(infinity)"], + ["calc(1 * -infinity)", "calc(-infinity)"], + ["calc(1 * 1/infinity)", "calc(0)"], + ["calc(1 * infinity * infinity)", "calc(infinity)"], + ["calc(1 * max(INFinity*3, 0))", "calc(infinity)"], + ["calc(1 * min(inFInity*4, 0))", "calc(0)"], + ["calc(1 * max(nAn*2, 0))", "calc(NaN)"], + ["calc(1 * clamp(-INFINITY*20, 0, infiniTY*10))", "calc(0)"], + ["calc(1 * max(NaN, min(0,10)))", "calc(NaN)"], + ["calc(1 * clamp(NaN, 0, 10))", "calc(NaN)"], + ["calc(1 * max(0, min(10, NaN)))", "calc(NaN)"], + ["calc(1 * clamp(0, 10, NaN))", "calc(NaN)"], + ["calc(1 * max(0, min(NaN, 10)))", "calc(NaN)"], + ["calc(1 * clamp(0, NaN, 10))", "calc(NaN)"], + ["calc(1 * clamp(-Infinity, 0, infinity))", "calc(0)"], + ["calc(1 * clamp(-inFinity, infinity, 10))", "calc(10)"], + ]; + + for (const [input, expected] of cases) { + div.style.setProperty("opacity", input, ""); + is(div.style.getPropertyValue("opacity"), expected, + `opacity should serialize ${input} as ${expected}`); + } + + div.remove(); +})(); From 20b2b3b9f53510f6625080286fea21f602db09ee Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 8 May 2026 14:29:43 -0400 Subject: [PATCH 11/30] Issue #2982 - Follow-up: allow color-mix to work with oklab and oklch Tag #2489 and #3003 --- layout/style/nsCSSNonSRGBColorSpace.h | 206 ++++++++++++++++++--- layout/style/nsCSSParser.cpp | 38 ++-- layout/style/nsCSSValue.cpp | 57 +++++- layout/style/nsCSSValue.h | 17 +- layout/style/nsRuleNode.cpp | 172 ++++++++++++++++- layout/style/test/property_database.js | 4 + layout/style/test/test_computed_style.html | 13 +- 7 files changed, 451 insertions(+), 56 deletions(-) diff --git a/layout/style/nsCSSNonSRGBColorSpace.h b/layout/style/nsCSSNonSRGBColorSpace.h index 4ce2e56a1b..e565649b71 100644 --- a/layout/style/nsCSSNonSRGBColorSpace.h +++ b/layout/style/nsCSSNonSRGBColorSpace.h @@ -6,6 +6,7 @@ #ifndef nsCSSNonSRGBColorSpace_h___ #define nsCSSNonSRGBColorSpace_h___ +#include #include #include "mozilla/MathAlgorithms.h" @@ -18,7 +19,46 @@ namespace css { static constexpr float kLabLightnessMax = 100.0f; static constexpr float kLchPercentScaleC = 150.0f; static constexpr float kOklabPercentScaleAB = 0.4f; +static constexpr float kOklchPowerlessChromaEpsilon = 0.000004f; static constexpr double kRadiansPerDegree = 0.01745329251994329576923690768489; +static constexpr double kDegreesPerRadian = 57.295779513082320876798154814105; + +struct OklabColor { + float mL; + float mA; + float mB; +}; + +struct OklchColor { + float mL; + float mChroma; + float mHue; +}; + +struct LinearSRGBColor { + float mR; + float mG; + float mB; +}; + +inline float +NormalizeHue(float aHue) +{ + float hue = std::fmod(aHue, 360.0f); + if (hue < 0.0f) { + hue += 360.0f; + } + return hue; +} + +inline float +EncodedSRGBToLinear(float aValue) +{ + if (aValue <= 0.04045f) { + return aValue / 12.92f; + } + return std::pow((aValue + 0.055f) / 1.055f, 2.4f); +} inline float LinearSRGBToEncoded(float aValue) @@ -44,43 +84,159 @@ LinearSRGBToColor(float aLinearR, float aLinearG, float aLinearB, aAlpha); } -inline nscolor -OklabToSRGBColor(float aL, float aA, float aB, float aAlpha) +inline LinearSRGBColor +OklabToLinearSRGB(float aL, float aA, float aB) { - // Per CSS Color, the lightness component for Oklab/Oklch is clamped. - float lightness = mozilla::clamped(aL, 0.0f, 1.0f); - uint8_t alpha = - nsStyleUtil::FloatToColorComponent(mozilla::clamped(aAlpha, 0.0f, 1.0f)); - - // Treat values extremely close to zero as zero to avoid tiny floating-point - // representation differences for percentage inputs. - static constexpr float kLightnessEndpointEpsilon = 0.000002f; - - if (lightness <= kLightnessEndpointEpsilon) { - return NS_RGBA(0, 0, 0, alpha); - } - - float lRoot = lightness + 0.3963377774f * aA + 0.2158037573f * aB; - float mRoot = lightness - 0.1055613458f * aA - 0.0638541728f * aB; - float sRoot = lightness - 0.0894841775f * aA - 1.2914855480f * aB; + float lRoot = aL + 0.3963377774f * aA + 0.2158037573f * aB; + float mRoot = aL - 0.1055613458f * aA - 0.0638541728f * aB; + float sRoot = aL - 0.0894841775f * aA - 1.2914855480f * aB; float l = lRoot * lRoot * lRoot; float m = mRoot * mRoot * mRoot; float s = sRoot * sRoot * sRoot; - float linearR = 4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s; - float linearG = -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s; - float linearB = -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s; + return { + 4.0767416361f * l - 3.3077115393f * m + 0.2309699032f * s, + -1.2684379733f * l + 2.6097573493f * m - 0.3413193760f * s, + -0.0041960761f * l - 0.7034186179f * m + 1.7076146941f * s + }; +} - return LinearSRGBToColor(linearR, linearG, linearB, alpha); +inline OklabColor +LinearSRGBToOklabColor(float aLinearR, float aLinearG, float aLinearB) +{ + float l = std::cbrt(0.4122214695f * aLinearR + + 0.5363325373f * aLinearG + + 0.0514459933f * aLinearB); + float m = std::cbrt(0.2119034958f * aLinearR + + 0.6806995506f * aLinearG + + 0.1073969535f * aLinearB); + float s = std::cbrt(0.0883024592f * aLinearR + + 0.2817188391f * aLinearG + + 0.6299787017f * aLinearB); + + return { + 0.2104542683f * l + 0.7936177747f * m - 0.0040720430f * s, + 1.9779985324f * l - 2.4285922420f * m + 0.4505937096f * s, + 0.0259040425f * l + 0.7827717125f * m - 0.8086757549f * s + }; +} + +inline OklabColor +SRGBToOklabColor(nscolor aColor) +{ + float linearR = EncodedSRGBToLinear(NS_GET_R(aColor) / 255.0f); + float linearG = EncodedSRGBToLinear(NS_GET_G(aColor) / 255.0f); + float linearB = EncodedSRGBToLinear(NS_GET_B(aColor) / 255.0f); + + return LinearSRGBToOklabColor(linearR, linearG, linearB); +} + +inline OklchColor +OklabToOklchColor(const OklabColor& aColor) +{ + float chroma = std::sqrt(aColor.mA * aColor.mA + aColor.mB * aColor.mB); + float hue = std::atan2(aColor.mB, aColor.mA) * kDegreesPerRadian; + if (hue < 0.0f) { + hue += 360.0f; + } + + return { aColor.mL, chroma, hue }; +} + +inline nscolor +OklabToSRGBColor(float aL, float aA, float aB, float aAlpha) +{ + // Per CSS Color, the lightness component for Oklab/Oklch is clamped at + // parsed-value time. + float lightness = mozilla::clamped(aL, 0.0f, 1.0f); + uint8_t alpha = + nsStyleUtil::FloatToColorComponent(mozilla::clamped(aAlpha, 0.0f, 1.0f)); + + OklabColor mapped = { lightness, aA, aB }; + auto isInSRGBGamut = [](const OklabColor& aColor) { + LinearSRGBColor linear = + OklabToLinearSRGB(aColor.mL, aColor.mA, aColor.mB); + return linear.mR >= 0.0f && linear.mR <= 1.0f && + linear.mG >= 0.0f && linear.mG <= 1.0f && + linear.mB >= 0.0f && linear.mB <= 1.0f; + }; + auto clippedOklab = [](const OklabColor& aColor) { + LinearSRGBColor linear = + OklabToLinearSRGB(aColor.mL, aColor.mA, aColor.mB); + return LinearSRGBToOklabColor( + mozilla::clamped(linear.mR, 0.0f, 1.0f), + mozilla::clamped(linear.mG, 0.0f, 1.0f), + mozilla::clamped(linear.mB, 0.0f, 1.0f)); + }; + auto deltaEOK = [](const OklabColor& aColor1, const OklabColor& aColor2) { + float deltaL = aColor1.mL - aColor2.mL; + float deltaA = aColor1.mA - aColor2.mA; + float deltaB = aColor1.mB - aColor2.mB; + return std::sqrt(deltaL * deltaL + deltaA * deltaA + deltaB * deltaB); + }; + + if (lightness <= 0.0f) { + mapped = { 0.0f, 0.0f, 0.0f }; + } else if (lightness >= 1.0f) { + mapped = { 1.0f, 0.0f, 0.0f }; + } else if (!isInSRGBGamut(mapped)) { + // CSS Color 4 binary search gamut mapping with local MINDE, targeting sRGB. + static constexpr float kJND = 0.02f; + static constexpr float kGamutMapEpsilon = 0.0001f; + + OklchColor origin = OklabToOklchColor(mapped); + OklabColor clipped = clippedOklab(mapped); + float delta = deltaEOK(clipped, mapped); + + if (delta < kJND) { + mapped = clipped; + } else { + float min = 0.0f; + float max = origin.mChroma; + bool minInGamut = true; + OklabColor current = mapped; + + while (max - min > kGamutMapEpsilon) { + float chroma = (min + max) / 2.0f; + current.mL = origin.mL; + current.mA = chroma * std::cos(origin.mHue * kRadiansPerDegree); + current.mB = chroma * std::sin(origin.mHue * kRadiansPerDegree); + + if (minInGamut && isInSRGBGamut(current)) { + min = chroma; + continue; + } + + clipped = clippedOklab(current); + delta = deltaEOK(clipped, current); + if (delta < kJND) { + if (kJND - delta < kGamutMapEpsilon) { + mapped = clipped; + break; + } + minInGamut = false; + min = chroma; + } else { + max = chroma; + } + mapped = clipped; + } + } + } + + LinearSRGBColor linear = + OklabToLinearSRGB(mapped.mL, mapped.mA, mapped.mB); + return LinearSRGBToColor(linear.mR, linear.mG, linear.mB, alpha); } inline nscolor OklchToSRGBColor(float aL, float aChroma, float aHue, float aAlpha) { - double hueRadians = aHue * kRadiansPerDegree; - float a = aChroma * std::cos(hueRadians); - float b = aChroma * std::sin(hueRadians); + float chroma = std::max(aChroma, 0.0f); + double hueRadians = NormalizeHue(aHue) * kRadiansPerDegree; + float a = chroma * std::cos(hueRadians); + float b = chroma * std::sin(hueRadians); return OklabToSRGBColor(aL, a, b, aAlpha); } diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index 94d61daf65..2b7267b2b7 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -1504,8 +1504,9 @@ protected: bool ParseHSLColor(float& aHue, float& aSaturation, float& aLightness, float& aOpacity); bool ParseLCHColor(nscolor& aColor); - bool ParseOKLabColor(nscolor& aColor); - bool ParseOKLCHColor(nscolor& aColor); + bool ParseOKLabColor(float& aL, float& aA, float& aB, float& aAlpha); + bool ParseOKLCHColor(float& aL, float& aChroma, float& aHue, + float& aAlpha); bool ParseOKLabComponent(float& aComponent, float aPercentScale, Maybe aSeparator); @@ -7748,7 +7749,7 @@ CSSParserImpl::ParseColor(nsCSSValue& aValue) return CSSParseResult::Error; } - // Check for supported color spaces: srgb or hsl + // Check for supported color spaces. if (!GetToken(true) || mToken.mType != eCSSToken_Ident) { SkipUntil(')'); return CSSParseResult::Error; @@ -7759,6 +7760,10 @@ CSSParserImpl::ParseColor(nsCSSValue& aValue) colorSpace = mozilla::css::ColorMixColorSpace::sRGB; } else if (mToken.mIdent.LowerCaseEqualsLiteral("hsl")) { colorSpace = mozilla::css::ColorMixColorSpace::HSL; + } else if (mToken.mIdent.LowerCaseEqualsLiteral("oklab")) { + colorSpace = mozilla::css::ColorMixColorSpace::Oklab; + } else if (mToken.mIdent.LowerCaseEqualsLiteral("oklch")) { + colorSpace = mozilla::css::ColorMixColorSpace::Oklch; } else { SkipUntil(')'); return CSSParseResult::Error; @@ -7907,16 +7912,19 @@ CSSParserImpl::ParseColor(nsCSSValue& aValue) return CSSParseResult::Error; } else if (mToken.mIdent.LowerCaseEqualsLiteral("oklab")) { - if (ParseOKLabColor(rgba)) { - aValue.SetColorValue(rgba); + float l, a, b, alpha; + if (ParseOKLabColor(l, a, b, alpha)) { + aValue.SetFloatColorValue(l, a, b, alpha, eCSSUnit_OklabColor); return CSSParseResult::Ok; } SkipUntil(')'); return CSSParseResult::Error; } else if (mToken.mIdent.LowerCaseEqualsLiteral("oklch")) { - if (ParseOKLCHColor(rgba)) { - aValue.SetColorValue(rgba); + float l, chroma, hue, alpha; + if (ParseOKLCHColor(l, chroma, hue, alpha)) { + aValue.SetFloatColorValue(l, chroma, hue, alpha, + eCSSUnit_OklchColor); return CSSParseResult::Ok; } SkipUntil(')'); @@ -8168,7 +8176,8 @@ CSSParserImpl::ParseLCHColor(nscolor& aColor) } bool -CSSParserImpl::ParseOKLabColor(nscolor& aColor) +CSSParserImpl::ParseOKLabColor(float& aL, float& aA, float& aB, + float& aAlpha) { const char commaSeparator = ','; float l, a, b, alpha; @@ -8186,12 +8195,16 @@ CSSParserImpl::ParseOKLabColor(nscolor& aColor) return false; } - aColor = OklabToSRGBColor(l, a, b, alpha); + aL = mozilla::clamped(l, 0.0f, 1.0f); + aA = a; + aB = b; + aAlpha = alpha; return true; } bool -CSSParserImpl::ParseOKLCHColor(nscolor& aColor) +CSSParserImpl::ParseOKLCHColor(float& aL, float& aChroma, float& aHue, + float& aAlpha) { const char commaSeparator = ','; float l, chroma, hue, alpha; @@ -8209,7 +8222,10 @@ CSSParserImpl::ParseOKLCHColor(nscolor& aColor) return false; } - aColor = OklchToSRGBColor(l, chroma, hue, alpha); + aL = mozilla::clamped(l, 0.0f, 1.0f); + aChroma = std::max(chroma, 0.0f); + aHue = NormalizeHue(hue); + aAlpha = alpha; return true; } diff --git a/layout/style/nsCSSValue.cpp b/layout/style/nsCSSValue.cpp index 0ae9720522..4a3e9b1b4f 100644 --- a/layout/style/nsCSSValue.cpp +++ b/layout/style/nsCSSValue.cpp @@ -18,6 +18,7 @@ #include "gfxFontConstants.h" #include "imgIRequest.h" #include "imgRequestProxy.h" +#include "nsCSSNonSRGBColorSpace.h" #include "nsIDocument.h" #include "nsNetUtil.h" #include "nsPresContext.h" @@ -1965,6 +1966,12 @@ nsCSSValue::AppendToString(nsCSSPropertyID aProperty, nsAString& aResult, case mozilla::css::ColorMixColorSpace::HSL: aResult.AppendLiteral("hsl"); break; + case mozilla::css::ColorMixColorSpace::Oklab: + aResult.AppendLiteral("oklab"); + break; + case mozilla::css::ColorMixColorSpace::Oklch: + aResult.AppendLiteral("oklch"); + break; } // append color1 and color2 @@ -2272,6 +2279,8 @@ nsCSSValue::AppendToString(nsCSSPropertyID aProperty, nsAString& aResult, case eCSSUnit_PercentageRGBAColor: break; case eCSSUnit_HSLColor: break; case eCSSUnit_HSLAColor: break; + case eCSSUnit_OklabColor: break; + case eCSSUnit_OklchColor: break; case eCSSUnit_ComplexColor: break; case eCSSUnit_ColorMix: break; case eCSSUnit_Percent: aResult.Append(char16_t('%')); break; @@ -2480,6 +2489,8 @@ nsCSSValue::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const case eCSSUnit_PercentageRGBAColor: case eCSSUnit_HSLColor: case eCSSUnit_HSLAColor: + case eCSSUnit_OklabColor: + case eCSSUnit_OklchColor: n += mValue.mFloatColor->SizeOfIncludingThis(aMallocSizeOf); break; @@ -3476,6 +3487,16 @@ nsCSSValueFloatColor::GetColorValue(nsCSSUnit aUnit) const NSToIntRound(mozilla::clamped(mAlpha, 0.0f, 1.0f) * 255.0f)); } + if (aUnit == eCSSUnit_OklabColor) { + return css::OklabToSRGBColor(mComponent1, mComponent2, mComponent3, + mAlpha); + } + + if (aUnit == eCSSUnit_OklchColor) { + return css::OklchToSRGBColor(mComponent1, mComponent2, mComponent3, + mAlpha); + } + // HSL color MOZ_ASSERT(aUnit == eCSSUnit_HSLColor || aUnit == eCSSUnit_HSLAColor); @@ -3504,8 +3525,14 @@ nsCSSValueFloatColor::AppendToString(nsCSSUnit aUnit, nsAString& aResult) const bool showAlpha = (mAlpha != 1.0f); bool isHSL = (aUnit == eCSSUnit_HSLColor || aUnit == eCSSUnit_HSLAColor); + bool isOklab = aUnit == eCSSUnit_OklabColor; + bool isOklch = aUnit == eCSSUnit_OklchColor; - if (isHSL) { + if (isOklab) { + aResult.AppendLiteral("oklab"); + } else if (isOklch) { + aResult.AppendLiteral("oklch"); + } else if (isHSL) { aResult.AppendLiteral("hsl"); } else { aResult.AppendLiteral("rgb"); @@ -3515,22 +3542,38 @@ nsCSSValueFloatColor::AppendToString(nsCSSUnit aUnit, nsAString& aResult) const } else { aResult.Append('('); } - if (isHSL) { + if (isOklab || isOklch) { + aResult.AppendFloat(mComponent1); + aResult.Append(' '); + aResult.AppendFloat(mComponent2); + aResult.Append(' '); + aResult.AppendFloat(mComponent3); + } else if (isHSL) { aResult.AppendFloat(mComponent1 * 360.0f); aResult.AppendLiteral(", "); } else { aResult.AppendFloat(mComponent1 * 100.0f); aResult.AppendLiteral("%, "); } - aResult.AppendFloat(mComponent2 * 100.0f); - aResult.AppendLiteral("%, "); - aResult.AppendFloat(mComponent3 * 100.0f); - if (showAlpha) { + if (!isOklab && !isOklch) { + aResult.AppendFloat(mComponent2 * 100.0f); aResult.AppendLiteral("%, "); + aResult.AppendFloat(mComponent3 * 100.0f); + } + if (showAlpha) { + if (isOklab || isOklch) { + aResult.AppendLiteral(" / "); + } else { + aResult.AppendLiteral("%, "); + } aResult.AppendFloat(mAlpha); aResult.Append(')'); } else { - aResult.AppendLiteral("%)"); + if (isOklab || isOklch) { + aResult.Append(')'); + } else { + aResult.AppendLiteral("%)"); + } } } diff --git a/layout/style/nsCSSValue.h b/layout/style/nsCSSValue.h index a6a118b113..cf608aaec6 100644 --- a/layout/style/nsCSSValue.h +++ b/layout/style/nsCSSValue.h @@ -520,8 +520,10 @@ enum nsCSSUnit { // allowed. eCSSUnit_HSLColor = 89, // (nsCSSValueFloatColor*) eCSSUnit_HSLAColor = 90, // (nsCSSValueFloatColor*) - eCSSUnit_ComplexColor = 91, // (ComplexColorValue*) - eCSSUnit_ColorMix = 92, // (ColorMixValue*) + eCSSUnit_OklabColor = 91, // (nsCSSValueFloatColor*) + eCSSUnit_OklchColor = 92, // (nsCSSValueFloatColor*) + eCSSUnit_ComplexColor = 93, // (ComplexColorValue*) + eCSSUnit_ColorMix = 94, // (ColorMixValue*) eCSSUnit_Percent = 100, // (float) 1.0 == 100%) value is percentage of something eCSSUnit_Number = 101, // (float) value is numeric (usually multiplier, different behavior than percent) @@ -733,6 +735,8 @@ public: // eCSSUnit_PercentageRGBAColor -- rgba(%,%,%,float) // eCSSUnit_HSLColor -- hsl(float,%,%) // eCSSUnit_HSLAColor -- hsla(float,%,%,float) + // eCSSUnit_OklabColor -- oklab(float,float,float,float) + // eCSSUnit_OklchColor -- oklch(float,float,float,float) // // - IsNumericColorUnit returns true for any of the above units. // @@ -745,7 +749,7 @@ public: { return eCSSUnit_RGBColor <= aUnit && aUnit <= eCSSUnit_ShortHexColorAlpha; } static bool IsFloatColorUnit(nsCSSUnit aUnit) { return eCSSUnit_PercentageRGBColor <= aUnit && - aUnit <= eCSSUnit_HSLAColor; } + aUnit <= eCSSUnit_OklchColor; } static bool IsNumericColorUnit(nsCSSUnit aUnit) { return IsIntegerColorUnit(aUnit) || IsFloatColorUnit(aUnit); } @@ -1927,6 +1931,9 @@ private: // [0, 1] for hue represents // [0deg, 360deg]. // + // OklabColor stores L in mComponent1, a in mComponent2, b in mComponent3. + // OklchColor stores L in mComponent1, C in mComponent2, H in mComponent3. + // // [-float::max(), float::max()] for PercentageRGBColor, PercentageRGBAColor. // 1.0 means 100%. float mComponent1; @@ -1993,7 +2000,9 @@ namespace css { enum class ColorMixColorSpace { sRGB, - HSL + HSL, + Oklab, + Oklch }; struct ColorMixValue final diff --git a/layout/style/nsRuleNode.cpp b/layout/style/nsRuleNode.cpp index 8826c8420e..aa5c02bd6f 100644 --- a/layout/style/nsRuleNode.cpp +++ b/layout/style/nsRuleNode.cpp @@ -43,6 +43,7 @@ #include "nsIStyleRule.h" #include "nsBidiUtils.h" #include "nsStyleStructInlines.h" +#include "nsCSSNonSRGBColorSpace.h" #include "nsCSSProps.h" #include "nsTArray.h" #include "nsContentUtils.h" @@ -1152,6 +1153,137 @@ SetPairCoords(const nsCSSValue& aValue, return cX; } +static void +GetColorMixPremultipliedWeights(float aAlpha1, float aAlpha2, + float aWeight1, float aWeight2, + float& aPremultipliedWeight1, + float& aPremultipliedWeight2, + float& aAlpha) +{ + float alphaWeight1 = aWeight1 * aAlpha1; + float alphaWeight2 = aWeight2 * aAlpha2; + + aAlpha = alphaWeight1 + alphaWeight2; + if (aAlpha <= 0.0f) { + aPremultipliedWeight1 = 0.0f; + aPremultipliedWeight2 = 0.0f; + return; + } + + aPremultipliedWeight1 = alphaWeight1 / aAlpha; + aPremultipliedWeight2 = alphaWeight2 / aAlpha; +} + +static css::OklabColor +OklchToOklabColor(const css::OklchColor& aColor) +{ + float hueRadians = aColor.mHue * css::kRadiansPerDegree; + return { + aColor.mL, + aColor.mChroma * std::cos(hueRadians), + aColor.mChroma * std::sin(hueRadians) + }; +} + +static void +GetColorMixColorComponents(const nsCSSValue& aValue, nscolor aResolvedColor, + css::OklabColor& aOklab, + css::OklchColor& aOklch, + float& aAlpha) +{ + nsCSSUnit unit = aValue.GetUnit(); + if (unit == eCSSUnit_OklabColor) { + const nsCSSValueFloatColor* color = aValue.GetFloatColorValue(); + aOklab = { color->Comp1(), color->Comp2(), color->Comp3() }; + aOklch = css::OklabToOklchColor(aOklab); + aAlpha = mozilla::clamped(color->Alpha(), 0.0f, 1.0f); + return; + } + + if (unit == eCSSUnit_OklchColor) { + const nsCSSValueFloatColor* color = aValue.GetFloatColorValue(); + aOklch = { color->Comp1(), color->Comp2(), color->Comp3() }; + aOklab = OklchToOklabColor(aOklch); + aAlpha = mozilla::clamped(color->Alpha(), 0.0f, 1.0f); + return; + } + + aOklab = css::SRGBToOklabColor(aResolvedColor); + aOklch = css::OklabToOklchColor(aOklab); + aAlpha = NS_GET_A(aResolvedColor) / 255.0f; +} + +static float +InterpolateColorMixHue(float aHue1, float aHue2, float aWeight1, + float aWeight2) +{ + float hueDiff = aHue2 - aHue1; + if (hueDiff > 180.0f) { + aHue1 += 360.0f; + } else if (hueDiff < -180.0f) { + aHue2 += 360.0f; + } + + float hue = aHue1 * aWeight1 + aHue2 * aWeight2; + hue = std::fmod(hue, 360.0f); + if (hue < 0.0f) { + hue += 360.0f; + } + return hue; +} + +static nscolor +MixColorsInOklab(const css::OklabColor& aColor1, float aAlpha1, + const css::OklabColor& aColor2, float aAlpha2, + float aWeight1, float aWeight2, float aAlphaMultiplier) +{ + float premultipliedWeight1, premultipliedWeight2, alpha; + GetColorMixPremultipliedWeights(aAlpha1, aAlpha2, aWeight1, aWeight2, + premultipliedWeight1, premultipliedWeight2, + alpha); + alpha *= aAlphaMultiplier; + if (alpha <= 0.0f) { + return NS_RGBA(0, 0, 0, 0); + } + + return css::OklabToSRGBColor( + aColor1.mL * premultipliedWeight1 + aColor2.mL * premultipliedWeight2, + aColor1.mA * premultipliedWeight1 + aColor2.mA * premultipliedWeight2, + aColor1.mB * premultipliedWeight1 + aColor2.mB * premultipliedWeight2, + alpha); +} + +static nscolor +MixColorsInOklch(css::OklchColor aColor1, float aAlpha1, + css::OklchColor aColor2, float aAlpha2, + float aWeight1, float aWeight2, float aAlphaMultiplier) +{ + if (aColor1.mChroma <= css::kOklchPowerlessChromaEpsilon) { + aColor1.mHue = aColor2.mHue; + } + if (aColor2.mChroma <= css::kOklchPowerlessChromaEpsilon) { + aColor2.mHue = aColor1.mHue; + } + + float premultipliedWeight1, premultipliedWeight2, alpha; + GetColorMixPremultipliedWeights(aAlpha1, aAlpha2, aWeight1, aWeight2, + premultipliedWeight1, premultipliedWeight2, + alpha); + alpha *= aAlphaMultiplier; + if (alpha <= 0.0f) { + return NS_RGBA(0, 0, 0, 0); + } + + float hue = InterpolateColorMixHue(aColor1.mHue, aColor2.mHue, + aWeight1, aWeight2); + return css::OklchToSRGBColor( + aColor1.mL * premultipliedWeight1 + aColor2.mL * premultipliedWeight2, + aColor1.mChroma * premultipliedWeight1 + + aColor2.mChroma * premultipliedWeight2, + hue, + alpha); +} + static bool SetColor(const nsCSSValue& aValue, const nscolor aParentColor, @@ -1240,22 +1372,29 @@ SetColor(const nsCSSValue& aValue, const mozilla::css::ColorMixValue* colorMix = aValue.GetColorMixValue(); if (colorMix) { nscolor color1, color2; + bool resolvedColor1, resolvedColor2; // XXX: This is a hack to avoid recursive calls to SetColor when either color resolves // to NS_COLOR_CURRENTCOLOR, as it would result in re-evaluation of the color. // Instead of recursing, we reach up to set either color to the parent color, instead. if (colorMix->mColor1.GetUnit() == eCSSUnit_EnumColor && colorMix->mColor1.GetIntValue() == NS_COLOR_CURRENTCOLOR) { color1 = aParentColor; + resolvedColor1 = true; } else { - SetColor(colorMix->mColor1, aParentColor, aPresContext, aContext, color1, aConditions); + resolvedColor1 = + SetColor(colorMix->mColor1, aParentColor, aPresContext, aContext, + color1, aConditions); } if (colorMix->mColor2.GetUnit() == eCSSUnit_EnumColor && colorMix->mColor2.GetIntValue() == NS_COLOR_CURRENTCOLOR) { color2 = aParentColor; + resolvedColor2 = true; } else { - SetColor(colorMix->mColor2, aParentColor, aPresContext, aContext, color2, aConditions); + resolvedColor2 = + SetColor(colorMix->mColor2, aParentColor, aPresContext, aContext, + color2, aConditions); } - if (color1 && color2) { + if (resolvedColor1 && resolvedColor2) { // interpolate each RGBA component with proper percentage handling float w1 = colorMix->mWeight1; float w2 = colorMix->mWeight2; @@ -1272,9 +1411,18 @@ SetColor(const nsCSSValue& aValue, w1 = w2 = 0.5f; sum = 1.0f; } + float alphaMultiplier = std::min(sum, 1.0f); float norm1 = w1 / sum; float norm2 = w2 / sum; + + css::OklabColor oklab1, oklab2; + css::OklchColor oklch1, oklch2; + float oklabAlpha1, oklabAlpha2; + GetColorMixColorComponents(colorMix->mColor1, color1, oklab1, + oklch1, oklabAlpha1); + GetColorMixColorComponents(colorMix->mColor2, color2, oklab2, + oklch2, oklabAlpha2); if (colorMix->mColorSpace == mozilla::css::ColorMixColorSpace::HSL) { // HSL color space mixing @@ -1347,10 +1495,23 @@ SetColor(const nsCSSValue& aValue, // Convert back to RGB nscolor hslResult = NS_HSL2RGB(h, s, l); - uint8_t aInt = (uint8_t)mozilla::clamped(a * 255.0f + 0.5f, 0.0f, 255.0f); + uint8_t aInt = (uint8_t)mozilla::clamped( + a * alphaMultiplier * 255.0f + 0.5f, 0.0f, 255.0f); aResult = NS_RGBA(NS_GET_R(hslResult), NS_GET_G(hslResult), NS_GET_B(hslResult), aInt); result = true; + } else if (colorMix->mColorSpace == + mozilla::css::ColorMixColorSpace::Oklab) { + aResult = MixColorsInOklab(oklab1, oklabAlpha1, + oklab2, oklabAlpha2, + norm1, norm2, alphaMultiplier); + result = true; + } else if (colorMix->mColorSpace == + mozilla::css::ColorMixColorSpace::Oklch) { + aResult = MixColorsInOklch(oklch1, oklabAlpha1, + oklch2, oklabAlpha2, + norm1, norm2, alphaMultiplier); + result = true; } else { // sRGB color space mixing with proper alpha premultiplication float r1 = NS_GET_R(color1); @@ -1400,7 +1561,8 @@ SetColor(const nsCSSValue& aValue, uint8_t rInt = (uint8_t)mozilla::clamped(r + 0.5f, 0.0f, 255.0f); uint8_t gInt = (uint8_t)mozilla::clamped(g + 0.5f, 0.0f, 255.0f); uint8_t bInt = (uint8_t)mozilla::clamped(b + 0.5f, 0.0f, 255.0f); - uint8_t aInt = (uint8_t)mozilla::clamped(a * 255.0f + 0.5f, 0.0f, 255.0f); + uint8_t aInt = (uint8_t)mozilla::clamped( + a * alphaMultiplier * 255.0f + 0.5f, 0.0f, 255.0f); aResult = NS_RGBA(rInt, gInt, bInt, aInt); result = true; diff --git a/layout/style/test/property_database.js b/layout/style/test/property_database.js index ef2e39d792..d895cabdac 100644 --- a/layout/style/test/property_database.js +++ b/layout/style/test/property_database.js @@ -4332,6 +4332,10 @@ var gCSSProperties = { "oklab(100% 0 0)", "oklab(60% 0.1 -0.1 / 75%)", "oklch(70% 0.2 180deg / 40%)", + "color-mix(in oklab, red, blue)", + "color-mix(in oklch, red, blue)", + "color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 0.7))", + "color-mix(in oklch, oklch(0.1 0.2 30), oklch(0.5 0.6 70))", ], invalid_values: [ "#f", diff --git a/layout/style/test/test_computed_style.html b/layout/style/test/test_computed_style.html index 55ba9edb30..5363e1dac5 100644 --- a/layout/style/test/test_computed_style.html +++ b/layout/style/test/test_computed_style.html @@ -406,14 +406,19 @@ var noframe_container = document.getElementById("content"); ["lch(150% 50 60 / 1)", "rgb(255, 255, 255)"], ["oklab(0 0 0 / 1)", "rgb(0, 0, 0)"], ["oklab(0 0 0 / 0.5)", "rgba(0, 0, 0, 0.5)"], - ["oklab(150% 0.5 0.2 / 1)", "rgb(255, 0, 0)"], + ["oklab(150% 0.5 0.2 / 1)", "rgb(255, 255, 255)"], ["oklch(0 0 0 / 1)", "rgb(0, 0, 0)"], ["oklch(0 0 0 / 0.5)", "rgba(0, 0, 0, 0.5)"], ["oklch(0.0001% 0.2 45 / 1)", "rgb(0, 0, 0)"], - ["oklch(99.9999% 0.2 45 / 1)", "rgb(255, 207, 132)"], + ["oklch(99.9999% 0.2 45 / 1)", "rgb(255, 251, 242)"], ["oklch(0% 1.1 60 / 1)", "rgb(0, 0, 0)"], - ["oklch(100% 110 60 / 1)", "rgb(0, 255, 0)"], - ["oklch(150% 0.5 50 / 1)", "rgb(255, 0, 0)"], + ["oklch(100% 110 60 / 1)", "rgb(255, 255, 255)"], + ["oklch(150% 0.5 50 / 1)", "rgb(255, 255, 255)"], + ["color-mix(in oklab, red, blue)", "rgb(140, 83, 162)"], + ["color-mix(in oklch, red, blue)", "rgb(183, 0, 190)"], + ["color-mix(in oklab, red, transparent)", "rgba(255, 0, 0, 0.5)"], + ["color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 0.7))", "rgb(83, 24, 0)"], + ["color-mix(in oklch, oklch(0.1 0.2 30), oklch(0.5 0.6 70))", "rgb(84, 23, 0)"], ]; var p = document.createElement("p"); From ee00ac9826322a55c3794953118c3e511768220b Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sat, 9 May 2026 13:13:49 -0400 Subject: [PATCH 12/30] Load mochitest modules without imp --- testing/mochitest/mach_commands.py | 33 +++++++++++++++++++----------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/testing/mochitest/mach_commands.py b/testing/mochitest/mach_commands.py index fb261ec827..4846a23215 100644 --- a/testing/mochitest/mach_commands.py +++ b/testing/mochitest/mach_commands.py @@ -27,6 +27,24 @@ from mach.decorators import ( here = os.path.abspath(os.path.dirname(__file__)) +def load_source_module(module_name, path): + if module_name in sys.modules: + return sys.modules[module_name] + + try: + import importlib.util + spec = importlib.util.spec_from_file_location(module_name, path) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + except ImportError: + import imp + with open(path, 'r') as fh: + return imp.load_module(module_name, fh, path, + ('.py', 'r', imp.PY_SOURCE)) + + ENG_BUILD_REQUIRED = ''' The mochitest command requires an engineering build. It may be the case that VARIANT=user or PRODUCTION=1 were set. Try re-building with VARIANT=eng: @@ -175,11 +193,8 @@ class MochitestRunner(MozbuildObject): """ # runtests.py is ambiguous, so we load the file/module manually. if 'mochitest' not in sys.modules: - import imp path = os.path.join(self.mochitest_dir, 'runtests.py') - with open(path, 'r') as fh: - imp.load_module('mochitest', fh, path, - ('.py', 'r', imp.PY_SOURCE)) + load_source_module('mochitest', path) import mochitest @@ -219,11 +234,8 @@ class MochitestRunner(MozbuildObject): if host_ret != 0: return host_ret - import imp path = os.path.join(self.mochitest_dir, 'runtestsremote.py') - with open(path, 'r') as fh: - imp.load_module('runtestsremote', fh, path, - ('.py', 'r', imp.PY_SOURCE)) + load_source_module('runtestsremote', path) import runtestsremote options = Namespace(**kwargs) @@ -273,11 +285,8 @@ def setup_argument_parser(): with warnings.catch_warnings(): warnings.simplefilter('ignore') - import imp path = os.path.join(build_obj.topobjdir, mochitest_dir, 'runtests.py') - with open(path, 'r') as fh: - imp.load_module('mochitest', fh, path, - ('.py', 'r', imp.PY_SOURCE)) + load_source_module('mochitest', path) from mochitest_options import MochitestArgumentParser From 4337565d3a01201225aca8acac0ef277dcc6678f Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sat, 9 May 2026 13:13:55 -0400 Subject: [PATCH 13/30] Remove stale imagebitmap support file entry --- dom/canvas/test/mochitest.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/dom/canvas/test/mochitest.ini b/dom/canvas/test/mochitest.ini index b9cba0da95..5219f05846 100644 --- a/dom/canvas/test/mochitest.ini +++ b/dom/canvas/test/mochitest.ini @@ -26,7 +26,6 @@ support-files = image_yellow75.png imagebitmap_bug1239300.js imagebitmap_bug1239752.js - imagebitmap_extensions.html imagebitmap_on_worker.js imagebitmap_structuredclone.js imagebitmap_structuredclone_iframe.html From 0d684399b2b0ef2339418ceca7e1beee2fb15aab Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sun, 10 May 2026 13:00:24 -0400 Subject: [PATCH 14/30] Issue #2404 - Enable CSS aspect-ratio sizing --- layout/generic/nsFrame.cpp | 18 ++++-- layout/style/nsCSSParser.cpp | 57 +++++++++++++++++++ layout/style/nsCSSPropList.h | 8 +-- layout/style/nsComputedDOMStyle.cpp | 17 +++++- layout/style/nsComputedDOMStyle.h | 1 + layout/style/nsComputedDOMStylePropertyList.h | 1 + layout/style/nsRuleNode.cpp | 15 +++-- layout/style/test/ListCSSProperties.cpp | 1 - layout/style/test/mochitest.ini | 1 + layout/style/test/property_database.js | 8 +++ .../test/test_aspect_ratio_property.html | 41 +++++++++++++ 11 files changed, 152 insertions(+), 16 deletions(-) create mode 100644 layout/style/test/test_aspect_ratio_property.html diff --git a/layout/generic/nsFrame.cpp b/layout/generic/nsFrame.cpp index b501fa85f5..4616757076 100644 --- a/layout/generic/nsFrame.cpp +++ b/layout/generic/nsFrame.cpp @@ -4652,7 +4652,10 @@ nsFrame::GetIntrinsicSize() /* virtual */ AspectRatio nsFrame::GetIntrinsicRatio() { - return AspectRatio(); + const nsStylePosition* stylePos = StylePosition(); + return stylePos->mAspectRatio == 0.0f + ? AspectRatio() + : AspectRatio(stylePos->mAspectRatio); } void @@ -4721,9 +4724,16 @@ nsFrame::ComputeSize(nsRenderingContext* aRenderingContext, const LogicalSize& aPadding, ComputeSizeFlags aFlags) { - MOZ_ASSERT(!GetIntrinsicRatio(), - "Please override this method and call " - "nsFrame::ComputeSizeWithIntrinsicDimensions instead."); + AspectRatio intrinsicRatio = GetIntrinsicRatio(); + if (intrinsicRatio) { + return ComputeSizeWithIntrinsicDimensions(aRenderingContext, aWM, + GetIntrinsicSize(), + intrinsicRatio, + aCBSize, aMargin, + aBorder, aPadding, + aFlags); + } + LogicalSize result = ComputeAutoSize(aRenderingContext, aWM, aCBSize, aAvailableISize, aMargin, aBorder, aPadding, diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index 2b7267b2b7..d17dea6c44 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -1170,6 +1170,8 @@ protected: // Property specific parsing routines bool ParseImageLayers(const nsCSSPropertyID aTable[]); + bool ParseAspectRatio(nsCSSValue& aValue); + bool ParseAspectRatioRatio(nsCSSValue& aValue); struct ImageLayersShorthandParseState { nsCSSValue& mColor; @@ -13256,6 +13258,59 @@ CSSParserImpl::ParsePropertyByFunction(nsCSSPropertyID aPropID) #define BG_CLR (BG_CENTER | BG_LEFT | BG_RIGHT) #define BG_LR (BG_LEFT | BG_RIGHT) +bool +CSSParserImpl::ParseAspectRatioRatio(nsCSSValue& aValue) +{ + nsCSSValue width; + if (!ParseNonNegativeNumber(width)) { + return false; + } + + float w = width.GetFloatValue(); + float h = 1.0f; + if (ExpectSymbol('/', true)) { + nsCSSValue height; + if (!ParseNonNegativeNumber(height)) { + return false; + } + h = height.GetFloatValue(); + } + + // Degenerate ratios behave as auto in layout. + aValue.SetFloatValue(w == 0.0f || h == 0.0f ? 0.0f : w / h, + eCSSUnit_Number); + return true; +} + +bool +CSSParserImpl::ParseAspectRatio(nsCSSValue& aValue) +{ + if (ParseSingleTokenVariant(aValue, VARIANT_INHERIT, nullptr)) { + return true; + } + + nsCSSValue autoValue; + bool hasAuto = ParseSingleTokenVariant(autoValue, VARIANT_AUTO, nullptr); + + nsCSSValue ratioValue; + bool hasRatio = ParseAspectRatioRatio(ratioValue); + if (!hasAuto && !hasRatio) { + return false; + } + + if (hasRatio) { + if (!hasAuto) { + // The grammar is "auto || ", so auto may appear after the ratio. + ParseSingleTokenVariant(autoValue, VARIANT_AUTO, nullptr); + } + aValue = ratioValue; + return true; + } + + aValue.SetAutoValue(); + return true; +} + CSSParseResult CSSParserImpl::ParseBoxProperty(nsCSSValue& aValue, nsCSSPropertyID aPropID) @@ -13294,6 +13349,8 @@ CSSParserImpl::ParseSingleValuePropertyByFunction(nsCSSValue& aValue, switch (aPropID) { case eCSSProperty_clip_path: return ParseClipPath(aValue); + case eCSSProperty_aspect_ratio: + return ParseAspectRatio(aValue); case eCSSProperty_contain: return ParseContain(aValue); case eCSSProperty_font_family: diff --git a/layout/style/nsCSSPropList.h b/layout/style/nsCSSPropList.h index 018f71b8d9..8ae43f0339 100644 --- a/layout/style/nsCSSPropList.h +++ b/layout/style/nsCSSPropList.h @@ -470,19 +470,17 @@ CSS_PROP_DISPLAY( kAppearanceKTable, CSS_PROP_NO_OFFSET, eStyleAnimType_Discrete) -#ifndef CSS_PROP_LIST_EXCLUDE_INTERNAL CSS_PROP_POSITION( aspect-ratio, aspect_ratio, AspectRatio, - CSS_PROPERTY_INTERNAL | - CSS_PROPERTY_PARSE_INACCESSIBLE, + CSS_PROPERTY_PARSE_VALUE | + CSS_PROPERTY_VALUE_PARSER_FUNCTION, "", - VARIANT_NUMBER, + 0, nullptr, offsetof(nsStylePosition, mAspectRatio), eStyleAnimType_None) -#endif // CSS_PROP_LIST_EXCLUDE_INTERNAL CSS_PROP_DISPLAY( backface-visibility, backface_visibility, diff --git a/layout/style/nsComputedDOMStyle.cpp b/layout/style/nsComputedDOMStyle.cpp index 3915b21707..02aa7258f2 100644 --- a/layout/style/nsComputedDOMStyle.cpp +++ b/layout/style/nsComputedDOMStyle.cpp @@ -964,6 +964,22 @@ nsComputedDOMStyle::DoGetBinding() return val.forget(); } +already_AddRefed +nsComputedDOMStyle::DoGetAspectRatio() +{ + RefPtr val = new nsROCSSPrimitiveValue; + float ratio = StylePosition()->mAspectRatio; + if (ratio == 0.0f) { + val->SetIdent(eCSSKeyword_auto); + } else { + nsAutoString ratioString; + nsStyleUtil::AppendCSSNumber(ratio, ratioString); + ratioString.AppendLiteral(" / 1"); + val->SetString(ratioString); + } + return val.forget(); +} + already_AddRefed nsComputedDOMStyle::DoGetClear() { @@ -6902,4 +6918,3 @@ nsComputedDOMStyle::DoGetOverflowBlockEnd() { return DoGetOverflowBlock(); } - diff --git a/layout/style/nsComputedDOMStyle.h b/layout/style/nsComputedDOMStyle.h index cf168554b8..0715f18444 100644 --- a/layout/style/nsComputedDOMStyle.h +++ b/layout/style/nsComputedDOMStyle.h @@ -226,6 +226,7 @@ private: */ already_AddRefed DoGetAppearance(); + already_AddRefed DoGetAspectRatio(); /* Box properties */ already_AddRefed DoGetBoxAlign(); diff --git a/layout/style/nsComputedDOMStylePropertyList.h b/layout/style/nsComputedDOMStylePropertyList.h index 51a1ed67f7..b62a484376 100644 --- a/layout/style/nsComputedDOMStylePropertyList.h +++ b/layout/style/nsComputedDOMStylePropertyList.h @@ -51,6 +51,7 @@ COMPUTED_STYLE_PROP(animation_iteration_count, AnimationIterationCount) COMPUTED_STYLE_PROP(animation_name, AnimationName) COMPUTED_STYLE_PROP(animation_play_state, AnimationPlayState) COMPUTED_STYLE_PROP(animation_timing_function, AnimationTimingFunction) +COMPUTED_STYLE_PROP(aspect_ratio, AspectRatio) COMPUTED_STYLE_PROP(backface_visibility, BackfaceVisibility) //// COMPUTED_STYLE_PROP(background, Background) COMPUTED_STYLE_PROP(background_attachment, BackgroundAttachment) diff --git a/layout/style/nsRuleNode.cpp b/layout/style/nsRuleNode.cpp index aa5c02bd6f..a2e9e91fa8 100644 --- a/layout/style/nsRuleNode.cpp +++ b/layout/style/nsRuleNode.cpp @@ -9304,11 +9304,16 @@ nsRuleNode::ComputePositionData(void* aStartStruct, SETCOORD_UNSET_INITIAL, aContext, mPresContext, conditions); - // aspect-ratio: float, initial - SetFactor(*aRuleData->ValueForAspectRatio(), - pos->mAspectRatio, conditions, - parentPos->mAspectRatio, 0.0f, - SETFCT_UNSET_INITIAL | SETFCT_POSITIVE | SETFCT_NONE); + // aspect-ratio: auto | + const nsCSSValue* aspectRatio = aRuleData->ValueForAspectRatio(); + if (aspectRatio->GetUnit() == eCSSUnit_Auto) { + pos->mAspectRatio = 0.0f; + } else { + SetFactor(*aspectRatio, + pos->mAspectRatio, conditions, + parentPos->mAspectRatio, 0.0f, + SETFCT_UNSET_INITIAL | SETFCT_POSITIVE | SETFCT_NONE); + } // box-sizing: enum, inherit, initial SetValue(*aRuleData->ValueForBoxSizing(), diff --git a/layout/style/test/ListCSSProperties.cpp b/layout/style/test/ListCSSProperties.cpp index 5e0405bb55..a3262b2f80 100644 --- a/layout/style/test/ListCSSProperties.cpp +++ b/layout/style/test/ListCSSProperties.cpp @@ -105,7 +105,6 @@ const char *gInaccessibleProperties[] = { "-x-span", "-x-system-font", "-x-text-zoom", - "aspect-ratio", // for now. "-moz-control-character-visibility", "-moz-script-level", // parsed by UA sheets only "-moz-script-size-multiplier", diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini index 45e5823343..b4caf214c6 100644 --- a/layout/style/test/mochitest.ini +++ b/layout/style/test/mochitest.ini @@ -70,6 +70,7 @@ support-files = file_animations_with_disabled_properties.html [test_any_dynamic.html] [test_at_rule_parse_serialize.html] [test_attribute_selector_eof_behavior.html] +[test_aspect_ratio_property.html] [test_background_blend_mode.html] [test_basic_nesting_flattening.html] [test_nesting_flattening_parser_edges.html] diff --git a/layout/style/test/property_database.js b/layout/style/test/property_database.js index d895cabdac..9cb650a049 100644 --- a/layout/style/test/property_database.js +++ b/layout/style/test/property_database.js @@ -1167,6 +1167,14 @@ var gCSSProperties = { other_values: ["url(foo.xml)"], invalid_values: [], }, + "aspect-ratio": { + domProp: "aspectRatio", + inherited: false, + type: CSS_TYPE_LONGHAND, + initial_values: ["auto"], + other_values: ["1", "2"], + invalid_values: ["none", "-1", "1px", "1 / -1", "1 /", "auto auto"], + }, "-moz-border-bottom-colors": { domProp: "MozBorderBottomColors", inherited: false, diff --git a/layout/style/test/test_aspect_ratio_property.html b/layout/style/test/test_aspect_ratio_property.html new file mode 100644 index 0000000000..b50788dcc1 --- /dev/null +++ b/layout/style/test/test_aspect_ratio_property.html @@ -0,0 +1,41 @@ + + + + Test CSS aspect-ratio property + + + + +
+
+
+
+
+
+
+
+
+ + From 93899c01572e7c14f96e0ba6701ea20af916eae5 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sun, 10 May 2026 14:04:56 -0400 Subject: [PATCH 15/30] Support CSS sizing math functions Prereq for parts of #2404 --- layout/style/CSSCalc.h | 108 +++++++++- layout/style/CSSStyleSheet.cpp | 49 +++++ layout/style/nsCSSParser.cpp | 145 ++++++++++++- layout/style/nsCSSValue.cpp | 7 +- layout/style/nsCSSValue.h | 9 +- layout/style/nsComputedDOMStyle.cpp | 103 ++++++++++ layout/style/nsRuleNode.cpp | 193 +++++++++++++++++- layout/style/nsRuleNode.h | 15 +- layout/style/nsStyleCoord.cpp | 161 ++++++++++++++- layout/style/nsStyleCoord.h | 46 ++++- layout/style/test/mochitest.ini | 1 + .../style/test/test_css_math_functions.html | 47 +++++ 12 files changed, 855 insertions(+), 29 deletions(-) create mode 100644 layout/style/test/test_css_math_functions.html diff --git a/layout/style/CSSCalc.h b/layout/style/CSSCalc.h index 665fcd3613..0dd1c37dd3 100644 --- a/layout/style/CSSCalc.h +++ b/layout/style/CSSCalc.h @@ -6,6 +6,7 @@ #include "nsCSSValue.h" #include "nsStyleCoord.h" +#include #include namespace mozilla { @@ -46,6 +47,13 @@ namespace css { * result_type aValue1, float aValue2); * * result_type + * MergeMinMax(nsCSSUnit aCalcFunction, + * result_type aValue1, result_type aValue2); + * + * result_type + * MergeClamp(result_type aMin, result_type aCenter, result_type aMax); + * + * result_type * ComputeLeafValue(const input_type& aValue); * * float @@ -70,6 +78,8 @@ namespace css { * MergeAdditive for Plus and Minus * MergeMultiplicativeL for Times_L (number * value) * MergeMultiplicativeR for Times_R (value * number) and Divided + * MergeMinMax for Min and Max + * MergeClamp for Clamp */ template static typename CalcOps::result_type @@ -104,6 +114,25 @@ ComputeCalc(const typename CalcOps::input_type& aValue, CalcOps &aOps) float rhs = aOps.ComputeNumber(arr->Item(1)); return aOps.MergeMultiplicativeR(CalcOps::GetUnit(aValue), lhs, rhs); } + case eCSSUnit_Calc_Min: + case eCSSUnit_Calc_Max: { + typename CalcOps::input_array_type *arr = aValue.GetArrayValue(); + MOZ_ASSERT(arr->Count() >= 1, "unexpected length"); + typename CalcOps::result_type result = ComputeCalc(arr->Item(0), aOps); + for (uint32_t i = 1; i < arr->Count(); ++i) { + typename CalcOps::result_type next = ComputeCalc(arr->Item(i), aOps); + result = aOps.MergeMinMax(CalcOps::GetUnit(aValue), result, next); + } + return result; + } + case eCSSUnit_Calc_Clamp: { + typename CalcOps::input_array_type *arr = aValue.GetArrayValue(); + MOZ_ASSERT(arr->Count() == 3, "unexpected length"); + typename CalcOps::result_type min = ComputeCalc(arr->Item(0), aOps); + typename CalcOps::result_type center = ComputeCalc(arr->Item(1), aOps); + typename CalcOps::result_type max = ComputeCalc(arr->Item(2), aOps); + return aOps.MergeClamp(min, center, max); + } default: { return aOps.ComputeLeafValue(aValue); } @@ -168,6 +197,23 @@ struct BasicCoordCalcOps } return NSCoordSaturatingMultiply(aValue1, aValue2); } + + result_type + MergeMinMax(nsCSSUnit aCalcFunction, + result_type aValue1, result_type aValue2) + { + if (aCalcFunction == eCSSUnit_Calc_Min) { + return std::min(aValue1, aValue2); + } + MOZ_ASSERT(aCalcFunction == eCSSUnit_Calc_Max, "unexpected unit"); + return std::max(aValue1, aValue2); + } + + result_type + MergeClamp(result_type aMin, result_type aCenter, result_type aMax) + { + return std::max(aMin, std::min(aCenter, aMax)); + } }; struct BasicFloatCalcOps @@ -206,6 +252,23 @@ struct BasicFloatCalcOps "unexpected unit"); return aValue1 / aValue2; } + + result_type + MergeMinMax(nsCSSUnit aCalcFunction, + result_type aValue1, result_type aValue2) + { + if (aCalcFunction == eCSSUnit_Calc_Min) { + return std::min(aValue1, aValue2); + } + MOZ_ASSERT(aCalcFunction == eCSSUnit_Calc_Max, "unexpected unit"); + return std::max(aValue1, aValue2); + } + + result_type + MergeClamp(result_type aMin, result_type aCenter, result_type aMax) + { + return std::max(aMin, std::min(aCenter, aMax)); + } }; /** @@ -255,8 +318,20 @@ template static void SerializeCalc(const typename CalcOps::input_type& aValue, CalcOps &aOps) { - aOps.Append("calc("); nsCSSUnit unit = CalcOps::GetUnit(aValue); + if (unit == eCSSUnit_Calc) { + const typename CalcOps::input_array_type *array = aValue.GetArrayValue(); + MOZ_ASSERT(array->Count() == 1, "unexpected length"); + nsCSSUnit childUnit = CalcOps::GetUnit(array->Item(0)); + if (childUnit == eCSSUnit_Calc_Min || + childUnit == eCSSUnit_Calc_Max || + childUnit == eCSSUnit_Calc_Clamp) { + SerializeCalcInternal(array->Item(0), aOps); + return; + } + } + + aOps.Append("calc("); if (unit == eCSSUnit_Calc) { const typename CalcOps::input_array_type *array = aValue.GetArrayValue(); MOZ_ASSERT(array->Count() == 1, "unexpected length"); @@ -282,6 +357,14 @@ IsCalcMultiplicativeUnit(nsCSSUnit aUnit) aUnit == eCSSUnit_Calc_Divided; } +static inline bool +IsCalcMinMaxClampUnit(nsCSSUnit aUnit) +{ + return aUnit == eCSSUnit_Calc_Min || + aUnit == eCSSUnit_Calc_Max || + aUnit == eCSSUnit_Calc_Clamp; +} + // Serialize a non-toplevel value in a calc() tree. See big comment // above. template @@ -340,6 +423,29 @@ SerializeCalcInternal(const typename CalcOps::input_type& aValue, CalcOps &aOps) if (needParens) { aOps.Append(")"); } + } else if (IsCalcMinMaxClampUnit(unit)) { + const typename CalcOps::input_array_type *array = aValue.GetArrayValue(); + MOZ_ASSERT(unit != eCSSUnit_Calc_Clamp || array->Count() == 3, + "unexpected length"); + MOZ_ASSERT(unit == eCSSUnit_Calc_Clamp || array->Count() >= 1, + "unexpected length"); + + if (unit == eCSSUnit_Calc_Min) { + aOps.Append("min("); + } else if (unit == eCSSUnit_Calc_Max) { + aOps.Append("max("); + } else { + MOZ_ASSERT(unit == eCSSUnit_Calc_Clamp, "unexpected unit"); + aOps.Append("clamp("); + } + + for (uint32_t i = 0; i < array->Count(); ++i) { + if (i != 0) { + aOps.Append(", "); + } + SerializeCalcInternal(array->Item(i), aOps); + } + aOps.Append(")"); } else { aOps.AppendLeafValue(aValue); } diff --git a/layout/style/CSSStyleSheet.cpp b/layout/style/CSSStyleSheet.cpp index ff044f3a77..e6351a6ba2 100644 --- a/layout/style/CSSStyleSheet.cpp +++ b/layout/style/CSSStyleSheet.cpp @@ -7,6 +7,8 @@ #include "mozilla/CSSStyleSheet.h" +#include + #include "nsIAtom.h" #include "nsCSSRuleProcessor.h" #include "mozilla/MemoryReporting.h" @@ -191,6 +193,53 @@ EvaluateMediaQueryTypedCalcLength(nsPresContext* aPresContext, return true; } + case eCSSUnit_Calc_Min: + case eCSSUnit_Calc_Max: + case eCSSUnit_Calc_Clamp: { + nsCSSValue::Array* array = aValue.GetArrayValue(); + MOZ_ASSERT(aValue.GetUnit() != eCSSUnit_Calc_Clamp || + array->Count() == 3, "unexpected length"); + MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_Calc_Clamp || + array->Count() >= 1, "unexpected length"); + + MediaQueryTypedCalcLengthResult result; + if (!EvaluateMediaQueryTypedCalcLength(aPresContext, array->Item(0), + result)) { + return false; + } + + if (aValue.GetUnit() == eCSSUnit_Calc_Clamp) { + MediaQueryTypedCalcLengthResult center; + MediaQueryTypedCalcLengthResult max; + if (!EvaluateMediaQueryTypedCalcLength(aPresContext, array->Item(1), + center) || + !EvaluateMediaQueryTypedCalcLength(aPresContext, array->Item(2), + max) || + result.mExponent != center.mExponent || + result.mExponent != max.mExponent) { + return false; + } + aResult.mExponent = result.mExponent; + aResult.mValue = std::max(result.mValue, + std::min(center.mValue, max.mValue)); + return true; + } + + for (uint32_t i = 1; i < array->Count(); ++i) { + MediaQueryTypedCalcLengthResult item; + if (!EvaluateMediaQueryTypedCalcLength(aPresContext, array->Item(i), + item) || + result.mExponent != item.mExponent) { + return false; + } + result.mValue = aValue.GetUnit() == eCSSUnit_Calc_Min + ? std::min(result.mValue, item.mValue) + : std::max(result.mValue, item.mValue); + } + aResult = result; + return true; + } + case eCSSUnit_Number: aResult.mValue = aValue.GetFloatValue(); aResult.mExponent = 0; diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index d17dea6c44..735ddf3e5a 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -348,6 +348,31 @@ GetCalcLengthTypedArithmeticExponent(const nsCSSValue& aValue, return true; } + case eCSSUnit_Calc_Min: + case eCSSUnit_Calc_Max: + case eCSSUnit_Calc_Clamp: { + nsCSSValue::Array* array = aValue.GetArrayValue(); + MOZ_ASSERT(aValue.GetUnit() != eCSSUnit_Calc_Clamp || + array->Count() == 3, "unexpected length"); + MOZ_ASSERT(aValue.GetUnit() == eCSSUnit_Calc_Clamp || + array->Count() >= 1, "unexpected length"); + + int32_t exponent; + if (!GetCalcLengthTypedArithmeticExponent(array->Item(0), exponent)) { + return false; + } + for (uint32_t i = 1; i < array->Count(); ++i) { + int32_t itemExponent; + if (!GetCalcLengthTypedArithmeticExponent(array->Item(i), + itemExponent) || + itemExponent != exponent) { + return false; + } + } + aExponent = exponent; + return true; + } + case eCSSUnit_Number: aExponent = 0; return true; @@ -430,6 +455,35 @@ NormalizeCalcForVariant(nsCSSValue& aValue, return false; } +static bool +MergeCalcFunctionVariantMask(uint32_t& aMergedMask, uint32_t aItemMask) +{ + MOZ_ASSERT(aItemMask != 0, "unexpected empty item mask"); + if (aMergedMask == 0) { + aMergedMask = aItemMask; + return true; + } + + const bool mergedIsNumber = (aMergedMask & VARIANT_NUMBER) != 0; + const bool itemIsNumber = (aItemMask & VARIANT_NUMBER) != 0; + if (mergedIsNumber || itemIsNumber) { + return mergedIsNumber == itemIsNumber; + } + + const uint32_t lengthPercentMask = VARIANT_LENGTH | VARIANT_PERCENT; + if ((aMergedMask & lengthPercentMask) && (aItemMask & lengthPercentMask)) { + aMergedMask = (aMergedMask | aItemMask) & lengthPercentMask; + return true; + } + + if ((aMergedMask & aItemMask) != 0) { + aMergedMask &= aItemMask; + return true; + } + + return false; +} + static_assert(css::eAuthorSheetFeatures == 0 && css::eUserSheetFeatures == 1 && css::eAgentSheetFeatures == 2, @@ -1251,6 +1305,8 @@ protected: uint32_t& aVariantMask, bool *aHadFinalWS); bool ParseCalcTerm(nsCSSValue& aValue, uint32_t& aVariantMask); + bool ParseCalcMinMaxClampFunction(nsCSSValue& aValue, + uint32_t& aVariantMask); bool ParseCalcNumberExpressionValue(float& aValue); bool ParseCalcNumberFunction(nsCSSValue& aValue, uint32_t& aVariantMask); bool RequireWhitespace(); @@ -13694,7 +13750,8 @@ CSSParserImpl::IsCalcFunctionToken(const nsCSSToken& aToken) const { return aToken.mType == eCSSToken_Function && (aToken.mIdent.LowerCaseEqualsLiteral("calc") || - aToken.mIdent.LowerCaseEqualsLiteral("-moz-calc")); + aToken.mIdent.LowerCaseEqualsLiteral("-moz-calc") || + IsCalcNumberFunctionName(aToken.mIdent)); } // Parse one item of the background shorthand property. @@ -15038,8 +15095,16 @@ CSSParserImpl::ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask, RefPtr arr = nsCSSValue::Array::Create(1); uint32_t resultVariantMask = aVariantMask; - if (!ParseCalcAdditiveExpression(arr->Item(0), resultVariantMask)) + const bool isMinMaxClamp = + mToken.mType == eCSSToken_Function && + IsCalcNumberFunctionName(mToken.mIdent); + if (isMinMaxClamp) { + if (!ParseCalcMinMaxClampFunction(arr->Item(0), resultVariantMask)) { + break; + } + } else if (!ParseCalcAdditiveExpression(arr->Item(0), resultVariantMask)) { break; + } if (mCalcAllowsTypedArithmetic) { int32_t exponent; @@ -15060,8 +15125,9 @@ CSSParserImpl::ParseCalc(nsCSSValue& aValue, uint32_t aVariantMask, } } - if (!ExpectSymbol(')', true)) + if (!isMinMaxClamp && !ExpectSymbol(')', true)) { break; + } aValue.SetArrayValue(arr, eCSSUnit_Calc); if (aResultVariantMask) { @@ -15271,6 +15337,14 @@ CSSParserImpl::ParseCalcTerm(nsCSSValue& aValue, uint32_t& aVariantMask) MOZ_ASSERT(aVariantMask != 0, "unexpected variant mask"); if (!GetToken(true)) return false; + if (mToken.mType == eCSSToken_Function && + IsCalcNumberFunctionName(mToken.mIdent)) { + if (!ParseCalcMinMaxClampFunction(aValue, aVariantMask)) { + SkipUntil(')'); + return false; + } + return true; + } // Either an additive expression in parentheses... if (mToken.IsSymbol('(') || // Treat nested calc() as plain parenthesis. @@ -15282,14 +15356,6 @@ CSSParserImpl::ParseCalcTerm(nsCSSValue& aValue, uint32_t& aVariantMask) } return true; } - if (mToken.mType == eCSSToken_Function && - IsCalcNumberFunctionName(mToken.mIdent)) { - if (!ParseCalcNumberFunction(aValue, aVariantMask)) { - SkipUntil(')'); - return false; - } - return true; - } if ((aVariantMask & VARIANT_NUMBER) != 0 && mToken.mType == eCSSToken_Ident) { float specialValue; @@ -15324,6 +15390,63 @@ CSSParserImpl::ParseCalcTerm(nsCSSValue& aValue, uint32_t& aVariantMask) return true; } +bool +CSSParserImpl::ParseCalcMinMaxClampFunction(nsCSSValue& aValue, + uint32_t& aVariantMask) +{ + MOZ_ASSERT(mToken.mType == eCSSToken_Function, "expected function token"); + MOZ_ASSERT(IsCalcNumberFunctionName(mToken.mIdent), + "unexpected calc() math function"); + + nsCSSUnit unit; + if (mToken.mIdent.LowerCaseEqualsLiteral("min")) { + unit = eCSSUnit_Calc_Min; + } else if (mToken.mIdent.LowerCaseEqualsLiteral("max")) { + unit = eCSSUnit_Calc_Max; + } else { + MOZ_ASSERT(mToken.mIdent.LowerCaseEqualsLiteral("clamp"), + "unexpected calc() math function"); + unit = eCSSUnit_Calc_Clamp; + } + + AutoTArray arguments; + uint32_t mergedVariantMask = 0; + + for (;;) { + nsCSSValue* argument = arguments.AppendElement(); + uint32_t argumentVariantMask = aVariantMask; + if (!ParseCalcAdditiveExpression(*argument, argumentVariantMask) || + !MergeCalcFunctionVariantMask(mergedVariantMask, + argumentVariantMask)) { + return false; + } + + if (!ExpectSymbol(',', true)) { + break; + } + } + + const uint32_t argumentCount = arguments.Length(); + if ((unit == eCSSUnit_Calc_Clamp && argumentCount != 3) || + (unit != eCSSUnit_Calc_Clamp && argumentCount == 0) || + !ExpectSymbol(')', true)) { + return false; + } + + if (unit != eCSSUnit_Calc_Clamp && argumentCount == 1) { + aValue = arguments[0]; + } else { + RefPtr array = nsCSSValue::Array::Create(argumentCount); + for (uint32_t i = 0; i < argumentCount; ++i) { + array->Item(i) = arguments[i]; + } + aValue.SetArrayValue(array, unit); + } + + aVariantMask = mergedVariantMask; + return true; +} + bool CSSParserImpl::ParseCalcNumberExpressionValue(float& aValue) { diff --git a/layout/style/nsCSSValue.cpp b/layout/style/nsCSSValue.cpp index 4a3e9b1b4f..e63c630b10 100644 --- a/layout/style/nsCSSValue.cpp +++ b/layout/style/nsCSSValue.cpp @@ -1693,7 +1693,6 @@ nsCSSValue::AppendToString(nsCSSPropertyID aProperty, nsAString& aResult, aResult.Append(')'); } else if (IsCalcUnit()) { - MOZ_ASSERT(GetUnit() == eCSSUnit_Calc, "unexpected unit"); if (!AppendNormalizedLengthPercentCalcToString(*this, aProperty, aResult, aSerialization)) { CSSValueSerializeCalcOps ops(aProperty, aResult, aSerialization); @@ -2266,6 +2265,9 @@ nsCSSValue::AppendToString(nsCSSPropertyID aProperty, nsAString& aResult, case eCSSUnit_Calc_Times_L: break; case eCSSUnit_Calc_Times_R: break; case eCSSUnit_Calc_Divided: break; + case eCSSUnit_Calc_Min: break; + case eCSSUnit_Calc_Max: break; + case eCSSUnit_Calc_Clamp: break; case eCSSUnit_Integer: break; case eCSSUnit_Enumerated: break; case eCSSUnit_EnumColor: break; @@ -2398,6 +2400,9 @@ nsCSSValue::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const case eCSSUnit_Calc_Times_L: case eCSSUnit_Calc_Times_R: case eCSSUnit_Calc_Divided: + case eCSSUnit_Calc_Min: + case eCSSUnit_Calc_Max: + case eCSSUnit_Calc_Clamp: break; // URL diff --git a/layout/style/nsCSSValue.h b/layout/style/nsCSSValue.h index cf608aaec6..60f3417fb3 100644 --- a/layout/style/nsCSSValue.h +++ b/layout/style/nsCSSValue.h @@ -477,6 +477,11 @@ enum nsCSSUnit { eCSSUnit_Calc_Times_L = 33, // (nsCSSValue::Array*) num * val within calc eCSSUnit_Calc_Times_R = 34, // (nsCSSValue::Array*) val * num within calc eCSSUnit_Calc_Divided = 35, // (nsCSSValue::Array*) / within calc + // Min and Max have arrays with one or more elements. Clamp has an array + // with exactly 3 elements: lower bound, center, upper bound. + eCSSUnit_Calc_Min = 36, // (nsCSSValue::Array*) min() node + eCSSUnit_Calc_Max = 37, // (nsCSSValue::Array*) max() node + eCSSUnit_Calc_Clamp = 38, // (nsCSSValue::Array*) clamp() node eCSSUnit_URL = 40, // (nsCSSValue::URL*) value eCSSUnit_Image = 41, // (nsCSSValue::Image*) value @@ -713,12 +718,12 @@ public: bool IsTimeUnit() const { return eCSSUnit_Seconds <= mUnit && mUnit <= eCSSUnit_Milliseconds; } bool IsCalcUnit() const - { return eCSSUnit_Calc <= mUnit && mUnit <= eCSSUnit_Calc_Divided; } + { return eCSSUnit_Calc <= mUnit && mUnit <= eCSSUnit_Calc_Clamp; } bool UnitHasStringValue() const { return eCSSUnit_String <= mUnit && mUnit <= eCSSUnit_Element; } bool UnitHasArrayValue() const - { return eCSSUnit_Array <= mUnit && mUnit <= eCSSUnit_Calc_Divided; } + { return eCSSUnit_Array <= mUnit && mUnit <= eCSSUnit_Calc_Clamp; } // Checks for the nsCSSValue being of a particular type of color unit: // diff --git a/layout/style/nsComputedDOMStyle.cpp b/layout/style/nsComputedDOMStyle.cpp index 02aa7258f2..8b7a61dfdd 100644 --- a/layout/style/nsComputedDOMStyle.cpp +++ b/layout/style/nsComputedDOMStyle.cpp @@ -1899,6 +1899,94 @@ nsComputedDOMStyle::DoGetBackgroundColor() return val.forget(); } +static void +AppendCalcNodeToString(const nsStyleCoord::CalcNode* aNode, + nsROCSSPrimitiveValue* aPrimitive, + nsAString& aResult); + +static void +AppendCalcLeafToString(const nsStyleCoord::CalcNode* aNode, + nsROCSSPrimitiveValue* aPrimitive, + nsAString& aResult) +{ + nsAutoString tmp; + + if (!aNode->mHasPercent) { + aPrimitive->SetAppUnits(aNode->mLength); + aPrimitive->GetCssText(tmp); + aResult.Append(tmp); + return; + } + + aResult.AppendLiteral("calc("); + aPrimitive->SetAppUnits(aNode->mLength); + aPrimitive->GetCssText(tmp); + aResult.Append(tmp); + aResult.AppendLiteral(" + "); + aPrimitive->SetPercent(aNode->mPercent); + aPrimitive->GetCssText(tmp); + aResult.Append(tmp); + aResult.Append(')'); +} + +static void +AppendCalcNodeToString(const nsStyleCoord::CalcNode* aNode, + nsROCSSPrimitiveValue* aPrimitive, + nsAString& aResult) +{ + using Type = nsStyleCoord::CalcNode::Type; + + switch (aNode->mType) { + case Type::Leaf: + AppendCalcLeafToString(aNode, aPrimitive, aResult); + return; + case Type::Add: + case Type::Subtract: + aResult.AppendLiteral("calc("); + AppendCalcNodeToString(aNode->mChildren[0], aPrimitive, aResult); + aResult.Append(aNode->mType == Type::Add + ? NS_LITERAL_STRING(" + ") + : NS_LITERAL_STRING(" - ")); + AppendCalcNodeToString(aNode->mChildren[1], aPrimitive, aResult); + aResult.Append(')'); + return; + case Type::Multiply: + case Type::Divide: { + nsAutoString tmp; + aResult.AppendLiteral("calc("); + AppendCalcNodeToString(aNode->mChildren[0], aPrimitive, aResult); + aResult.Append(aNode->mType == Type::Multiply + ? NS_LITERAL_STRING(" * ") + : NS_LITERAL_STRING(" / ")); + aPrimitive->SetNumber(aNode->mNumber); + aPrimitive->GetCssText(tmp); + aResult.Append(tmp); + aResult.Append(')'); + return; + } + case Type::Min: + case Type::Max: + case Type::Clamp: + if (aNode->mType == Type::Min) { + aResult.AppendLiteral("min("); + } else if (aNode->mType == Type::Max) { + aResult.AppendLiteral("max("); + } else { + aResult.AppendLiteral("clamp("); + } + for (uint32_t i = 0; i < aNode->mChildren.Length(); ++i) { + if (i != 0) { + aResult.AppendLiteral(", "); + } + AppendCalcNodeToString(aNode->mChildren[i], aPrimitive, aResult); + } + aResult.Append(')'); + return; + } + + MOZ_ASSERT_UNREACHABLE("unexpected calc node type"); +} + static void SetValueToCalc(const nsStyleCoord::CalcValue* aCalc, nsROCSSPrimitiveValue* aValue) @@ -1925,6 +2013,21 @@ SetValueToCalc(const nsStyleCoord::CalcValue* aCalc, aValue->SetString(result); // not really SetString } +static void +SetValueToCalc(const nsStyleCoord::Calc* aCalc, + nsROCSSPrimitiveValue* aValue) +{ + if (!aCalc->HasCalcNode()) { + SetValueToCalc(static_cast(aCalc), aValue); + return; + } + + RefPtr val = new nsROCSSPrimitiveValue; + nsAutoString result; + AppendCalcNodeToString(aCalc->mNode, val, result); + aValue->SetString(result); // not really SetString +} + static void AppendCSSGradientLength(const nsStyleCoord& aValue, nsROCSSPrimitiveValue* aPrimitive, diff --git a/layout/style/nsRuleNode.cpp b/layout/style/nsRuleNode.cpp index a2e9e91fa8..f9319ac1da 100644 --- a/layout/style/nsRuleNode.cpp +++ b/layout/style/nsRuleNode.cpp @@ -798,6 +798,16 @@ CalcLengthTowardZero(const nsCSSValue& aValue, AppUnitRounding::TowardZero); } +already_AddRefed +nsRuleNode::ComputedCalc::ToCalcNode() const +{ + if (mNode) { + RefPtr node = mNode; + return node.forget(); + } + return nsStyleCoord::CalcNode::CreateLeaf(mLength, mPercent, mHasPercent); +} + /* static */ nscoord nsRuleNode::CalcLengthWithInitialFont(nsPresContext* aPresContext, const nsCSSValue& aValue) @@ -830,27 +840,42 @@ struct LengthPercentPairCalcOps : public css::NumbersAlreadyNormalizedOps { if (aValue.GetUnit() == eCSSUnit_Percent) { mHasPercent = true; - return result_type(0, aValue.GetPercentValue()); + return result_type(0, aValue.GetPercentValue(), true); } return result_type(CalcLength(aValue, mContext, mPresContext, mConditions), - 0.0f); + 0.0f, false); } result_type MergeAdditive(nsCSSUnit aCalcFunction, result_type aValue1, result_type aValue2) { + if (aValue1.HasNode() || aValue2.HasNode()) { + RefPtr node = + nsStyleCoord::CalcNode::Create( + aCalcFunction == eCSSUnit_Calc_Plus + ? nsStyleCoord::CalcNode::Type::Add + : nsStyleCoord::CalcNode::Type::Subtract); + node->mChildren.AppendElement(aValue1.ToCalcNode()); + node->mChildren.AppendElement(aValue2.ToCalcNode()); + node->mHasPercent = node->HasPercent(); + mHasPercent = mHasPercent || node->mHasPercent; + return result_type(node.forget()); + } + if (aCalcFunction == eCSSUnit_Calc_Plus) { return result_type(NSCoordSaturatingAdd(aValue1.mLength, aValue2.mLength), - aValue1.mPercent + aValue2.mPercent); + aValue1.mPercent + aValue2.mPercent, + aValue1.mHasPercent || aValue2.mHasPercent); } MOZ_ASSERT(aCalcFunction == eCSSUnit_Calc_Minus, "min() and max() are not allowed in calc() on transform"); return result_type(NSCoordSaturatingSubtract(aValue1.mLength, aValue2.mLength, 0), - aValue1.mPercent - aValue2.mPercent); + aValue1.mPercent - aValue2.mPercent, + aValue1.mHasPercent || aValue2.mHasPercent); } result_type @@ -859,8 +884,19 @@ struct LengthPercentPairCalcOps : public css::NumbersAlreadyNormalizedOps { MOZ_ASSERT(aCalcFunction == eCSSUnit_Calc_Times_L, "unexpected unit"); + if (aValue2.HasNode()) { + RefPtr node = + nsStyleCoord::CalcNode::Create(nsStyleCoord::CalcNode::Type::Multiply); + node->mChildren.AppendElement(aValue2.ToCalcNode()); + node->mNumber = aValue1; + node->mHasPercent = node->HasPercent(); + mHasPercent = mHasPercent || node->mHasPercent; + return result_type(node.forget()); + } + return result_type(NSCoordSaturatingMultiply(aValue2.mLength, aValue1), - aValue1 * aValue2.mPercent); + aValue1 * aValue2.mPercent, + aValue2.mHasPercent); } result_type @@ -871,10 +907,89 @@ struct LengthPercentPairCalcOps : public css::NumbersAlreadyNormalizedOps aCalcFunction == eCSSUnit_Calc_Divided, "unexpected unit"); if (aCalcFunction == eCSSUnit_Calc_Divided) { + if (aValue1.HasNode()) { + RefPtr node = + nsStyleCoord::CalcNode::Create(nsStyleCoord::CalcNode::Type::Divide); + node->mChildren.AppendElement(aValue1.ToCalcNode()); + node->mNumber = aValue2; + node->mHasPercent = node->HasPercent(); + mHasPercent = mHasPercent || node->mHasPercent; + return result_type(node.forget()); + } aValue2 = 1.0f / aValue2; + } else if (aValue1.HasNode()) { + RefPtr node = + nsStyleCoord::CalcNode::Create(nsStyleCoord::CalcNode::Type::Multiply); + node->mChildren.AppendElement(aValue1.ToCalcNode()); + node->mNumber = aValue2; + node->mHasPercent = node->HasPercent(); + mHasPercent = mHasPercent || node->mHasPercent; + return result_type(node.forget()); } return result_type(NSCoordSaturatingMultiply(aValue1.mLength, aValue2), - aValue1.mPercent * aValue2); + aValue1.mPercent * aValue2, + aValue1.mHasPercent); + } + + result_type + MergeMinMax(nsCSSUnit aCalcFunction, + result_type aValue1, result_type aValue2) + { + const bool hasNode = aValue1.HasNode() || aValue2.HasNode(); + const bool hasPercent = aValue1.mHasPercent || aValue2.mHasPercent; + if (!hasNode && !hasPercent) { + if (aCalcFunction == eCSSUnit_Calc_Min) { + return result_type(std::min(aValue1.mLength, aValue2.mLength), + 0.0f, false); + } + MOZ_ASSERT(aCalcFunction == eCSSUnit_Calc_Max, "unexpected unit"); + return result_type(std::max(aValue1.mLength, aValue2.mLength), + 0.0f, false); + } + + RefPtr node = + nsStyleCoord::CalcNode::Create( + aCalcFunction == eCSSUnit_Calc_Min + ? nsStyleCoord::CalcNode::Type::Min + : nsStyleCoord::CalcNode::Type::Max); + if (aValue1.HasNode() && + aValue1.mNode->mType == node->mType) { + node->mChildren.AppendElements(aValue1.mNode->mChildren); + } else { + node->mChildren.AppendElement(aValue1.ToCalcNode()); + } + if (aValue2.HasNode() && + aValue2.mNode->mType == node->mType) { + node->mChildren.AppendElements(aValue2.mNode->mChildren); + } else { + node->mChildren.AppendElement(aValue2.ToCalcNode()); + } + node->mHasPercent = node->HasPercent(); + mHasPercent = mHasPercent || node->mHasPercent; + return result_type(node.forget()); + } + + result_type + MergeClamp(result_type aMin, result_type aCenter, result_type aMax) + { + const bool hasNode = aMin.HasNode() || aCenter.HasNode() || aMax.HasNode(); + const bool hasPercent = aMin.mHasPercent || + aCenter.mHasPercent || + aMax.mHasPercent; + if (!hasNode && !hasPercent) { + return result_type(std::max(aMin.mLength, + std::min(aCenter.mLength, aMax.mLength)), + 0.0f, false); + } + + RefPtr node = + nsStyleCoord::CalcNode::Create(nsStyleCoord::CalcNode::Type::Clamp); + node->mChildren.AppendElement(aMin.ToCalcNode()); + node->mChildren.AppendElement(aCenter.ToCalcNode()); + node->mChildren.AppendElement(aMax.ToCalcNode()); + node->mHasPercent = node->HasPercent(); + mHasPercent = mHasPercent || node->mHasPercent; + return result_type(node.forget()); } }; @@ -892,7 +1007,8 @@ SpecifiedCalcToComputedCalc(const nsCSSValue& aValue, nsStyleCoord& aCoord, calcObj->mLength = vals.mLength; calcObj->mPercent = vals.mPercent; - calcObj->mHasPercent = ops.mHasPercent; + calcObj->mHasPercent = vals.mHasPercent || ops.mHasPercent; + calcObj->mNode = vals.mNode; aCoord.SetCalcValue(calcObj); } @@ -915,8 +1031,7 @@ nsRuleNode::ComputeComputedCalc(const nsStyleCoord& aValue, nscoord aPercentageBasis) { nsStyleCoord::Calc* calc = aValue.GetCalcValue(); - return calc->mLength + - NSToCoordFloorClamped(aPercentageBasis * calc->mPercent); + return calc->Resolve(aPercentageBasis); } /* static */ nscoord @@ -5108,6 +5223,34 @@ struct LengthNumberCalcOps : public css::NumbersAlreadyNormalizedOps return result; } + result_type + MergeMinMax(nsCSSUnit aCalcFunction, + result_type aValue1, result_type aValue2) + { + MOZ_ASSERT(aValue1.mIsNumber == aValue2.mIsNumber); + LengthNumberCalcObj result; + result.mIsNumber = aValue1.mIsNumber; + if (aCalcFunction == eCSSUnit_Calc_Min) { + result.mValue = std::min(aValue1.mValue, aValue2.mValue); + return result; + } + MOZ_ASSERT(aCalcFunction == eCSSUnit_Calc_Max, "unexpected unit"); + result.mValue = std::max(aValue1.mValue, aValue2.mValue); + return result; + } + + result_type + MergeClamp(result_type aMin, result_type aCenter, result_type aMax) + { + MOZ_ASSERT(aMin.mIsNumber == aCenter.mIsNumber && + aCenter.mIsNumber == aMax.mIsNumber); + LengthNumberCalcObj result; + result.mIsNumber = aCenter.mIsNumber; + result.mValue = std::max(aMin.mValue, + std::min(aCenter.mValue, aMax.mValue)); + return result; + } + result_type ComputeLeafValue(const nsCSSValue& aValue) { LengthNumberCalcObj result; @@ -5198,6 +5341,38 @@ struct LengthPercentNumberCalcOps : public css::NumbersAlreadyNormalizedOps return result; } + result_type + MergeMinMax(nsCSSUnit aCalcFunction, + result_type aValue1, result_type aValue2) + { + MOZ_ASSERT(aValue1.mIsNumber == aValue2.mIsNumber); + result_type result; + result.mIsNumber = aValue1.mIsNumber; + if (aCalcFunction == eCSSUnit_Calc_Min) { + result.mLength = std::min(aValue1.mLength, aValue2.mLength); + result.mPercent = std::min(aValue1.mPercent, aValue2.mPercent); + } else { + MOZ_ASSERT(aCalcFunction == eCSSUnit_Calc_Max, "unexpected unit"); + result.mLength = std::max(aValue1.mLength, aValue2.mLength); + result.mPercent = std::max(aValue1.mPercent, aValue2.mPercent); + } + return result; + } + + result_type + MergeClamp(result_type aMin, result_type aCenter, result_type aMax) + { + MOZ_ASSERT(aMin.mIsNumber == aCenter.mIsNumber && + aCenter.mIsNumber == aMax.mIsNumber); + result_type result; + result.mIsNumber = aCenter.mIsNumber; + result.mLength = std::max(aMin.mLength, + std::min(aCenter.mLength, aMax.mLength)); + result.mPercent = std::max(aMin.mPercent, + std::min(aCenter.mPercent, aMax.mPercent)); + return result; + } + result_type ComputeLeafValue(const nsCSSValue& aValue) { diff --git a/layout/style/nsRuleNode.h b/layout/style/nsRuleNode.h index 7ca3a165f9..b4118335e1 100644 --- a/layout/style/nsRuleNode.h +++ b/layout/style/nsRuleNode.h @@ -986,9 +986,22 @@ public: struct ComputedCalc { nscoord mLength; float mPercent; + bool mHasPercent; + RefPtr mNode; ComputedCalc(nscoord aLength, float aPercent) - : mLength(aLength), mPercent(aPercent) {} + : mLength(aLength), mPercent(aPercent), + mHasPercent(aPercent != 0.0f) {} + + ComputedCalc(nscoord aLength, float aPercent, bool aHasPercent) + : mLength(aLength), mPercent(aPercent), + mHasPercent(aHasPercent) {} + + explicit ComputedCalc(already_AddRefed aNode) + : mLength(0), mPercent(0.0f), mHasPercent(true), mNode(aNode) {} + + bool HasNode() const { return !!mNode; } + already_AddRefed ToCalcNode() const; }; static ComputedCalc SpecifiedCalcToComputedCalc(const nsCSSValue& aValue, diff --git a/layout/style/nsStyleCoord.cpp b/layout/style/nsStyleCoord.cpp index f93bb53f63..d26e02438c 100644 --- a/layout/style/nsStyleCoord.cpp +++ b/layout/style/nsStyleCoord.cpp @@ -8,6 +8,150 @@ #include "nsStyleCoord.h" #include "mozilla/HashFunctions.h" #include "mozilla/PodOperations.h" +#include + +already_AddRefed +nsStyleCoord::CalcNode::CreateLeaf(nscoord aLength, float aPercent, + bool aHasPercent) +{ + RefPtr node = new CalcNode(Type::Leaf); + node->mLength = aLength; + node->mPercent = aPercent; + node->mHasPercent = aHasPercent; + return node.forget(); +} + +already_AddRefed +nsStyleCoord::CalcNode::Create(Type aType) +{ + RefPtr node = new CalcNode(aType); + return node.forget(); +} + +nsStyleCoord::CalcNode::CalcNode(Type aType) + : mType(aType) + , mLength(0) + , mPercent(0.0f) + , mNumber(0.0f) + , mHasPercent(false) +{ +} + +bool +nsStyleCoord::CalcNode::HasPercent() const +{ + if (mHasPercent) { + return true; + } + for (const RefPtr& child : mChildren) { + if (child->HasPercent()) { + return true; + } + } + return false; +} + +bool +nsStyleCoord::CalcNode::Equals(const CalcNode& aOther) const +{ + if (mType != aOther.mType || + mLength != aOther.mLength || + mPercent != aOther.mPercent || + mNumber != aOther.mNumber || + mHasPercent != aOther.mHasPercent || + mChildren.Length() != aOther.mChildren.Length()) { + return false; + } + + for (uint32_t i = 0; i < mChildren.Length(); ++i) { + if (!mChildren[i]->Equals(*aOther.mChildren[i])) { + return false; + } + } + return true; +} + +uint32_t +nsStyleCoord::CalcNode::HashValue(uint32_t aHash) const +{ + aHash = mozilla::AddToHash(aHash, uint8_t(mType), mLength, mPercent, + mNumber, mHasPercent); + for (const RefPtr& child : mChildren) { + aHash = child->HashValue(aHash); + } + return aHash; +} + +static nscoord +ResolveCalcNode(const nsStyleCoord::CalcNode& aNode, nscoord aPercentageBasis) +{ + using Type = nsStyleCoord::CalcNode::Type; + + switch (aNode.mType) { + case Type::Leaf: + return aNode.mLength + + NSToCoordFloorClamped(aPercentageBasis * aNode.mPercent); + case Type::Add: + MOZ_ASSERT(aNode.mChildren.Length() == 2, "unexpected child count"); + return NSCoordSaturatingAdd( + ResolveCalcNode(*aNode.mChildren[0], aPercentageBasis), + ResolveCalcNode(*aNode.mChildren[1], aPercentageBasis)); + case Type::Subtract: + MOZ_ASSERT(aNode.mChildren.Length() == 2, "unexpected child count"); + return NSCoordSaturatingSubtract( + ResolveCalcNode(*aNode.mChildren[0], aPercentageBasis), + ResolveCalcNode(*aNode.mChildren[1], aPercentageBasis), 0); + case Type::Multiply: + MOZ_ASSERT(aNode.mChildren.Length() == 1, "unexpected child count"); + return NSCoordSaturatingMultiply( + ResolveCalcNode(*aNode.mChildren[0], aPercentageBasis), + aNode.mNumber); + case Type::Divide: + MOZ_ASSERT(aNode.mChildren.Length() == 1, "unexpected child count"); + return NSCoordSaturatingMultiply( + ResolveCalcNode(*aNode.mChildren[0], aPercentageBasis), + 1.0f / aNode.mNumber); + case Type::Min: { + MOZ_ASSERT(!aNode.mChildren.IsEmpty(), "unexpected child count"); + nscoord result = ResolveCalcNode(*aNode.mChildren[0], aPercentageBasis); + for (uint32_t i = 1; i < aNode.mChildren.Length(); ++i) { + result = std::min(result, + ResolveCalcNode(*aNode.mChildren[i], + aPercentageBasis)); + } + return result; + } + case Type::Max: { + MOZ_ASSERT(!aNode.mChildren.IsEmpty(), "unexpected child count"); + nscoord result = ResolveCalcNode(*aNode.mChildren[0], aPercentageBasis); + for (uint32_t i = 1; i < aNode.mChildren.Length(); ++i) { + result = std::max(result, + ResolveCalcNode(*aNode.mChildren[i], + aPercentageBasis)); + } + return result; + } + case Type::Clamp: + MOZ_ASSERT(aNode.mChildren.Length() == 3, "unexpected child count"); + return std::max(ResolveCalcNode(*aNode.mChildren[0], aPercentageBasis), + std::min(ResolveCalcNode(*aNode.mChildren[1], + aPercentageBasis), + ResolveCalcNode(*aNode.mChildren[2], + aPercentageBasis))); + } + + MOZ_ASSERT_UNREACHABLE("unexpected calc node type"); + return 0; +} + +nscoord +nsStyleCoord::Calc::Resolve(nscoord aPercentageBasis) const +{ + if (mNode) { + return ResolveCalcNode(*mNode, aPercentageBasis); + } + return mLength + NSToCoordFloorClamped(aPercentageBasis * mPercent); +} nsStyleCoord::nsStyleCoord(nsStyleUnit aUnit) : mUnit(aUnit) @@ -71,8 +215,15 @@ bool nsStyleCoord::operator==(const nsStyleCoord& aOther) const case eStyleUnit_Integer: case eStyleUnit_Enumerated: return mValue.mInt == aOther.mValue.mInt; - case eStyleUnit_Calc: - return *this->GetCalcValue() == *aOther.GetCalcValue(); + case eStyleUnit_Calc: { + Calc* thisCalc = GetCalcValue(); + Calc* otherCalc = aOther.GetCalcValue(); + if (thisCalc->HasCalcNode() || otherCalc->HasCalcNode()) { + return thisCalc->HasCalcNode() == otherCalc->HasCalcNode() && + thisCalc->mNode->Equals(*otherCalc->mNode); + } + return *thisCalc == *otherCalc; + } } MOZ_ASSERT(false, "unexpected unit"); return false; @@ -100,13 +251,17 @@ uint32_t nsStyleCoord::HashValue(uint32_t aHash = 0) const case eStyleUnit_Integer: case eStyleUnit_Enumerated: return mozilla::AddToHash(aHash, mValue.mInt); - case eStyleUnit_Calc: + case eStyleUnit_Calc: { Calc* calcValue = GetCalcValue(); + if (calcValue->HasCalcNode()) { + return calcValue->mNode->HashValue(aHash); + } aHash = mozilla::AddToHash(aHash, calcValue->mLength); if (HasPercent()) { return mozilla::AddToHash(aHash, calcValue->mPercent); } return aHash; + } } MOZ_ASSERT(false, "unexpected unit"); return aHash; diff --git a/layout/style/nsStyleCoord.h b/layout/style/nsStyleCoord.h index bd4f15ab51..4718726bd4 100644 --- a/layout/style/nsStyleCoord.h +++ b/layout/style/nsStyleCoord.h @@ -10,6 +10,8 @@ #include "nsCoord.h" #include "nsStyleConsts.h" +#include "nsTArray.h" +#include "mozilla/RefPtr.h" namespace mozilla { @@ -106,7 +108,44 @@ public: // If this returns true the value is definitely zero. It it returns false // it might be zero. So it's best used for conservative optimization. - bool IsDefinitelyZero() const { return mLength == 0 && mPercent == 0; } + bool IsDefinitelyZero() const { + return mLength == 0 && mPercent == 0 && !mHasPercent; + } + }; + + struct CalcNode final { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CalcNode) + + enum class Type : uint8_t { + Leaf, + Add, + Subtract, + Multiply, + Divide, + Min, + Max, + Clamp + }; + + static already_AddRefed CreateLeaf(nscoord aLength, + float aPercent, + bool aHasPercent); + static already_AddRefed Create(Type aType); + + bool HasPercent() const; + bool Equals(const CalcNode& aOther) const; + uint32_t HashValue(uint32_t aHash) const; + + Type mType; + nscoord mLength; + float mPercent; + float mNumber; + bool mHasPercent; + nsTArray> mChildren; + + private: + explicit CalcNode(Type aType); + ~CalcNode() {} }; // Reference counted calc() value. This is the type that is used to store @@ -115,6 +154,11 @@ public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Calc) Calc() {} + bool HasCalcNode() const { return !!mNode; } + nscoord Resolve(nscoord aPercentageBasis) const; + + RefPtr mNode; + private: Calc(const Calc&) = delete; ~Calc() {} diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini index b4caf214c6..bac78cfacf 100644 --- a/layout/style/test/mochitest.ini +++ b/layout/style/test/mochitest.ini @@ -78,6 +78,7 @@ support-files = file_animations_with_disabled_properties.html [test_box_size_keywords.html] [test_border_width_rounding.html] [test_bug73586.html] +[test_css_math_functions.html] [test_bug74880.html] [test_bug98997.html] [test_bug160403.html] diff --git a/layout/style/test/test_css_math_functions.html b/layout/style/test/test_css_math_functions.html new file mode 100644 index 0000000000..040251edf5 --- /dev/null +++ b/layout/style/test/test_css_math_functions.html @@ -0,0 +1,47 @@ + + + + Test CSS sizing math functions + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + From 98c9f7387acf56f559eb60cbf6a0050bc9956d5b Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sun, 10 May 2026 15:14:31 -0400 Subject: [PATCH 16/30] Revert "Cloudflare Image Resizing fix take 2" This reverts commit ef19ad3ffd3a449e28888d3a636ecc8851c9ad65. No longer necessary with aspect-ratio in place. --- netwerk/base/nsStandardURL.cpp | 52 ---------------------------------- 1 file changed, 52 deletions(-) diff --git a/netwerk/base/nsStandardURL.cpp b/netwerk/base/nsStandardURL.cpp index 92dce5423e..6d26072046 100644 --- a/netwerk/base/nsStandardURL.cpp +++ b/netwerk/base/nsStandardURL.cpp @@ -1106,58 +1106,6 @@ nsStandardURL::ParseURL(const char *spec, int32_t specLen) nsresult nsStandardURL::ParsePath(const char *spec, uint32_t pathPos, int32_t pathLen) { -// Cloudflare Image Resizing compatibility (pref-controlled) -// -// This feature detects the "/cdn-cgi/image/" marker in the URL path and -// treats everything after it as opaque path data. Cloudflare's Image -// Resizing service expects clients to preserve the entire suffix exactly. -// -// Because this code runs in a hot path (URL parsing), we avoid calling -// Preferences::GetBool() repeatedly. Instead, we use AddBoolVarCache() -// to cache the pref value once and read it cheaply thereafter. - -// Cached preference: true = enable Cloudflare Image Resizing fixup -static bool sCloudflareImageResizingEnabled = true; -static bool sCloudflareImageResizingPrefCached = false; - -if (!sCloudflareImageResizingPrefCached) { - Preferences::AddBoolVarCache( - &sCloudflareImageResizingEnabled, - "network.url.cloudflare_image_resizing.enabled", - true // default if pref does not exist - ); - sCloudflareImageResizingPrefCached = true; -} - -if (sCloudflareImageResizingEnabled) { - - // Extract the full path substring from the full URL spec. - nsDependentCSubstring fullPath(spec + pathPos, pathLen); - - // Prepare iterators for scanning the path. - nsACString::const_iterator begin, end; - fullPath.BeginReading(begin); - fullPath.EndReading(end); - - // Search for the Cloudflare Image Resizing marker. - nsACString::const_iterator cfPos = begin; - if (FindInReadable(NS_LITERAL_CSTRING("/cdn-cgi/image/"), cfPos, end)) { - - // Compute how far into the path the marker was found. - uint32_t offset = cfPos.get() - begin.get(); - - // Rewrite the internal path representation so that the path - // begins at the Cloudflare marker. Everything before it is ignored. - mPath.mPos = pathPos + offset; - mPath.mLen = pathLen - offset; - - // We handled the path; no further parsing needed. - return NS_OK; - } -} - - - LOG(("ParsePath: %s pathpos %d len %d\n",spec,pathPos,pathLen)); if (pathLen > net_GetURLMaxLength()) { From 27f2a0869cd7437d0bb9af97ee95f9fd79ee08e7 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sun, 10 May 2026 15:40:48 -0400 Subject: [PATCH 17/30] Issue #3089 - Support logical border radius properties Resolves #3089 --- layout/style/nsCSSDataBlock.cpp | 44 ++++ layout/style/nsCSSParser.cpp | 4 + layout/style/nsCSSPropList.h | 72 ++++++ layout/style/nsCSSPropLogicalGroupList.h | 3 +- layout/style/nsCSSProps.cpp | 9 +- layout/style/nsCSSProps.h | 22 +- layout/style/test/mochitest.ini | 1 + layout/style/test/property_database.js | 212 ++++++++++++++++++ .../test/test_logical_border_radius.html | 76 +++++++ 9 files changed, 437 insertions(+), 6 deletions(-) create mode 100644 layout/style/test/test_logical_border_radius.html diff --git a/layout/style/nsCSSDataBlock.cpp b/layout/style/nsCSSDataBlock.cpp index 09ac37550e..85561f0c3d 100644 --- a/layout/style/nsCSSDataBlock.cpp +++ b/layout/style/nsCSSDataBlock.cpp @@ -209,11 +209,38 @@ MapSinglePropertyInto(nsCSSPropertyID aTargetProp, * property based on writing mode information obtained from aRuleData's * style context. */ +static inline int +PhysicalCornerIndexForLogicalCorner(mozilla::Side aBlockSide, + mozilla::Side aInlineSide) +{ + // gBorderRadiusSubpropTable is in top-left, top-right, bottom-right, + // bottom-left order. + switch (aBlockSide) { + case eSideTop: + MOZ_ASSERT(aInlineSide == eSideLeft || aInlineSide == eSideRight); + return aInlineSide == eSideLeft ? 0 : 1; + case eSideRight: + MOZ_ASSERT(aInlineSide == eSideTop || aInlineSide == eSideBottom); + return aInlineSide == eSideTop ? 1 : 2; + case eSideBottom: + MOZ_ASSERT(aInlineSide == eSideLeft || aInlineSide == eSideRight); + return aInlineSide == eSideRight ? 2 : 3; + case eSideLeft: + MOZ_ASSERT(aInlineSide == eSideTop || aInlineSide == eSideBottom); + return aInlineSide == eSideTop ? 0 : 3; + } + + MOZ_ASSERT_UNREACHABLE("unexpected physical side"); + return 0; +} + static inline void EnsurePhysicalProperty(nsCSSPropertyID& aProperty, nsRuleData* aRuleData) { bool isAxisProperty = nsCSSProps::PropHasFlags(aProperty, CSS_PROPERTY_LOGICAL_AXIS); + bool isCornerProperty = + nsCSSProps::PropHasFlags(aProperty, CSS_PROPERTY_LOGICAL_CORNER); bool isBlock = nsCSSProps::PropHasFlags(aProperty, CSS_PROPERTY_LOGICAL_BLOCK_AXIS); @@ -230,6 +257,23 @@ EnsurePhysicalProperty(nsCSSPropertyID& aProperty, nsRuleData* aRuleData) static_assert(eAxisVertical == 0 && eAxisHorizontal == 1, "unexpected axis constant values"); index = axis; + } else if (isCornerProperty) { + bool isInlineEnd = + nsCSSProps::PropHasFlags(aProperty, CSS_PROPERTY_LOGICAL_END_EDGE); + + LogicalEdge blockEdge = + isBlock ? eLogicalEdgeEnd : eLogicalEdgeStart; + LogicalEdge inlineEdge = + isInlineEnd ? eLogicalEdgeEnd : eLogicalEdgeStart; + + uint8_t wmBits = aRuleData->mStyleContext->StyleVisibility()->mWritingMode; + mozilla::Side blockSide = + WritingMode::PhysicalSideForBlockAxis(wmBits, blockEdge); + + WritingMode wm(aRuleData->mStyleContext); + mozilla::Side inlineSide = wm.PhysicalSideForInlineAxis(inlineEdge); + + index = PhysicalCornerIndexForLogicalCorner(blockSide, inlineSide); } else { bool isEnd = nsCSSProps::PropHasFlags(aProperty, CSS_PROPERTY_LOGICAL_END_EDGE); diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index 735ddf3e5a..0a1bf33e29 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -13146,6 +13146,10 @@ CSSParserImpl::ParsePropertyByFunction(nsCSSPropertyID aPropID) case eCSSProperty_border_top_right_radius: case eCSSProperty_border_bottom_right_radius: case eCSSProperty_border_bottom_left_radius: + case eCSSProperty_border_start_start_radius: + case eCSSProperty_border_start_end_radius: + case eCSSProperty_border_end_start_radius: + case eCSSProperty_border_end_end_radius: case eCSSProperty__moz_outline_radius_topLeft: case eCSSProperty__moz_outline_radius_topRight: case eCSSProperty__moz_outline_radius_bottomRight: diff --git a/layout/style/nsCSSPropList.h b/layout/style/nsCSSPropList.h index 8ae43f0339..189180591d 100644 --- a/layout/style/nsCSSPropList.h +++ b/layout/style/nsCSSPropList.h @@ -883,6 +883,43 @@ CSS_PROP_SHORTHAND( CSS_PROPERTY_PARSE_FUNCTION | CSS_PROPERTY_HASHLESS_COLOR_QUIRK, "") +CSS_PROP_LOGICAL( + border-end-end-radius, + border_end_end_radius, + BorderEndEndRadius, + CSS_PROPERTY_PARSE_FUNCTION | + CSS_PROPERTY_APPLIES_TO_FIRST_LETTER | + CSS_PROPERTY_VALUE_NONNEGATIVE | + CSS_PROPERTY_STORES_CALC | + CSS_PROPERTY_LOGICAL | + CSS_PROPERTY_LOGICAL_CORNER | + CSS_PROPERTY_LOGICAL_BLOCK_AXIS | + CSS_PROPERTY_LOGICAL_END_EDGE, + "", + 0, + nullptr, + BorderRadius, + Border, + CSS_PROP_NO_OFFSET, + eStyleAnimType_None) +CSS_PROP_LOGICAL( + border-end-start-radius, + border_end_start_radius, + BorderEndStartRadius, + CSS_PROPERTY_PARSE_FUNCTION | + CSS_PROPERTY_APPLIES_TO_FIRST_LETTER | + CSS_PROPERTY_VALUE_NONNEGATIVE | + CSS_PROPERTY_STORES_CALC | + CSS_PROPERTY_LOGICAL | + CSS_PROPERTY_LOGICAL_CORNER | + CSS_PROPERTY_LOGICAL_BLOCK_AXIS, + "", + 0, + nullptr, + BorderRadius, + Border, + CSS_PROP_NO_OFFSET, + eStyleAnimType_None) CSS_PROP_SHORTHAND( border-image, border_image, @@ -1178,6 +1215,41 @@ CSS_PROP_TABLEBORDER( nullptr, CSS_PROP_NO_OFFSET, eStyleAnimType_Custom) +CSS_PROP_LOGICAL( + border-start-end-radius, + border_start_end_radius, + BorderStartEndRadius, + CSS_PROPERTY_PARSE_FUNCTION | + CSS_PROPERTY_APPLIES_TO_FIRST_LETTER | + CSS_PROPERTY_VALUE_NONNEGATIVE | + CSS_PROPERTY_STORES_CALC | + CSS_PROPERTY_LOGICAL | + CSS_PROPERTY_LOGICAL_CORNER | + CSS_PROPERTY_LOGICAL_END_EDGE, + "", + 0, + nullptr, + BorderRadius, + Border, + CSS_PROP_NO_OFFSET, + eStyleAnimType_None) +CSS_PROP_LOGICAL( + border-start-start-radius, + border_start_start_radius, + BorderStartStartRadius, + CSS_PROPERTY_PARSE_FUNCTION | + CSS_PROPERTY_APPLIES_TO_FIRST_LETTER | + CSS_PROPERTY_VALUE_NONNEGATIVE | + CSS_PROPERTY_STORES_CALC | + CSS_PROPERTY_LOGICAL | + CSS_PROPERTY_LOGICAL_CORNER, + "", + 0, + nullptr, + BorderRadius, + Border, + CSS_PROP_NO_OFFSET, + eStyleAnimType_None) CSS_PROP_SHORTHAND( border-style, border_style, diff --git a/layout/style/nsCSSPropLogicalGroupList.h b/layout/style/nsCSSPropLogicalGroupList.h index 81dbee641d..5d471950ae 100644 --- a/layout/style/nsCSSPropLogicalGroupList.h +++ b/layout/style/nsCSSPropLogicalGroupList.h @@ -46,6 +46,7 @@ // order, followed by an nsCSSProperty_UNKNOWN entry. CSS_PROP_LOGICAL_GROUP_SHORTHAND(BorderColor) +CSS_PROP_LOGICAL_GROUP_SHORTHAND(BorderRadius) CSS_PROP_LOGICAL_GROUP_SHORTHAND(BorderStyle) CSS_PROP_LOGICAL_GROUP_SHORTHAND(BorderWidth) CSS_PROP_LOGICAL_GROUP_SHORTHAND(Margin) @@ -54,4 +55,4 @@ CSS_PROP_LOGICAL_GROUP_BOX(Offset) CSS_PROP_LOGICAL_GROUP_SHORTHAND(Padding) CSS_PROP_LOGICAL_GROUP_AXIS(MinSize) CSS_PROP_LOGICAL_GROUP_AXIS(Size) -CSS_PROP_LOGICAL_GROUP_AXIS(Overflow) \ No newline at end of file +CSS_PROP_LOGICAL_GROUP_AXIS(Overflow) diff --git a/layout/style/nsCSSProps.cpp b/layout/style/nsCSSProps.cpp index dca949f7f4..0421528ab8 100644 --- a/layout/style/nsCSSProps.cpp +++ b/layout/style/nsCSSProps.cpp @@ -3490,7 +3490,10 @@ nsCSSProps::gPropertyEnabled[eCSSProperty_COUNT_with_aliases] = { "the CSS_PROPERTY_LOGICAL_BLOCK_AXIS flag"); \ static_assert(!((flags_) & CSS_PROPERTY_LOGICAL_END_EDGE), \ "only properties defined with CSS_PROP_LOGICAL can use " \ - "the CSS_PROPERTY_LOGICAL_END_EDGE flag"); + "the CSS_PROPERTY_LOGICAL_END_EDGE flag"); \ + static_assert(!((flags_) & CSS_PROPERTY_LOGICAL_CORNER), \ + "only properties defined with CSS_PROP_LOGICAL can use " \ + "the CSS_PROPERTY_LOGICAL_CORNER flag"); #define CSS_PROP_LOGICAL(name_, id_, method_, flags_, pref_, parsevariant_, \ kwtable_, group_, stylestruct_, \ stylestructoffset_, animtype_) \ @@ -3503,6 +3506,10 @@ nsCSSProps::gPropertyEnabled[eCSSProperty_COUNT_with_aliases] = { static_assert(!(((flags_) & CSS_PROPERTY_LOGICAL_AXIS) && \ ((flags_) & CSS_PROPERTY_LOGICAL_END_EDGE)), \ "CSS_PROPERTY_LOGICAL_END_EDGE makes no sense when used " \ + "with CSS_PROPERTY_LOGICAL_AXIS"); \ + static_assert(!(((flags_) & CSS_PROPERTY_LOGICAL_AXIS) && \ + ((flags_) & CSS_PROPERTY_LOGICAL_CORNER)), \ + "CSS_PROPERTY_LOGICAL_CORNER makes no sense when used " \ "with CSS_PROPERTY_LOGICAL_AXIS"); #include "nsCSSPropList.h" #undef CSS_PROP_LOGICAL diff --git a/layout/style/nsCSSProps.h b/layout/style/nsCSSProps.h index 0995fba61b..bbfd308045 100644 --- a/layout/style/nsCSSProps.h +++ b/layout/style/nsCSSProps.h @@ -155,7 +155,8 @@ // example, the block-size logical property has this flag set, as it // represents the size in either the block or inline axis dimensions, and // has two corresponding physical properties, width and height. Must not -// be used in conjunction with CSS_PROPERTY_LOGICAL_END_EDGE. +// be used in conjunction with CSS_PROPERTY_LOGICAL_END_EDGE or +// CSS_PROPERTY_LOGICAL_CORNER. #define CSS_PROPERTY_LOGICAL_AXIS (1<<7) // This property allows calc() between lengths and percentages and @@ -247,7 +248,9 @@ static_assert((CSS_PROPERTY_PARSE_PROPERTY_MASK & // sides (such as margin-block-start or margin-block-end). Must only be // set if CSS_PROPERTY_LOGICAL is set. When not set, the logical // property is for one of the two inline axis sides (such as -// margin-inline-start or margin-inline-end). +// margin-inline-start or margin-inline-end). For logical corner +// properties, this flag means the block-end edge; when not set, the +// property is for the block-start edge. #define CSS_PROPERTY_LOGICAL_BLOCK_AXIS (1<<25) // This property is a logical property for the "end" edge of the @@ -255,9 +258,18 @@ static_assert((CSS_PROPERTY_PARSE_PROPERTY_MASK & // CSS_PROPERTY_LOGICAL_BLOCK_AXIS (such as margin-block-end or // margin-inline-end). Must only be set if CSS_PROPERTY_LOGICAL is set. // When not set, the logical property is for the "start" edge (such as -// margin-block-start or margin-inline-start). +// margin-block-start or margin-inline-start). For logical corner +// properties, this flag means the inline-end edge; when not set, the +// property is for the inline-start edge. #define CSS_PROPERTY_LOGICAL_END_EDGE (1<<26) +// This property is a logical corner property, such as +// border-start-start-radius. CSS_PROPERTY_LOGICAL_BLOCK_AXIS and +// CSS_PROPERTY_LOGICAL_END_EDGE select the block and inline edges that +// form the corner. Must only be set if CSS_PROPERTY_LOGICAL is set, and +// must not be used in conjunction with CSS_PROPERTY_LOGICAL_AXIS. +#define CSS_PROPERTY_LOGICAL_CORNER (1u<<31) + // This property can be animated on the compositor. #define CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR (1<<27) @@ -584,7 +596,9 @@ public: * * When called with a property that has the CSS_PROPERTY_LOGICAL_AXIS * flag, the returned array will have two values preceding the sentinel; - * otherwise it will have four. + * otherwise it will have four. For logical corner properties, the four + * properties are returned in top-left, top-right, bottom-right, + * bottom-left order. * * (Note that the running time of this function is proportional to the * number of logical longhand properties that exist. If we start diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini index bac78cfacf..1e4900e62f 100644 --- a/layout/style/test/mochitest.ini +++ b/layout/style/test/mochitest.ini @@ -216,6 +216,7 @@ support-files = [test_initial_storage.html] [test_keyframes_rules.html] [test_load_events_on_stylesheets.html] +[test_logical_border_radius.html] [test_logical_properties.html] [test_media_queries.html] [test_media_queries_dynamic.html] diff --git a/layout/style/test/property_database.js b/layout/style/test/property_database.js index 9cb650a049..fb332aea6a 100644 --- a/layout/style/test/property_database.js +++ b/layout/style/test/property_database.js @@ -1655,6 +1655,154 @@ var gCSSProperties = { "2px calc(0px + rubbish)", ], }, + "border-end-end-radius": { + domProp: "borderEndEndRadius", + inherited: false, + type: CSS_TYPE_LONGHAND, + logical: true, + get_computed: logical_corner_prop_get_computed, + prerequisites: { width: "200px", height: "100px", display: "inline-block" }, + initial_values: ["0", "0px", "calc(-2px)"], + other_values: [ + "0%", + "3%", + "1px", + "2em", // circular + "3% 2%", + "1px 4px", + "2em 2pt", // elliptical + "calc(-1%)", + "calc(2px)", + "calc(50%)", + "calc(3*25px)", + "calc(3*25px) 5px", + "5px calc(3*25px)", + "calc(20%) calc(3*25px)", + "calc(25px*3)", + "calc(3*25px + 50%)", + ], + invalid_values: [ + "-1px", + "4px -2px", + "inherit 2px", + "2px inherit", + "2", + "2px 2", + "2 2px", + "2px calc(0px + rubbish)", + ], + }, + "border-end-start-radius": { + domProp: "borderEndStartRadius", + inherited: false, + type: CSS_TYPE_LONGHAND, + logical: true, + get_computed: logical_corner_prop_get_computed, + prerequisites: { width: "200px", height: "100px", display: "inline-block" }, + initial_values: ["0", "0px", "calc(-2px)"], + other_values: [ + "0%", + "3%", + "1px", + "2em", // circular + "3% 2%", + "1px 4px", + "2em 2pt", // elliptical + "calc(-1%)", + "calc(2px)", + "calc(50%)", + "calc(3*25px)", + "calc(3*25px) 5px", + "5px calc(3*25px)", + "calc(20%) calc(3*25px)", + "calc(25px*3)", + "calc(3*25px + 50%)", + ], + invalid_values: [ + "-1px", + "4px -2px", + "inherit 2px", + "2px inherit", + "2", + "2px 2", + "2 2px", + "2px calc(0px + rubbish)", + ], + }, + "border-start-end-radius": { + domProp: "borderStartEndRadius", + inherited: false, + type: CSS_TYPE_LONGHAND, + logical: true, + get_computed: logical_corner_prop_get_computed, + prerequisites: { width: "200px", height: "100px", display: "inline-block" }, + initial_values: ["0", "0px", "calc(-2px)"], + other_values: [ + "0%", + "3%", + "1px", + "2em", // circular + "3% 2%", + "1px 4px", + "2em 2pt", // elliptical + "calc(-1%)", + "calc(2px)", + "calc(50%)", + "calc(3*25px)", + "calc(3*25px) 5px", + "5px calc(3*25px)", + "calc(20%) calc(3*25px)", + "calc(25px*3)", + "calc(3*25px + 50%)", + ], + invalid_values: [ + "-1px", + "4px -2px", + "inherit 2px", + "2px inherit", + "2", + "2px 2", + "2 2px", + "2px calc(0px + rubbish)", + ], + }, + "border-start-start-radius": { + domProp: "borderStartStartRadius", + inherited: false, + type: CSS_TYPE_LONGHAND, + logical: true, + get_computed: logical_corner_prop_get_computed, + prerequisites: { width: "200px", height: "100px", display: "inline-block" }, + initial_values: ["0", "0px", "calc(-2px)"], + other_values: [ + "0%", + "3%", + "1px", + "2em", // circular + "3% 2%", + "1px 4px", + "2em 2pt", // elliptical + "calc(-1%)", + "calc(2px)", + "calc(50%)", + "calc(3*25px)", + "calc(3*25px) 5px", + "5px calc(3*25px)", + "calc(20%) calc(3*25px)", + "calc(25px*3)", + "calc(3*25px + 50%)", + ], + invalid_values: [ + "-1px", + "4px -2px", + "inherit 2px", + "2px inherit", + "2", + "2px 2", + "2 2px", + "2px calc(0px + rubbish)", + ], + }, "-moz-border-right-colors": { domProp: "MozBorderRightColors", inherited: false, @@ -8903,6 +9051,54 @@ function logical_box_prop_get_computed(cs, property) { return cs.getPropertyValue(property); } +function logical_corner_prop_get_computed(cs, property) { + // Use default for writing-mode in case the vertical text + // pref (which it lives behind) is turned off. + var writingMode = cs.getPropertyValue("writing-mode") || "horizontal-tb"; + var direction = cs.getPropertyValue("direction"); + + var blockMappings = { + "horizontal-tb": { start: "top", end: "bottom" }, + "vertical-rl": { start: "right", end: "left" }, + "vertical-lr": { start: "left", end: "right" }, + "sideways-rl": { start: "right", end: "left" }, + "sideways-lr": { start: "left", end: "right" }, + }; + + var inlineMappings = { + "horizontal-tb ltr": { start: "left", end: "right" }, + "horizontal-tb rtl": { start: "right", end: "left" }, + "vertical-.. ltr": { start: "top", end: "bottom" }, + "vertical-.. rtl": { start: "bottom", end: "top" }, + "sideways-lr ltr": { start: "bottom", end: "top" }, + "sideways-lr rtl": { start: "top", end: "bottom" }, + "sideways-rl ltr": { start: "top", end: "bottom" }, + "sideways-rl rtl": { start: "bottom", end: "top" }, + }; + + var blockMapping = blockMappings[writingMode]; + var inlineMapping; + + var key = `${writingMode} ${direction}`; + for (var k in inlineMappings) { + if (new RegExp(k).test(key)) { + inlineMapping = inlineMappings[k]; + break; + } + } + + var match = /^border-(start|end)-(start|end)-radius$/.exec(property); + if (!match || !blockMapping || !inlineMapping) { + throw "Unexpected logical corner property"; + } + + var blockSide = blockMapping[match[1]]; + var inlineSide = inlineMapping[match[2]]; + var verticalSide = /^(top|bottom)$/.test(blockSide) ? blockSide : inlineSide; + var horizontalSide = /^(left|right)$/.test(blockSide) ? blockSide : inlineSide; + return cs.getPropertyValue(`border-${verticalSide}-${horizontalSide}-radius`); +} + // Get the computed value for a property. For shorthands, return the // computed values of all the subproperties, delimited by " ; ". function get_computed_value(cs, property) { @@ -11555,6 +11751,22 @@ if (IsCSSPropertyPrefEnabled("layout.css.unset-value.enabled")) { "unset 2px", "2px unset", ); + gCSSProperties["border-end-end-radius"].invalid_values.push( + "unset 2px", + "2px unset", + ); + gCSSProperties["border-end-start-radius"].invalid_values.push( + "unset 2px", + "2px unset", + ); + gCSSProperties["border-start-end-radius"].invalid_values.push( + "unset 2px", + "2px unset", + ); + gCSSProperties["border-start-start-radius"].invalid_values.push( + "unset 2px", + "2px unset", + ); gCSSProperties["-moz-border-right-colors"].invalid_values.push( "red unset", "unset red", diff --git a/layout/style/test/test_logical_border_radius.html b/layout/style/test/test_logical_border_radius.html new file mode 100644 index 0000000000..881fad8ecf --- /dev/null +++ b/layout/style/test/test_logical_border_radius.html @@ -0,0 +1,76 @@ + + +Test logical border radius properties + + + +
+ + From 4634a74b31690bff350dbd58cee5b2771aae34e4 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Mon, 11 May 2026 15:16:48 -0400 Subject: [PATCH 18/30] Issue #1826 - Parse calc() weights in color-mix --- layout/style/nsCSSParser.cpp | 68 ++++++++++++++-------- layout/style/test/test_computed_style.html | 2 + 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index 0a1bf33e29..c962ea04d2 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -1553,6 +1553,7 @@ protected: const nsCSSPropertyID aPropIDs[], int32_t aNumIDs); CSSParseResult ParseColor(nsCSSValue& aValue); + CSSParseResult ParseColorMixPercentage(float& aWeight); template bool ParseRGBColor(ComponentType& aR, @@ -7740,6 +7741,37 @@ CSSParserImpl::ParseDeclarationBlock(uint32_t aFlags, nsCSSContextType aContext) return declaration.forget(); } +CSSParseResult +CSSParserImpl::ParseColorMixPercentage(float& aWeight) +{ + if (!GetToken(true)) { + return CSSParseResult::NotFound; + } + + if (mToken.mType == eCSSToken_Percentage) { + aWeight = mToken.mNumber; + } else if (IsCalcFunctionToken(mToken)) { + nsCSSValue calcValue; + uint32_t calcResultVariantMask = VARIANT_PERCENT; + if (!ParseCalc(calcValue, VARIANT_PERCENT, &calcResultVariantMask) || + !(calcResultVariantMask & VARIANT_PERCENT)) { + return CSSParseResult::Error; + } + + ReducePercentageCalcOps ops; + aWeight = mozilla::css::ComputeCalc(calcValue, ops); + } else { + UngetToken(); + return CSSParseResult::NotFound; + } + + if (aWeight < 0.0f || aWeight > 1.0f) { + return CSSParseResult::Error; + } + + return CSSParseResult::Ok; +} + CSSParseResult CSSParserImpl::ParseColor(nsCSSValue& aValue) { @@ -7841,18 +7873,12 @@ CSSParserImpl::ParseColor(nsCSSValue& aValue) // parse optional weight for first color bool w1_specified = false; float w1 = 0.5f; // Default to 50% - if (GetToken(true)) { - if (mToken.mType == eCSSToken_Percentage) { - w1 = mToken.mNumber; // percentage tokens are already normalized (0.0-1.0) - w1_specified = true; - // Reject invalid percentages (outside 0-100% range) - if (w1 < 0.0f || w1 > 1.0f) { - SkipUntil(')'); - return CSSParseResult::Error; - } - } else { - UngetToken(); - } + CSSParseResult weightResult = ParseColorMixPercentage(w1); + if (weightResult == CSSParseResult::Ok) { + w1_specified = true; + } else if (weightResult == CSSParseResult::Error) { + SkipUntil(')'); + return CSSParseResult::Error; } if (!ExpectSymbol(',', true)) { @@ -7869,18 +7895,12 @@ CSSParserImpl::ParseColor(nsCSSValue& aValue) // parse optional weight for second color bool w2_specified = false; float w2 = 0.5f; // default to 50% - if (GetToken(true)) { - if (mToken.mType == eCSSToken_Percentage) { - w2 = mToken.mNumber; // percentage tokens are already normalized (0.0-1.0) - w2_specified = true; - // Reject invalid percentages (outside 0-100% range) - if (w2 < 0.0f || w2 > 1.0f) { - SkipUntil(')'); - return CSSParseResult::Error; - } - } else { - UngetToken(); - } + weightResult = ParseColorMixPercentage(w2); + if (weightResult == CSSParseResult::Ok) { + w2_specified = true; + } else if (weightResult == CSSParseResult::Error) { + SkipUntil(')'); + return CSSParseResult::Error; } if (w1_specified && !w2_specified) { diff --git a/layout/style/test/test_computed_style.html b/layout/style/test/test_computed_style.html index 5363e1dac5..f3951bc50b 100644 --- a/layout/style/test/test_computed_style.html +++ b/layout/style/test/test_computed_style.html @@ -417,6 +417,8 @@ var noframe_container = document.getElementById("content"); ["color-mix(in oklab, red, blue)", "rgb(140, 83, 162)"], ["color-mix(in oklch, red, blue)", "rgb(183, 0, 190)"], ["color-mix(in oklab, red, transparent)", "rgba(255, 0, 0, 0.5)"], + ["color-mix(in srgb, red, transparent calc(100% - 100%*1))", "rgb(255, 0, 0)"], + ["color-mix(in srgb, red, transparent calc(50%))", "rgba(255, 0, 0, 0.5)"], ["color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 0.7))", "rgb(83, 24, 0)"], ["color-mix(in oklch, oklch(0.1 0.2 30), oklch(0.5 0.6 70))", "rgb(84, 23, 0)"], ]; From 6df85ff502b10809c1cfc0d929dfbe58c61f6581 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Mon, 11 May 2026 17:54:07 -0400 Subject: [PATCH 19/30] Issue #2506 - Support range media query syntax Resolves #2506 --- layout/style/CSSStyleSheet.cpp | 12 +++++- layout/style/nsCSSParser.cpp | 45 +++++++++++++++++++++-- layout/style/nsIMediaList.h | 2 +- layout/style/test/test_media_queries.html | 9 +++++ 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/layout/style/CSSStyleSheet.cpp b/layout/style/CSSStyleSheet.cpp index e6351a6ba2..8ecf9c85ee 100644 --- a/layout/style/CSSStyleSheet.cpp +++ b/layout/style/CSSStyleSheet.cpp @@ -436,6 +436,10 @@ nsMediaExpression::Matches(nsPresContext *aPresContext, return cmp != 1; case nsMediaExpression::eEqual: return cmp == 0; + case nsMediaExpression::eMinExclusive: + return cmp == 1; + case nsMediaExpression::eMaxExclusive: + return cmp == -1; } NS_NOTREACHED("unexpected mRange"); return false; @@ -640,7 +644,13 @@ nsMediaQuery::AppendToString(nsAString& aString) const aString.Append(nsDependentAtomString(*feature->mName)); if (expr.mValue.GetUnit() != eCSSUnit_Null) { - aString.AppendLiteral(": "); + if (expr.mRange == nsMediaExpression::eMinExclusive) { + aString.AppendLiteral(" > "); + } else if (expr.mRange == nsMediaExpression::eMaxExclusive) { + aString.AppendLiteral(" < "); + } else { + aString.AppendLiteral(": "); + } switch (feature->mValueType) { case nsMediaFeature::eLength: NS_ASSERTION(expr.mValue.IsLengthUnit() || expr.mValue.IsCalcUnit(), diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index c962ea04d2..06400ab7ce 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -4149,9 +4149,7 @@ CSSParserImpl::ParseMediaQueryExpression(nsMediaQuery* aQuery) break; } } - if (!feature->mName || - (expr->mRange != nsMediaExpression::eEqual && - feature->mRangeType != nsMediaFeature::eMinMaxAllowed)) { + if (!feature->mName) { REPORT_UNEXPECTED_TOKEN(PEMQExpectedFeatureName); SkipUntil(')'); return false; @@ -4169,13 +4167,52 @@ CSSParserImpl::ParseMediaQueryExpression(nsMediaQuery* aQuery) return true; } - if (!mToken.IsSymbol(':')) { + bool usesRangeSyntax = false; + if (mToken.IsSymbol('<') || mToken.IsSymbol('>') || mToken.IsSymbol('=')) { + if (expr->mRange != nsMediaExpression::eEqual) { + REPORT_UNEXPECTED_TOKEN(PEMQExpectedFeatureNameEnd); + UngetToken(); + SkipUntil(')'); + return false; + } + + usesRangeSyntax = true; + char16_t rangeSymbol = mToken.mSymbol; + bool inclusive = rangeSymbol == '='; + + if (rangeSymbol != '=') { + if (!GetToken(true)) { + REPORT_UNEXPECTED_EOF(PEMQExpressionEOF); + return false; + } + if (mToken.IsSymbol('=')) { + inclusive = true; + } else { + UngetToken(); + } + } + + if (rangeSymbol == '>') { + expr->mRange = inclusive ? nsMediaExpression::eMin + : nsMediaExpression::eMinExclusive; + } else if (rangeSymbol == '<') { + expr->mRange = inclusive ? nsMediaExpression::eMax + : nsMediaExpression::eMaxExclusive; + } + } else if (!mToken.IsSymbol(':')) { REPORT_UNEXPECTED_TOKEN(PEMQExpectedFeatureNameEnd); UngetToken(); SkipUntil(')'); return false; } + if ((expr->mRange != nsMediaExpression::eEqual || usesRangeSyntax) && + feature->mRangeType != nsMediaFeature::eMinMaxAllowed) { + REPORT_UNEXPECTED_TOKEN(PEMQExpectedFeatureName); + SkipUntil(')'); + return false; + } + bool rv = false; switch (feature->mValueType) { case nsMediaFeature::eLength: diff --git a/layout/style/nsIMediaList.h b/layout/style/nsIMediaList.h index 8f8c692cd6..52244b48f4 100644 --- a/layout/style/nsIMediaList.h +++ b/layout/style/nsIMediaList.h @@ -33,7 +33,7 @@ class DocumentRule; } // namespace mozilla struct nsMediaExpression { - enum Range { eMin, eMax, eEqual }; + enum Range { eMin, eMax, eEqual, eMinExclusive, eMaxExclusive }; const nsMediaFeature *mFeature; Range mRange; diff --git a/layout/style/test/test_media_queries.html b/layout/style/test/test_media_queries.html index fc5da98f9d..fddfabb3dd 100644 --- a/layout/style/test/test_media_queries.html +++ b/layout/style/test/test_media_queries.html @@ -297,6 +297,12 @@ function run() { should_apply("all and (max-" + feature + ": " + value + "px)"); should_apply("all and (max-" + feature + ": " + (value + 1) + "px)"); should_not_apply("all and (max-" + feature + ": " + (value - 1) + "px)"); + should_apply("all and (" + feature + " >= " + value + "px)"); + should_not_apply("all and (" + feature + " > " + value + "px)"); + should_apply("all and (" + feature + " > " + (value - 1) + "px)"); + should_apply("all and (" + feature + " <= " + value + "px)"); + should_not_apply("all and (" + feature + " < " + value + "px)"); + should_apply("all and (" + feature + " < " + (value + 1) + "px)"); should_not_apply("all and (min-" + feature + ": " + (Math.ceil(value/em_size) + 1) + "em)"); should_apply("all and (min-" + feature + ": " + @@ -371,6 +377,9 @@ function run() { should_apply("(orientation: landscape)"); should_not_apply("(orientation: portrait)"); should_apply("not all and (orientation: portrait)"); + expression_should_not_be_parseable("orientation > landscape"); + expression_should_not_be_parseable("min-width > 1px"); + expression_should_not_be_parseable("width >"); // ratio that reduces to 59/80 change_state(function() { iframe_style.height = "320px"; From 51767db33a19fece1c42ee1381604cb46a8ee74d Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Mon, 11 May 2026 19:33:30 -0400 Subject: [PATCH 20/30] Support CSS shadow parts --- dom/base/nsGkAtomList.h | 1 + dom/xbl/nsBindingManager.cpp | 35 ++++++++++--- layout/style/RuleCascadeData.cpp | 39 +++++++++++++-- layout/style/StyleRule.cpp | 1 + layout/style/nsCSSParser.cpp | 32 +++++++++--- layout/style/nsCSSPseudoClassList.h | 3 ++ layout/style/nsCSSPseudoClasses.cpp | 6 ++- layout/style/nsCSSPseudoElementList.h | 7 ++- layout/style/nsCSSPseudoElements.cpp | 3 +- layout/style/nsCSSRuleUtils.cpp | 72 +++++++++++++++++++++++---- 10 files changed, 167 insertions(+), 32 deletions(-) diff --git a/dom/base/nsGkAtomList.h b/dom/base/nsGkAtomList.h index 95e15b1f18..7f4c3ba01d 100644 --- a/dom/base/nsGkAtomList.h +++ b/dom/base/nsGkAtomList.h @@ -1005,6 +1005,7 @@ GK_ATOM(parameter, "parameter") GK_ATOM(parent, "parent") GK_ATOM(parentfocused, "parentfocused") GK_ATOM(parsetype, "parsetype") +GK_ATOM(part, "part") GK_ATOM(password, "password") GK_ATOM(pattern, "pattern") GK_ATOM(patternSeparator, "pattern-separator") diff --git a/dom/xbl/nsBindingManager.cpp b/dom/xbl/nsBindingManager.cpp index 5245821776..e44d29fa83 100644 --- a/dom/xbl/nsBindingManager.cpp +++ b/dom/xbl/nsBindingManager.cpp @@ -823,16 +823,37 @@ nsBindingManager::WalkAllRules(nsIStyleRuleProcessor::EnumFunc aFunc, } } -// The approach in WalkAllShadowRootHostRules seems reasonable on the surface and reminds me of what is done with mScopedRoot elsewhere. But something important is missing, either here or in the code that would normally use it. - void nsBindingManager::WalkAllShadowRootHostRules(nsIStyleRuleProcessor::EnumFunc aFunc, ElementDependentRuleProcessorData* aData) - { - aData->mTreeMatchContext.mOnlyMatchHostPseudo = true; - WalkAllRules(aFunc, aData, true); - aData->mTreeMatchContext.mOnlyMatchHostPseudo = false; - } +{ + if (!mBoundContentSet) { + return; + } + + bool oldOnlyMatchHostPseudo = aData->mTreeMatchContext.mOnlyMatchHostPseudo; + nsIContent* oldScopedRoot = aData->mTreeMatchContext.mScopedRoot; + aData->mTreeMatchContext.mOnlyMatchHostPseudo = true; + + for (auto iter = mBoundContentSet->Iter(); !iter.Done(); iter.Next()) { + nsIContent* boundContent = iter.Get()->GetKey(); + ShadowRoot* shadowRoot = boundContent->GetShadowRoot(); + if (!shadowRoot) { + continue; + } + + nsXBLBinding* binding = shadowRoot->GetAssociatedBinding(); + if (!binding) { + continue; + } + + aData->mTreeMatchContext.mScopedRoot = boundContent; + binding->WalkRules(aFunc, aData); + } + + aData->mTreeMatchContext.mScopedRoot = oldScopedRoot; + aData->mTreeMatchContext.mOnlyMatchHostPseudo = oldOnlyMatchHostPseudo; +} nsresult nsBindingManager::MediumFeaturesChanged(nsPresContext* aPresContext, diff --git a/layout/style/RuleCascadeData.cpp b/layout/style/RuleCascadeData.cpp index da6a4e70de..20856c69f0 100644 --- a/layout/style/RuleCascadeData.cpp +++ b/layout/style/RuleCascadeData.cpp @@ -404,6 +404,22 @@ RuleHash::AppendUniversalRule(const RuleSelectorPair& aRuleInfo) RuleValue(aRuleInfo, mRuleCount++, mQuirksMode)); } +static bool +SelectorContainsPseudoClass(nsCSSSelector* aSelector, CSSPseudoClassType aType) +{ + for (nsCSSSelector* selector = aSelector; selector; selector = selector->mNext) { + for (nsPseudoClassList* pseudoClass = selector->mPseudoClassList; + pseudoClass; + pseudoClass = pseudoClass->mNext) { + if (pseudoClass->mType == aType) { + return true; + } + } + } + + return false; +} + void RuleHash::AppendRule(const RuleSelectorPair& aRuleInfo) { @@ -411,7 +427,10 @@ RuleHash::AppendRule(const RuleSelectorPair& aRuleInfo) if (selector->IsPseudoElement()) { selector = selector->mNext; } - if (nullptr != selector->mIDList) { + if (SelectorContainsPseudoClass(selector, CSSPseudoClassType::part)) { + AppendUniversalRule(aRuleInfo); + RULE_HASH_STAT_INCREMENT(mUniversalSelectors); + } else if (nullptr != selector->mIDList) { AppendRuleToTable(&mIdTable, selector->mIDList->mAtom, aRuleInfo); RULE_HASH_STAT_INCREMENT(mIdSelectors); } else if (nullptr != selector->mClassList) { @@ -452,6 +471,10 @@ LookForTargetPseudo(nsCSSSelector* aSelector, nsRestyleHint* possibleChange) { if (aMatchContext->mOnlyMatchHostPseudo) { + if (SelectorContainsPseudoClass(aSelector, CSSPseudoClassType::part)) { + return true; + } + while (aSelector && aSelector->mNext != nullptr) { aSelector = aSelector->mNext; } @@ -502,7 +525,8 @@ ContentEnumFunc(const RuleValue& value, data->mTreeMatchContext.SetHaveRelevantLink(); } // XXX: Ignore the ancestor filter if we're testing the assigned slot. - bool useAncestorFilter = !(data->mTreeMatchContext.mForAssignedSlot); + bool useAncestorFilter = !(data->mTreeMatchContext.mForAssignedSlot || + data->mTreeMatchContext.mOnlyMatchHostPseudo); if (useAncestorFilter && ancestorFilter && !ancestorFilter->MightHaveMatchingAncestor( value.mAncestorSelectorHashes)) { @@ -556,9 +580,18 @@ ContentEnumFunc(const RuleValue& value, nodeContext, data->mTreeMatchContext, selectorFlags)) { + Element* treeMatchStart = data->mElement; + if (SelectorContainsPseudoClass(selector, CSSPseudoClassType::part)) { + nsIContent* partHost = data->mElement->GetContainingShadowHost(); + if (!partHost || !partHost->IsElement()) { + return; + } + treeMatchStart = partHost->AsElement(); + } + nsCSSSelector* next = selector->mNext; if (!next || nsCSSRuleUtils::SelectorMatchesTree( - data->mElement, + treeMatchStart, next, data->mTreeMatchContext, nodeContext.mIsRelevantLink ? SelectorMatchesTreeFlags(0) diff --git a/layout/style/StyleRule.cpp b/layout/style/StyleRule.cpp index 42aa43be4a..9135eefce8 100644 --- a/layout/style/StyleRule.cpp +++ b/layout/style/StyleRule.cpp @@ -335,6 +335,7 @@ nsCSSSelector::Clone(bool aDeepNext, bool aDeepNegations) const result->mCasedTag = mCasedTag; result->mOperator = mOperator; result->mPseudoType = mPseudoType; + result->mHybridPseudoType = mHybridPseudoType; NS_IF_CLONE(mIDList); NS_IF_CLONE(mClassList); diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index 06400ab7ce..246bf65b8f 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -6998,6 +6998,8 @@ CSSParserImpl::ParsePseudoSelector(int32_t& aDataMask, if (hybridPseudoElementType == CSSPseudoElementType::slotted) { pseudoClassType = CSSPseudoClassType::slotted; aFlags |= SelectorParsingFlags::eDisallowCombinators; + } else if (hybridPseudoElementType == CSSPseudoElementType::part) { + pseudoClassType = CSSPseudoClassType::part; } } @@ -7150,6 +7152,28 @@ CSSParserImpl::ParsePseudoSelector(int32_t& aDataMask, UngetToken(); return eSelectorParsingStatus_Error; } + else if (hybridPseudoElementType != CSSPseudoElementType::NotPseudo) { + aSelector.SetHybridPseudoType(hybridPseudoElementType); + // Ensure hybrid pseudo-elements are rejected if they're not allowed. + if (disallowPseudoElements) { + UngetToken(); + return eSelectorParsingStatus_Error; + } + + if (nsCSSPseudoClasses::HasStringArg(pseudoClassType)) { + parsingStatus = + ParsePseudoClassWithIdentArg(aSelector, pseudoClassType); + } + else if (nsCSSPseudoClasses::HasSelectorListArg(pseudoClassType)) { + parsingStatus = ParsePseudoClassWithSelectorListArg(aSelector, + pseudoClassType, + flags); + } + else { + MOZ_ASSERT(false, "unexpected hybrid pseudo-element"); + parsingStatus = eSelectorParsingStatus_Error; + } + } else if (nsCSSPseudoClasses::HasStringArg(pseudoClassType)) { parsingStatus = ParsePseudoClassWithIdentArg(aSelector, pseudoClassType); @@ -7161,14 +7185,6 @@ CSSParserImpl::ParsePseudoSelector(int32_t& aDataMask, else { MOZ_ASSERT(nsCSSPseudoClasses::HasSelectorListArg(pseudoClassType), "unexpected pseudo with function token"); - if (hybridPseudoElementType != CSSPseudoElementType::NotPseudo) { - aSelector.SetHybridPseudoType(hybridPseudoElementType); - // Ensure hybrid pseudo-elements are rejected if they're not allowed. - if (disallowPseudoElements) { - UngetToken(); - return eSelectorParsingStatus_Error; - } - } parsingStatus = ParsePseudoClassWithSelectorListArg(aSelector, pseudoClassType, flags); diff --git a/layout/style/nsCSSPseudoClassList.h b/layout/style/nsCSSPseudoClassList.h index f1ab538357..ab70f39ce5 100644 --- a/layout/style/nsCSSPseudoClassList.h +++ b/layout/style/nsCSSPseudoClassList.h @@ -94,6 +94,9 @@ CSS_PSEUDO_CLASS(nthLastOfType, ":nth-last-of-type", 0, "") // Match slot nodes. CSS_PSEUDO_CLASS(slotted, ":slotted", 0, "layout.css.slotted-pseudo.enabled") +// Match elements exposed through a shadow host's part attribute. +CSS_PSEUDO_CLASS(part, ":part", 0, "dom.webcomponents.enabled") + // Match nodes that are HTML but not XHTML CSS_PSEUDO_CLASS(mozIsHTML, ":-moz-is-html", 0, "") diff --git a/layout/style/nsCSSPseudoClasses.cpp b/layout/style/nsCSSPseudoClasses.cpp index 8e12a9648c..c3681fc518 100644 --- a/layout/style/nsCSSPseudoClasses.cpp +++ b/layout/style/nsCSSPseudoClasses.cpp @@ -125,7 +125,8 @@ nsCSSPseudoClasses::HasStringArg(Type aType) aType == Type::mozSystemMetric || aType == Type::mozLocaleDir || aType == Type::mozDir || - aType == Type::dir; + aType == Type::dir || + aType == Type::part; } bool @@ -210,5 +211,6 @@ nsCSSPseudoClasses::IsUserActionPseudoClass(Type aType) /* static */ bool nsCSSPseudoClasses::IsHybridPseudoElement(Type aType) { - return aType == Type::slotted; + return aType == Type::slotted || + aType == Type::part; } diff --git a/layout/style/nsCSSPseudoElementList.h b/layout/style/nsCSSPseudoElementList.h index 6693d42e75..881c9bce37 100644 --- a/layout/style/nsCSSPseudoElementList.h +++ b/layout/style/nsCSSPseudoElementList.h @@ -28,10 +28,13 @@ CSS_PSEUDO_ELEMENT(after, ":after", CSS_PSEUDO_ELEMENT_IS_CSS2) CSS_PSEUDO_ELEMENT(before, ":before", CSS_PSEUDO_ELEMENT_IS_CSS2) -// XXX: ::slotted() is treated as if it were a pseudo-class, and -// is never parsed as a pseudo-element. +// XXX: ::slotted() and ::part() are treated as if they were pseudo-classes, +// and are never parsed as pseudo-elements. CSS_PSEUDO_ELEMENT(slotted, ":slotted", CSS_PSEUDO_ELEMENT_SUPPORTS_TREE_ABIDING) +CSS_PSEUDO_ELEMENT(part, ":part", + CSS_PSEUDO_ELEMENT_SUPPORTS_TREE_ABIDING | + CSS_PSEUDO_ELEMENT_SUPPORTS_USER_ACTION_STATE) CSS_PSEUDO_ELEMENT(backdrop, ":backdrop", 0) diff --git a/layout/style/nsCSSPseudoElements.cpp b/layout/style/nsCSSPseudoElements.cpp index fb871ff858..6053bd39e4 100644 --- a/layout/style/nsCSSPseudoElements.cpp +++ b/layout/style/nsCSSPseudoElements.cpp @@ -78,7 +78,8 @@ nsCSSPseudoElements::IsCSS2PseudoElement(nsIAtom *aAtom) /* static */ bool nsCSSPseudoElements::IsHybridPseudoElement(CSSPseudoElementType aType) { - return aType == CSSPseudoElementType::slotted; + return aType == CSSPseudoElementType::slotted || + aType == CSSPseudoElementType::part; } /* static */ bool diff --git a/layout/style/nsCSSRuleUtils.cpp b/layout/style/nsCSSRuleUtils.cpp index 1b987cbab8..8c944c06c8 100644 --- a/layout/style/nsCSSRuleUtils.cpp +++ b/layout/style/nsCSSRuleUtils.cpp @@ -9,6 +9,7 @@ #include "mozilla/dom/HTMLSlotElement.h" #include "mozilla/dom/ShadowRoot.h" #include "nsIMozBrowserFrame.h" +#include "nsGkAtoms.h" #include "nsRuleWalker.h" #include "nsStyleUtil.h" #include "StyleRule.h" @@ -188,6 +189,36 @@ InitSystemMetrics() return true; } +static nsPseudoClassList* +FindPseudoClass(const nsCSSSelector* aSelector, CSSPseudoClassType aType) +{ + for (nsPseudoClassList* pseudoClass = aSelector->mPseudoClassList; + pseudoClass; + pseudoClass = pseudoClass->mNext) { + if (pseudoClass->mType == aType) { + return pseudoClass; + } + } + + return nullptr; +} + +static bool +ElementMatchesPart(Element* aElement, const nsPseudoClassList* aPseudoClass) +{ + MOZ_ASSERT(aPseudoClass->mType == CSSPseudoClassType::part); + MOZ_ASSERT(aPseudoClass->u.mString); + + nsAutoString partValue; + if (!aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::part, partValue)) { + return false; + } + + const nsDefaultStringComparator comparator; + return nsStyleUtil::ValueIncludes( + partValue, nsDependentString(aPseudoClass->u.mString), comparator); +} + /* static */ void nsCSSRuleUtils::FreeSystemMetrics() { @@ -640,7 +671,10 @@ nsCSSRuleUtils::SelectorMatches(Element* aElement, "is false since we don't know how to set it correctly in " "Has(Attribute|State)DependentStyle"); - if (aNodeMatchContext.mIsFeatureless && + nsPseudoClassList* partPseudo = + FindPseudoClass(aSelector, CSSPseudoClassType::part); + + if (aNodeMatchContext.mIsFeatureless && !partPseudo && !CanMatchFeaturelessElement(aSelector)) { return false; } @@ -656,6 +690,27 @@ nsCSSRuleUtils::SelectorMatches(Element* aElement, targetElement = slot->AsElement(); } + if (partPseudo) { + if (!ElementMatchesPart(aElement, partPseudo)) { + return false; + } + + nsIContent* partHostContent = aElement->GetContainingShadowHost(); + if (!partHostContent || !partHostContent->IsElement()) { + return false; + } + + Element* partHost = partHostContent->AsElement(); + if (aTreeMatchContext.mScopedRoot) { + ShadowRoot* scopedShadow = aTreeMatchContext.mScopedRoot->GetShadowRoot(); + if (!scopedShadow || partHost->GetContainingShadow() != scopedShadow) { + return false; + } + } + + targetElement = partHost; + } + // namespace/tag match // optimization : bail out early if we can if ((kNameSpaceID_Unknown != aSelector->mNameSpace && @@ -915,6 +970,9 @@ nsCSSRuleUtils::SelectorMatches(Element* aElement, } } break; + case CSSPseudoClassType::part: + break; + case CSSPseudoClassType::host: { ShadowRoot* shadow = aElement->GetShadowRoot(); // In order to match :host, the element must be a shadow root host, @@ -927,15 +985,11 @@ nsCSSRuleUtils::SelectorMatches(Element* aElement, return false; } - // We're matching :host from inside the shadow root. - if (!aTreeMatchContext.mOnlyMatchHostPseudo) { - // Check if the element has the same shadow root. - if (aTreeMatchContext.mScopedRoot) { - if (shadow != aTreeMatchContext.mScopedRoot->GetShadowRoot()) { - return false; - } + // Check if the element has the same shadow root. + if (aTreeMatchContext.mScopedRoot) { + if (shadow != aTreeMatchContext.mScopedRoot->GetShadowRoot()) { + return false; } - // We were called elsewhere. } // Reject if the next selector is an explicit universal selector. From 235bcb010b540136080bd013a1813a40fa345c57 Mon Sep 17 00:00:00 2001 From: wuggy Date: Wed, 13 May 2026 14:08:47 -0700 Subject: [PATCH 21/30] Whitelist virtual GPUs (VirtualBox, VMware, VirtIO (QEMU and forks like UTM), Parallels) --- widget/windows/GfxInfo.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/widget/windows/GfxInfo.cpp b/widget/windows/GfxInfo.cpp index 5789cfd1d0..e6e029b16a 100644 --- a/widget/windows/GfxInfo.cpp +++ b/widget/windows/GfxInfo.cpp @@ -1188,11 +1188,16 @@ GfxInfo::GetFeatureStatusImpl(int32_t aFeature, !adapterVendorID.Equals(GfxDriverInfo::GetDeviceVendor(VendorMicrosoft), nsCaseInsensitiveStringComparator()) && // FIXME - these special hex values are currently used in xpcshell tests introduced by // bug 625160 patch 8/8. Maybe these tests need to be adjusted now that we're only whitelisting - // intel/ati/nvidia. + // intel/ati/nvidia. Also allow common virtual GPU vendor IDs (VirtualBox, VMware, QEMU/virtio, Parallels). !adapterVendorID.LowerCaseEqualsLiteral("0xabcd") && !adapterVendorID.LowerCaseEqualsLiteral("0xdcba") && !adapterVendorID.LowerCaseEqualsLiteral("0xabab") && - !adapterVendorID.LowerCaseEqualsLiteral("0xdcdc")) + !adapterVendorID.LowerCaseEqualsLiteral("0xdcdc") && + !adapterVendorID.LowerCaseEqualsLiteral("0x80ee") && /* VirtualBox */ + !adapterVendorID.LowerCaseEqualsLiteral("0x15ad") && /* VMware */ + !adapterVendorID.LowerCaseEqualsLiteral("0x1234") && /* QEMU (common) */ + !adapterVendorID.LowerCaseEqualsLiteral("0x1af4") && /* virtio/QEMU */ + !adapterVendorID.LowerCaseEqualsLiteral("0x1ab8") ) /* Parallels */ { aFailureId = "FEATURE_FAILURE_UNKNOWN_DEVICE_VENDOR"; *aStatus = FEATURE_BLOCKED_DEVICE; From a7a75b785192f86e5e21a0a7b436030f2ea68d5b Mon Sep 17 00:00:00 2001 From: ownedbywuigi Date: Sun, 10 May 2026 06:29:18 -0700 Subject: [PATCH 22/30] Issue #3092 - Refactor WASM compilation handling --- js/src/vm/HelperThreads.cpp | 36 +++++++++---------- js/src/vm/HelperThreads.h | 4 +-- js/src/wasm/WasmGenerator.cpp | 68 +++++++++++++++++++---------------- js/src/wasm/WasmGenerator.h | 2 ++ js/src/wasm/WasmIonCompile.h | 23 ++++++++++++ 5 files changed, 81 insertions(+), 52 deletions(-) diff --git a/js/src/vm/HelperThreads.cpp b/js/src/vm/HelperThreads.cpp index 9c36dce216..e07d0e6dd6 100644 --- a/js/src/vm/HelperThreads.cpp +++ b/js/src/vm/HelperThreads.cpp @@ -49,10 +49,6 @@ js::CreateHelperThreadsState() { MOZ_ASSERT(!gHelperThreadState); gHelperThreadState = js_new(); - return gHelperThreadState != nullptr; -} - -void js::DestroyHelperThreadsState() { MOZ_ASSERT(gHelperThreadState); @@ -477,14 +473,17 @@ js::CancelOffThreadParses(JSRuntime* rt) HelperThreadState().wait(lock, GlobalHelperThreadState::CONSUMER); } + return gHelperThreadState != nullptr; + } + + void + js::DestroyHelperThreadsState() + { + MOZ_ASSERT(gHelperThreadState); + gHelperThreadState->finish(); + js_delete(gHelperThreadState); + gHelperThreadState = nullptr; // Clean up any parse tasks which haven't been finished by the main thread. - GlobalHelperThreadState::ParseTaskVector& finished = HelperThreadState().parseFinishedList(lock); - while (true) { - bool found = false; - for (size_t i = 0; i < finished.length(); i++) { - ParseTask* task = finished[i]; - if (task->runtimeMatches(rt)) { - found = true; AutoUnlockHelperThreadState unlock(lock); HelperThreadState().cancelParseTask(rt->contextFromMainThread(), task->kind, task); } @@ -972,8 +971,7 @@ GlobalHelperThreadState::maxGCParallelThreads() const bool GlobalHelperThreadState::canStartWasmCompile(const AutoLockHelperThreadState& lock) { - // Don't execute an wasm job if an earlier one failed. - if (wasmWorklist(lock).empty() || numWasmFailedJobs) + if (wasmWorklist(lock).empty()) return false; // Honor the maximum allowed threads to compile wasm jobs at once, @@ -1423,13 +1421,13 @@ HelperThread::handleWasmWorkload(AutoLockHelperThreadState& locked) success = wasm::CompileFunction(task); } - // On success, try to move work to the finished list. - if (success) - success = HelperThreadState().wasmFinishedList(locked).append(task); - - // On failure, note the failure for harvesting by the parent. + // Append the task to the finished queue owned by its module generator. if (!success) - HelperThreadState().noteWasmFailure(locked); + task->setFailed(); + + AutoEnterOOMUnsafeRegion oomUnsafe; + if (!task->finishedList()->append(task)) + oomUnsafe.crash("HelperThread::handleWasmWorkload"); // Notify the main thread in case it's waiting. HelperThreadState().notifyAll(GlobalHelperThreadState::CONSUMER, locked); diff --git a/js/src/vm/HelperThreads.h b/js/src/vm/HelperThreads.h index 14cfa80396..6763f57b5d 100644 --- a/js/src/vm/HelperThreads.h +++ b/js/src/vm/HelperThreads.h @@ -89,8 +89,8 @@ class GlobalHelperThreadState wasm::IonCompileTaskPtrVector wasmWorklist_, wasmFinishedList_; public: - // For now, only allow a single parallel wasm compilation to happen at a - // time. This avoids race conditions on wasmWorklist/wasmFinishedList/etc. + // Helper-thread initiated wasm compilations are serialized to avoid the + // deadlock scenario described in WasmGenerator.cpp. mozilla::Atomic wasmCompilationInProgress; private: diff --git a/js/src/wasm/WasmGenerator.cpp b/js/src/wasm/WasmGenerator.cpp index daff135077..8bf10cc4ef 100644 --- a/js/src/wasm/WasmGenerator.cpp +++ b/js/src/wasm/WasmGenerator.cpp @@ -54,6 +54,7 @@ ModuleGenerator::ModuleGenerator(ImportVector&& imports) lastPatchedCallsite_(0), startOfUnpatchedCallsites_(0), parallel_(false), + parallelCompilationInProgressOnHelperThread_(false), outstanding_(0), activeFuncDef_(nullptr), startedFuncDefs_(false), @@ -71,18 +72,21 @@ ModuleGenerator::~ModuleGenerator() AutoLockHelperThreadState lock; while (true) { IonCompileTaskPtrVector& worklist = HelperThreadState().wasmWorklist(lock); - MOZ_ASSERT(outstanding_ >= worklist.length()); - outstanding_ -= worklist.length(); - worklist.clear(); + for (size_t i = worklist.length(); i > 0;) { + if (worklist[i - 1]->finishedList() == &finishedTasks_) { + HelperThreadState().remove(worklist, &i); + MOZ_ASSERT(outstanding_ > 0); + outstanding_--; + } else { + i--; + } + } - IonCompileTaskPtrVector& finished = HelperThreadState().wasmFinishedList(lock); - MOZ_ASSERT(outstanding_ >= finished.length()); - outstanding_ -= finished.length(); - finished.clear(); - - uint32_t numFailed = HelperThreadState().harvestFailedWasmJobs(lock); - MOZ_ASSERT(outstanding_ >= numFailed); - outstanding_ -= numFailed; + for (size_t i = finishedTasks_.length(); i > 0;) { + HelperThreadState().remove(finishedTasks_, &i); + MOZ_ASSERT(outstanding_ > 0); + outstanding_--; + } if (!outstanding_) break; @@ -91,8 +95,10 @@ ModuleGenerator::~ModuleGenerator() } } - MOZ_ASSERT(HelperThreadState().wasmCompilationInProgress); - HelperThreadState().wasmCompilationInProgress = false; + if (parallelCompilationInProgressOnHelperThread_) { + MOZ_ASSERT(HelperThreadState().wasmCompilationInProgress); + HelperThreadState().wasmCompilationInProgress = false; + } } else { MOZ_ASSERT(!outstanding_); } @@ -208,12 +214,9 @@ ModuleGenerator::finishOutstandingTask() while (true) { MOZ_ASSERT(outstanding_ > 0); - if (HelperThreadState().wasmFailed(lock)) - return false; - - if (!HelperThreadState().wasmFinishedList(lock).empty()) { + if (!finishedTasks_.empty()) { outstanding_--; - task = HelperThreadState().wasmFinishedList(lock).popCopy(); + task = finishedTasks_.popCopy(); break; } @@ -221,6 +224,9 @@ ModuleGenerator::finishOutstandingTask() } } + if (task->failed()) + return false; + return finishTask(task); } @@ -365,6 +371,9 @@ ModuleGenerator::patchFarJumps(const TrapExitOffsetArray& trapExits) bool ModuleGenerator::finishTask(IonCompileTask* task) { + if (task->failed()) + return false; + const FuncBytes& func = task->func(); FuncCompileResults& results = task->results(); @@ -856,31 +865,25 @@ ModuleGenerator::startFuncDefs() MOZ_ASSERT(!startedFuncDefs_); MOZ_ASSERT(!finishedFuncDefs_); - // The wasmCompilationInProgress atomic ensures that there is only one - // parallel compilation in progress at a time. In the special case of - // asm.js, where the ModuleGenerator itself can be on a helper thread, this - // avoids the possibility of deadlock since at most 1 helper thread will be - // blocking on other helper threads and there are always >1 helper threads. - // With wasm, this restriction could be relaxed by moving the worklist state - // out of HelperThreadState since each independent compilation needs its own - // worklist pair. Alternatively, the deadlock could be avoided by having the - // ModuleGenerator thread make progress (on compile tasks) instead of - // blocking. + // Helper-thread initiated wasm compilations stay serialized so that we do + // not end up with multiple helper threads blocking on other helper threads. + // Main-thread compilations can still overlap because they drain their own + // finished-task queue and do not steal tasks from other generators. GlobalHelperThreadState& threads = HelperThreadState(); MOZ_ASSERT(threads.threadCount > 1); uint32_t numTasks; - if (CanUseExtraThreads() && threads.wasmCompilationInProgress.compareExchange(false, true)) { + if (CanUseExtraThreads() && (!CurrentHelperThread() || + threads.wasmCompilationInProgress.compareExchange(false, true))) { #ifdef DEBUG { AutoLockHelperThreadState lock; - MOZ_ASSERT(!HelperThreadState().wasmFailed(lock)); MOZ_ASSERT(HelperThreadState().wasmWorklist(lock).empty()); - MOZ_ASSERT(HelperThreadState().wasmFinishedList(lock).empty()); } #endif parallel_ = true; + parallelCompilationInProgressOnHelperThread_ = !!CurrentHelperThread(); numTasks = 2 * threads.maxWasmCompilationThreads(); } else { numTasks = 1; @@ -891,6 +894,9 @@ ModuleGenerator::startFuncDefs() for (size_t i = 0; i < numTasks; i++) tasks_.infallibleEmplaceBack(*shared_, COMPILATION_LIFO_DEFAULT_CHUNK_SIZE); + for (auto& task : tasks_) + task.setFinishedList(&finishedTasks_); + if (!freeTasks_.reserve(numTasks)) return false; for (size_t i = 0; i < numTasks; i++) diff --git a/js/src/wasm/WasmGenerator.h b/js/src/wasm/WasmGenerator.h index 85b23664a2..520e296d7b 100644 --- a/js/src/wasm/WasmGenerator.h +++ b/js/src/wasm/WasmGenerator.h @@ -104,9 +104,11 @@ class MOZ_STACK_CLASS ModuleGenerator // Parallel compilation bool parallel_; + bool parallelCompilationInProgressOnHelperThread_; uint32_t outstanding_; IonCompileTaskVector tasks_; IonCompileTaskPtrVector freeTasks_; + IonCompileTaskPtrVector finishedTasks_; // Assertions DebugOnly activeFuncDef_; diff --git a/js/src/wasm/WasmIonCompile.h b/js/src/wasm/WasmIonCompile.h index dcd5c3c633..6d3a9728ba 100644 --- a/js/src/wasm/WasmIonCompile.h +++ b/js/src/wasm/WasmIonCompile.h @@ -19,6 +19,7 @@ #define wasm_ion_compile_h #include "jit/MacroAssembler.h" +#include "vm/HelperThreads.h" #include "wasm/WasmTypes.h" namespace js { @@ -106,6 +107,8 @@ class IonCompileTask UniqueFuncBytes func_; CompileMode mode_; Maybe results_; + IonCompileTaskPtrVector* finishedList_; + bool failed_; IonCompileTask(const IonCompileTask&) = delete; IonCompileTask& operator=(const IonCompileTask&) = delete; @@ -113,7 +116,26 @@ class IonCompileTask public: IonCompileTask(const ModuleGeneratorData& mg, size_t defaultChunkSize) : mg_(mg), lifo_(defaultChunkSize), func_(nullptr), mode_(CompileMode::None) + , finishedList_(nullptr) + , failed_(false) {} + + void setFinishedList(IonCompileTaskPtrVector* finishedList) { + finishedList_ = finishedList; + } + + IonCompileTaskPtrVector* finishedList() const { + MOZ_ASSERT(finishedList_); + return finishedList_; + } + + void setFailed() { + failed_ = true; + } + + bool failed() const { + return failed_; + } LifoAlloc& lifo() { return lifo_; } @@ -143,6 +165,7 @@ class IonCompileTask results_.reset(); lifo_.releaseAll(); mode_ = CompileMode::None; + failed_ = false; } }; From 47746b476e760c536e10b5d746ea646b9becb184 Mon Sep 17 00:00:00 2001 From: ownedbywuigi Date: Sun, 10 May 2026 06:34:18 -0700 Subject: [PATCH 23/30] Issue #3092 - Add new GC sweep tasks. For saved stacks, self-hosting scripts, and native iterators. --- js/src/jsgc.cpp | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/js/src/jsgc.cpp b/js/src/jsgc.cpp index a41c635161..f1d41bdf0e 100644 --- a/js/src/jsgc.cpp +++ b/js/src/jsgc.cpp @@ -4429,7 +4429,9 @@ MAKE_GC_SWEEP_TASK(SweepBaseShapesTask); MAKE_GC_SWEEP_TASK(SweepInitialShapesTask); MAKE_GC_SWEEP_TASK(SweepObjectGroupsTask); MAKE_GC_SWEEP_TASK(SweepRegExpsTask); -MAKE_GC_SWEEP_TASK(SweepMiscTask); +MAKE_GC_SWEEP_TASK(SweepSavedStacksTask); +MAKE_GC_SWEEP_TASK(SweepSelfHostingScriptSourceTask); +MAKE_GC_SWEEP_TASK(SweepNativeIteratorsTask); #undef MAKE_GC_SWEEP_TASK /* virtual */ void @@ -4462,15 +4464,27 @@ SweepRegExpsTask::run() } /* virtual */ void -SweepMiscTask::run() +SweepSavedStacksTask::run() { for (GCCompartmentGroupIter c(runtime); !c.done(); c.next()) { c->sweepSavedStacks(); - c->sweepSelfHostingScriptSource(); - c->sweepNativeIterators(); } } +/* virtual */ void +SweepSelfHostingScriptSourceTask::run() +{ + for (GCCompartmentGroupIter c(runtime); !c.done(); c.next()) + c->sweepSelfHostingScriptSource(); +} + +/* virtual */ void +SweepNativeIteratorsTask::run() +{ + for (GCCompartmentGroupIter c(runtime); !c.done(); c.next()) + c->sweepNativeIterators(); +} + void GCRuntime::startTask(GCParallelTask& task, gcstats::Phase phase, AutoLockHelperThreadState& locked) @@ -4551,7 +4565,9 @@ GCRuntime::beginSweepingZoneGroup(AutoLockForExclusiveAccess& lock) SweepCCWrappersTask sweepCCWrappersTask(rt); SweepObjectGroupsTask sweepObjectGroupsTask(rt); SweepRegExpsTask sweepRegExpsTask(rt); - SweepMiscTask sweepMiscTask(rt); + SweepSavedStacksTask sweepSavedStacksTask(rt); + SweepSelfHostingScriptSourceTask sweepSelfHostingScriptSourceTask(rt); + SweepNativeIteratorsTask sweepNativeIteratorsTask(rt); WeakCacheTaskVector sweepCacheTasks = PrepareWeakCacheTasks(rt); for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) { @@ -4599,7 +4615,9 @@ GCRuntime::beginSweepingZoneGroup(AutoLockForExclusiveAccess& lock) startTask(sweepCCWrappersTask, gcstats::PHASE_SWEEP_CC_WRAPPER, helperLock); startTask(sweepObjectGroupsTask, gcstats::PHASE_SWEEP_TYPE_OBJECT, helperLock); startTask(sweepRegExpsTask, gcstats::PHASE_SWEEP_REGEXP, helperLock); - startTask(sweepMiscTask, gcstats::PHASE_SWEEP_MISC, helperLock); + startTask(sweepSavedStacksTask, gcstats::PHASE_SWEEP_MISC, helperLock); + startTask(sweepSelfHostingScriptSourceTask, gcstats::PHASE_SWEEP_MISC, helperLock); + startTask(sweepNativeIteratorsTask, gcstats::PHASE_SWEEP_MISC, helperLock); for (auto& task : sweepCacheTasks) startTask(task, gcstats::PHASE_SWEEP_MISC, helperLock); } @@ -4678,7 +4696,9 @@ GCRuntime::beginSweepingZoneGroup(AutoLockForExclusiveAccess& lock) joinTask(sweepCCWrappersTask, gcstats::PHASE_SWEEP_CC_WRAPPER, helperLock); joinTask(sweepObjectGroupsTask, gcstats::PHASE_SWEEP_TYPE_OBJECT, helperLock); joinTask(sweepRegExpsTask, gcstats::PHASE_SWEEP_REGEXP, helperLock); - joinTask(sweepMiscTask, gcstats::PHASE_SWEEP_MISC, helperLock); + joinTask(sweepSavedStacksTask, gcstats::PHASE_SWEEP_MISC, helperLock); + joinTask(sweepSelfHostingScriptSourceTask, gcstats::PHASE_SWEEP_MISC, helperLock); + joinTask(sweepNativeIteratorsTask, gcstats::PHASE_SWEEP_MISC, helperLock); for (auto& task : sweepCacheTasks) joinTask(task, gcstats::PHASE_SWEEP_MISC, helperLock); } From c06776336dddce9d7014e782ea751a356908654b Mon Sep 17 00:00:00 2001 From: ownedbywuigi Date: Sun, 10 May 2026 06:38:06 -0700 Subject: [PATCH 24/30] Issue #3092 - Implement BackgroundFinalizeTask for parallel garbage collection finalization --- js/src/jsgc.cpp | 6 ++---- js/src/vm/HelperThreads.cpp | 22 ++++------------------ 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/js/src/jsgc.cpp b/js/src/jsgc.cpp index f1d41bdf0e..947185f4b6 100644 --- a/js/src/jsgc.cpp +++ b/js/src/jsgc.cpp @@ -4523,10 +4523,8 @@ PrepareWeakCacheTasks(JSRuntime* rt) WeakCacheTaskVector out; for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) { for (JS::WeakCache* cache : zone->weakCaches_) { - if (!out.append(SweepWeakCacheTask(rt, *cache))) { - SweepWeakCachesFromMainThread(rt); - return WeakCacheTaskVector(); - } + if (!out.emplaceBack(rt, *cache)) + return out; } } return out; diff --git a/js/src/vm/HelperThreads.cpp b/js/src/vm/HelperThreads.cpp index e07d0e6dd6..a7b5c1ae1f 100644 --- a/js/src/vm/HelperThreads.cpp +++ b/js/src/vm/HelperThreads.cpp @@ -49,6 +49,10 @@ js::CreateHelperThreadsState() { MOZ_ASSERT(!gHelperThreadState); gHelperThreadState = js_new(); + return gHelperThreadState != nullptr; +} + +void js::DestroyHelperThreadsState() { MOZ_ASSERT(gHelperThreadState); @@ -473,24 +477,6 @@ js::CancelOffThreadParses(JSRuntime* rt) HelperThreadState().wait(lock, GlobalHelperThreadState::CONSUMER); } - return gHelperThreadState != nullptr; - } - - void - js::DestroyHelperThreadsState() - { - MOZ_ASSERT(gHelperThreadState); - gHelperThreadState->finish(); - js_delete(gHelperThreadState); - gHelperThreadState = nullptr; - // Clean up any parse tasks which haven't been finished by the main thread. - AutoUnlockHelperThreadState unlock(lock); - HelperThreadState().cancelParseTask(rt->contextFromMainThread(), task->kind, task); - } - } - if (!found) - break; - } } bool From 3433d538ed354a07e9e4468eca12d4aaf9ceb001 Mon Sep 17 00:00:00 2001 From: ownedbywuigi Date: Sun, 10 May 2026 10:07:06 -0700 Subject: [PATCH 25/30] Issue #3092 - Implement parallel sweeping and compaction tasks for improved garbage collection performance --- js/src/gc/GCRuntime.h | 6 +- js/src/jsgc.cpp | 228 +++++++++++++++++++++++++++++++++++------- 2 files changed, 194 insertions(+), 40 deletions(-) diff --git a/js/src/gc/GCRuntime.h b/js/src/gc/GCRuntime.h index cf2f7b036a..8690411a2f 100644 --- a/js/src/gc/GCRuntime.h +++ b/js/src/gc/GCRuntime.h @@ -678,6 +678,10 @@ class GCRuntime void requestMinorGC(JS::gcreason::Reason reason); + // Zone relocation for compacting GC (can be called from helper threads) + MOZ_MUST_USE bool relocateArenas(Zone* zone, JS::gcreason::Reason reason, + Arena*& relocatedListOut, SliceBudget& sliceBudget); + #ifdef DEBUG bool onBackgroundThread() { return helperState.onBackgroundThread(); } #endif // DEBUG @@ -976,8 +980,6 @@ class GCRuntime void endCompactPhase(JS::gcreason::Reason reason); void sweepTypesAfterCompacting(Zone* zone); void sweepZoneAfterCompacting(Zone* zone); - MOZ_MUST_USE bool relocateArenas(Zone* zone, JS::gcreason::Reason reason, - Arena*& relocatedListOut, SliceBudget& sliceBudget); void updateTypeDescrObjects(MovingTracer* trc, Zone* zone); void updateCellPointers(MovingTracer* trc, Zone* zone, AllocKinds kinds, size_t bgTaskCount); void updateAllCellPointers(MovingTracer* trc, Zone* zone); diff --git a/js/src/jsgc.cpp b/js/src/jsgc.cpp index 947185f4b6..34020282e0 100644 --- a/js/src/jsgc.cpp +++ b/js/src/jsgc.cpp @@ -4628,15 +4628,67 @@ GCRuntime::beginSweepingZoneGroup(AutoLockForExclusiveAccess& lock) // Cancel any active or pending off thread compilations. js::CancelOffThreadIonCompile(rt, JS::Zone::Sweep); - for (GCCompartmentGroupIter c(rt); !c.done(); c.next()) { - c->sweepGlobalObject(&fop); - c->sweepDebugEnvironments(); - c->sweepJitCompartment(&fop); - c->sweepTemplateObjects(); - } + // Parallelize compartment and zone sweeping operations + struct CompartmentZoneCleanupTask : public GCParallelTaskHelper + { + JSRuntime* rt; + JSCompartment* comp; + Zone* zone; + explicit CompartmentZoneCleanupTask(JSRuntime* r, JSCompartment* c, Zone* z) + : rt(r), comp(c), zone(z) + {} + + CompartmentZoneCleanupTask(CompartmentZoneCleanupTask&& other) + : GCParallelTaskHelper(mozilla::Move(other)), + rt(other.rt), + comp(other.comp), + zone(other.zone) + {} + + void run() { + FreeOp fop(rt); + if (comp) { + comp->sweepGlobalObject(&fop); + comp->sweepDebugEnvironments(); + comp->sweepJitCompartment(&fop); + comp->sweepTemplateObjects(); + } + if (zone) + zone->sweepWeakMaps(); + } + }; + + typedef Vector CleanupTaskVector; + CleanupTaskVector tasks; + + size_t taskCount = 0; + for (GCCompartmentGroupIter c(rt); !c.done(); c.next()) + taskCount++; for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) - zone->sweepWeakMaps(); + taskCount++; + + if (taskCount > 1 && tasks.reserve(taskCount)) { + for (GCCompartmentGroupIter c(rt); !c.done(); c.next()) + tasks.infallibleEmplaceBack(rt, c, nullptr); + for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) + tasks.infallibleEmplaceBack(rt, nullptr, zone); + + AutoLockHelperThreadState helperLock; + for (auto& task : tasks) + startTask(task, gcstats::PHASE_SWEEP_MISC, helperLock); + for (auto& task : tasks) + joinTask(task, gcstats::PHASE_SWEEP_MISC, helperLock); + } else { + for (GCCompartmentGroupIter c(rt); !c.done(); c.next()) { + c->sweepGlobalObject(&fop); + c->sweepDebugEnvironments(); + c->sweepJitCompartment(&fop); + c->sweepTemplateObjects(); + } + for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) + zone->sweepWeakMaps(); + } // Bug 1071218: the following two methods have not yet been // refactored to work on a single zone-group at once. @@ -4649,29 +4701,57 @@ GCRuntime::beginSweepingZoneGroup(AutoLockForExclusiveAccess& lock) jit::JitRuntime::SweepJitcodeGlobalTable(rt); } + // Parallelize per-zone JIT cleanup and metadata sweeping + struct ZoneCleanupTask : public GCParallelTaskHelper { - gcstats::AutoPhase apdc(stats, gcstats::PHASE_SWEEP_DISCARD_CODE); - for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) + JSRuntime* rt; + Zone* zone; + bool releaseObservedTypes; + + explicit ZoneCleanupTask(JSRuntime* r, Zone* z, bool releaseTypes) + : rt(r), zone(z), releaseObservedTypes(releaseTypes) + {} + + ZoneCleanupTask(ZoneCleanupTask&& other) + : GCParallelTaskHelper(mozilla::Move(other)), + rt(other.rt), + zone(other.zone), + releaseObservedTypes(other.releaseObservedTypes) + {} + + void run() { + FreeOp fop(rt); zone->discardJitCode(&fop); - } - - { - gcstats::AutoPhase ap1(stats, gcstats::PHASE_SWEEP_TYPES); - gcstats::AutoPhase ap2(stats, gcstats::PHASE_SWEEP_TYPES_BEGIN); - for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) zone->beginSweepTypes(&fop, releaseObservedTypes && !zone->isPreservingCode()); - } - - { - gcstats::AutoPhase ap(stats, gcstats::PHASE_SWEEP_BREAKPOINT); - for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) zone->sweepBreakpoints(&fop); - } + zone->sweepUniqueIds(&fop); + } + }; { - gcstats::AutoPhase ap(stats, gcstats::PHASE_SWEEP_BREAKPOINT); + typedef Vector ZoneCleanupTaskVector; + ZoneCleanupTaskVector tasks; + size_t zoneCount = 0; for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) - zone->sweepUniqueIds(&fop); + zoneCount++; + + if (zoneCount > 1 && tasks.reserve(zoneCount)) { + for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) + tasks.infallibleEmplaceBack(rt, zone, releaseObservedTypes); + + AutoLockHelperThreadState helperLock; + for (auto& task : tasks) + startTask(task, gcstats::PHASE_SWEEP_DISCARD_CODE, helperLock); + for (auto& task : tasks) + joinTask(task, gcstats::PHASE_SWEEP_DISCARD_CODE, helperLock); + } else { + for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) { + zone->discardJitCode(&fop); + zone->beginSweepTypes(&fop, releaseObservedTypes && !zone->isPreservingCode()); + zone->sweepBreakpoints(&fop); + zone->sweepUniqueIds(&fop); + } + } } } @@ -5097,6 +5177,43 @@ GCRuntime::endSweepPhase(bool destroyingRuntime, AutoLockForExclusiveAccess& loc AssertNoWrappersInGrayList(rt); } +// Zone relocation task for parallel compaction +struct ZoneCompactionTask : public GCParallelTaskHelper +{ + JSRuntime* runtime; + Zone* zone; + JS::gcreason::Reason reason; + Arena* relocatedArenas; + bool relocateSucceeded; + + explicit ZoneCompactionTask(JSRuntime* rt, Zone* z, JS::gcreason::Reason r) + : runtime(rt), zone(z), reason(r), relocatedArenas(nullptr), relocateSucceeded(false) + {} + + ZoneCompactionTask(ZoneCompactionTask&& other) + : GCParallelTaskHelper(mozilla::Move(other)), + runtime(other.runtime), + zone(other.zone), + reason(other.reason), + relocatedArenas(nullptr), + relocateSucceeded(false) + {} + + void run() { + AutoSuppressProfilerSampling suppressSampling(runtime); + zone->setGCState(Zone::Compact); + SliceBudget unlimited = SliceBudget::unlimited(); + relocateSucceeded = runtime->gc.relocateArenas(zone, reason, relocatedArenas, unlimited); + zone->setGCState(Zone::Finished); + } + + Arena* takeRelocatedArenas() { + Arena* result = relocatedArenas; + relocatedArenas = nullptr; + return result; + } +}; + void GCRuntime::beginCompactPhase() { @@ -5125,22 +5242,57 @@ GCRuntime::compactPhase(JS::gcreason::Reason reason, SliceBudget& sliceBudget, gcstats::AutoPhase ap(stats, gcstats::PHASE_COMPACT); Arena* relocatedArenas = nullptr; - while (!zonesToMaybeCompact.isEmpty()) { - // TODO: JSScripts can move. If the sampler interrupts the GC in the - // middle of relocating an arena, invalid JSScript pointers may be - // accessed. Suppress all sampling until a finer-grained solution can be - // found. See bug 1295775. - AutoSuppressProfilerSampling suppressSampling(rt); - Zone* zone = zonesToMaybeCompact.front(); - MOZ_ASSERT(zone->isGCFinished()); - zone->setGCState(Zone::Compact); - if (relocateArenas(zone, reason, relocatedArenas, sliceBudget)) - updatePointersToRelocatedCells(zone, lock); - zone->setGCState(Zone::Finished); - zonesToMaybeCompact.removeFront(); - if (sliceBudget.isOverBudget()) - break; + // Collect zones to compact and parallelize when there are multiple zones + typedef Vector ZoneCompactionTaskVector; + ZoneCompactionTaskVector tasks; + + size_t zoneCount = 0; + for (Zone* zone = zonesToMaybeCompact.front(); zone; zone = zone->nextZone()) + zoneCount++; + + // Use parallel compaction for multiple zones, fall back to serial for single zone + if (zoneCount > 1 && tasks.reserve(zoneCount)) { + for (Zone* zone = zonesToMaybeCompact.front(); zone; zone = zone->nextZone()) { + MOZ_ASSERT(zone->isGCFinished()); + tasks.infallibleEmplaceBack(rt, zone, reason); + } + + AutoLockHelperThreadState helperLock; + for (auto& task : tasks) + startTask(task, gcstats::PHASE_COMPACT_MOVE, helperLock); + for (auto& task : tasks) { + joinTask(task, gcstats::PHASE_COMPACT_MOVE, helperLock); + Arena* taskRelocated = task.takeRelocatedArenas(); + if (taskRelocated) { + Arena* tail = taskRelocated; + while (tail->next) + tail = tail->next; + tail->next = relocatedArenas; + relocatedArenas = taskRelocated; + } + } + + // Update pointers for all compacted zones + for (auto& task : tasks) + updatePointersToRelocatedCells(task.zone, lock); + + while (!zonesToMaybeCompact.isEmpty()) + zonesToMaybeCompact.removeFront(); + } else { + // Fall back to serial compaction for single zone or OOM + while (!zonesToMaybeCompact.isEmpty()) { + AutoSuppressProfilerSampling suppressSampling(rt); + Zone* zone = zonesToMaybeCompact.front(); + MOZ_ASSERT(zone->isGCFinished()); + zone->setGCState(Zone::Compact); + if (relocateArenas(zone, reason, relocatedArenas, sliceBudget)) + updatePointersToRelocatedCells(zone, lock); + zone->setGCState(Zone::Finished); + zonesToMaybeCompact.removeFront(); + if (sliceBudget.isOverBudget()) + break; + } } if (ShouldProtectRelocatedArenas(reason)) From 18ddd00afe1599ce6c4c8c9393e70058d6cd41d5 Mon Sep 17 00:00:00 2001 From: ownedbywuigi Date: Sun, 10 May 2026 11:56:54 -0700 Subject: [PATCH 26/30] Issue #3092 - Initial idle GC implementation --- js/src/gc/GCRuntime.h | 11 ++ js/src/gc/IdleGC.cpp | 262 ++++++++++++++++++++++++++++++++++++++++++ js/src/gc/IdleGC.h | 118 +++++++++++++++++++ js/src/jsapi.cpp | 30 +++++ js/src/jsapi.h | 34 ++++++ js/src/jsgc.cpp | 17 +++ js/src/moz.build | 1 + js/src/vm/Runtime.cpp | 6 + 8 files changed, 479 insertions(+) create mode 100644 js/src/gc/IdleGC.cpp create mode 100644 js/src/gc/IdleGC.h diff --git a/js/src/gc/GCRuntime.h b/js/src/gc/GCRuntime.h index 8690411a2f..a92ab66b5c 100644 --- a/js/src/gc/GCRuntime.h +++ b/js/src/gc/GCRuntime.h @@ -13,6 +13,7 @@ #include "jsgc.h" #include "gc/Heap.h" +#include "gc/IdleGC.h" #include "gc/Nursery.h" #include "gc/Statistics.h" #include "gc/StoreBuffer.h" @@ -650,6 +651,13 @@ class GCRuntime void onOutOfMallocMemory(); void onOutOfMallocMemory(const AutoLockGC& lock); + /* Idle-time GC notifications. */ + void notifyJSExecutionStart(); + void notifyJSExecutionEnd(); + IdleGCManager& idleGCMgr() { + return idleGC; + } + size_t maxMallocBytesAllocated() { return maxMallocBytes; } uint64_t nextCellUniqueId() { @@ -1021,6 +1029,9 @@ class GCRuntime GCSchedulingTunables tunables; GCSchedulingState schedulingState; + /* Idle-time garbage collection manager. */ + IdleGCManager idleGC; + MemProfiler mMemProfiler; private: diff --git a/js/src/gc/IdleGC.cpp b/js/src/gc/IdleGC.cpp new file mode 100644 index 0000000000..ac5c0d3059 --- /dev/null +++ b/js/src/gc/IdleGC.cpp @@ -0,0 +1,262 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * 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/. */ + +/** + * IDLE-TIME GARBAGE COLLECTION SYSTEM + * + * Overview + * -------- + * This system implements idle-time-only garbage collection, deferring GC work + * to periods when the JavaScript engine is not actively processing code. + * This improves perceived responsiveness by avoiding GC pauses during critical + * execution windows. + * + * Architecture + * ----------- + * + * The system consists of several key components: + * + * 1. IdleGCManager (gc/IdleGC.h, gc/IdleGC.cpp) + * - Tracks when the JS engine is executing vs. idle + * - Maintains timestamp of last execution activity + * - Checks if sufficient idle time has passed + * - Configurable idle threshold (default: 100ms) + * - Can determine which GC reasons should bypass idle checks + * + * 2. GCRuntime Integration (gc/GCRuntime.h) + * - Contains an IdleGCManager instance + * - Provides public methods: notifyJSExecutionStart/End() + * - Modified checkIfGCAllowedInCurrentState() to check idle status + * + * 3. Activity Tracking (vm/Runtime.cpp) + * - triggerActivityCallback() now notifies IdleGCManager + * - Called when JS enters/exits request (execution boundary) + * - Updated in jsapi.cpp's StartRequest/StopRequest functions + * + * 4. Public API (jsapi.h, jsapi.cpp) + * - JS_SetIdleGCEnabled() / JS_IsIdleGCEnabled() + * - JS_SetIdleGCThreshold() / JS_GetIdleGCThreshold() + * - JS_GetIdleTimeSinceLastExecution() + * - Allows embedders to configure idle GC behavior + * + * Behavior + * -------- + * + * Normal GC Trigger (Idle GC Enabled): + * + * 1. JS code executes → notifyJSExecutionStart() called + * 2. JS code finishes → notifyJSExecutionEnd() called, timestamp recorded + * 3. GC needed → checkIfGCAllowedInCurrentState() checks idle status + * 4a. If idle >= threshold → GC proceeds normally + * 4b. If still executing/idle < threshold → GC is deferred + * 5. Once idle threshold is met, next GC request proceeds + * + * Critical GC Triggers (Always Proceed): + * + * The following GC reasons bypass idle checking: + * - OUT_OF_MEMORY: Memory pressure conditions + * - ALLOC_TRIGGER: Allocation threshold exceeded + * - MALLOC_PRESSURE: Malloc pressure from OS + * - EAGER_ALLOC_TRIGGER: Eager allocation trigger + * - API: Explicit JS API calls + * - DETERMINISTIC: Deterministic tests + * - EVICT_NURSERY: Nursery eviction + * - SHUTDOWN_CC, DESTROY_RUNTIME, LAST_DITCH: Shutdown GCs + * - DESTROY_ZONE, COMPARTMENT_REVOKED: Zone/compartment destruction + * + * Configuration + * ------------- + * + * Idle GC Enabled (default: true): + * - Enables idle-time-only GC mode + * - Can be toggled dynamically via JS_SetIdleGCEnabled() + * + * Idle Threshold (default: 100ms): + * - Minimum idle time before GC is permitted + * - Configurable via JS_SetIdleGCThreshold(ms) + * - Typical values: 50-200ms depending on application + * + * Integration Examples + * -------------------- + * + * Browser Integration: + * + * // When browser loads configuration + * JS_SetIdleGCEnabled(cx, true); + * JS_SetIdleGCThreshold(cx, 100); // 100ms idle threshold + * + * // Monitor idle GC effectiveness (optional) + * uint64_t idleTime = JS_GetIdleTimeSinceLastExecution(cx); + * if (idleTime > JS_GetIdleGCThreshold(cx)) { + * // System is idle, GC would be allowed if triggered + * } + * + * Disabling for Specific Scenarios: + * + * // During initialization when nothing is "idle" yet + * JS_SetIdleGCEnabled(cx, false); + * // ... do initial setup ... + * JS_SetIdleGCEnabled(cx, true); // Re-enable for normal operation + * + * Performance Considerations + * -------------------------- + * + * Benefits: + * - Reduced jank during active JS execution + * - GC pauses moved to idle periods where users won't notice + * - Especially effective for interactive applications + * - Improves Time-to-Interactive and First Input Delay metrics + * + * Tradeoffs: + * - May accumulate more garbage before collection + * - Requires predictable idle periods (not suitable for all workloads) + * - Critical memory pressure GCs still proceed immediately + * + * Tuning: + * - Lower threshold (50ms) = more frequent GC, less memory overhead + * - Higher threshold (200ms) = less GC overhead, more memory usage + * - Optimal value depends on application's execution pattern + * + * Testing + * ------- + * + * Unit Tests: + * // Test idle detection + * JS_SetIdleGCThreshold(cx, 100); + * // Simulate JS execution + * cx->runtime()->gc.notifyJSExecutionStart(); + * // ... wait 50ms ... + * MOZ_ASSERT(!cx->runtime()->gc.idleGCMgr().isIdleEnough()); + * cx->runtime()->gc.notifyJSExecutionEnd(); + * // ... wait 150ms ... + * MOZ_ASSERT(cx->runtime()->gc.idleGCMgr().isIdleEnough()); + * + * Integration Tests: + * - Verify GC is deferred during active execution + * - Verify GC proceeds after idle period + * - Verify critical GC reasons bypass idle check + * - Measure latency improvements + * + * Implementation Notes + * -------------------- + * + * Thread Safety: + * - IdleGCManager uses mozilla::Atomic for thread-safe state + * - TimeStamp operations are atomic + * - No additional locking needed beyond existing GC locks + * + * Compatibility: + * - Works with both incremental and non-incremental GC + * - Compatible with generational GC + * - Works with zone GC and full GC + * - Respects existing GC suppression mechanisms + * + * Future Enhancements + * ------------------- + * + * Potential improvements: + * - Adaptive idle threshold based on historical GC times + * - Per-zone idle configuration + * - Integration with browser rendering idle callback API + * - Metrics/telemetry for idle GC effectiveness + * - Machine learning-based prediction of idle periods + * - Cooperative GC scheduling with other subsystems + * + */ + +#include "gc/IdleGC.h" +#include "jsapi.h" + +namespace js { +namespace gc { + +IdleGCManager::IdleGCManager() + : lastExecutionTime_(mozilla::TimeStamp::Now()), + idleGCEnabled_(true), + idleThresholdMs_(100), // 100ms default idle threshold + isExecuting_(false) +{ +} + +void +IdleGCManager::notifyJSExecutionStart() +{ + isExecuting_ = true; +} + +void +IdleGCManager::notifyJSExecutionEnd() +{ + isExecuting_ = false; + lastExecutionTime_ = mozilla::TimeStamp::Now(); +} + +bool +IdleGCManager::isIdleEnough() const +{ + if (!idleGCEnabled_) { + return true; // If disabled, always consider idle + } + + if (isExecuting_) { + return false; // Still executing, not idle + } + + uint64_t idleTime = idleTimeSinceLastExecution(); + return idleTime >= idleThresholdMs_; +} + +uint64_t +IdleGCManager::idleTimeSinceLastExecution() const +{ + mozilla::TimeStamp now = mozilla::TimeStamp::Now(); + mozilla::TimeDuration idle = now - lastExecutionTime_; + return idle.ToMilliseconds(); +} + +bool +IdleGCManager::shouldBypassIdleCheck(JS::gcreason::Reason reason) +{ + // These reasons indicate urgent GC needs that should bypass idle checking + switch (reason) { + // Allocation and memory pressure conditions. + case JS::gcreason::ALLOC_TRIGGER: + case JS::gcreason::EAGER_ALLOC_TRIGGER: + case JS::gcreason::TOO_MUCH_MALLOC: + case JS::gcreason::MEM_PRESSURE: + case JS::gcreason::LAST_DITCH: + + // Nursery/store-buffer pressure. + case JS::gcreason::OUT_OF_NURSERY: + case JS::gcreason::EVICT_NURSERY: + case JS::gcreason::FULL_STORE_BUFFER: + case JS::gcreason::SHARED_MEMORY_LIMIT: + + // Explicit API calls + case JS::gcreason::API: + case JS::gcreason::ABORT_GC: + + // Shutdown and finalization + case JS::gcreason::SHUTDOWN_CC: + case JS::gcreason::DESTROY_RUNTIME: + case JS::gcreason::NSJSCONTEXT_DESTROY: + case JS::gcreason::XPCONNECT_SHUTDOWN: + + return true; + + default: + return false; + } +} + +void +IdleGCManager::reset() +{ + lastExecutionTime_ = mozilla::TimeStamp::Now(); + isExecuting_ = false; +} + +} // namespace gc +} // namespace js diff --git a/js/src/gc/IdleGC.h b/js/src/gc/IdleGC.h new file mode 100644 index 0000000000..8763699783 --- /dev/null +++ b/js/src/gc/IdleGC.h @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * 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 gc_IdleGC_h +#define gc_IdleGC_h + +#include +#include "mozilla/Atomics.h" +#include "mozilla/TimeStamp.h" +#include "js/GCAPI.h" + +namespace js { +namespace gc { + +/* + * Idle-Time Garbage Collection System + * ==================================== + * + * This system defers garbage collection to occur only during periods when + * the browser is not actively processing JavaScript. This helps maintain + * responsiveness by avoiding GC pauses during critical execution windows. + * + * When JavaScript execution is active, GC triggers are deferred. Once the + * JS engine has been idle for a configurable threshold period, pending GC + * work is performed immediately or incrementally as appropriate. + * + * Key characteristics: + * - Tracks JavaScript activity via hooks in the execution engine + * - Configurable idle time threshold (default: 100ms) + * - Can be disabled per-zone or globally + * - Works with both incremental and non-incremental GC modes + * - Respects critical GC reasons that override idle checking + */ + +class IdleGCManager +{ + public: + // Initialize the idle GC manager + IdleGCManager(); + + /* + * Called when JavaScript execution begins. Marks the engine as active. + */ + void notifyJSExecutionStart(); + + /* + * Called when JavaScript execution ends. Records the end time for + * idle detection purposes. + */ + void notifyJSExecutionEnd(); + + /* + * Check if the system has been idle for long enough to permit GC. + * Returns true if sufficient idle time has passed since last JS execution. + */ + bool isIdleEnough() const; + + /* + * Get the amount of idle time since the last JS execution. + * Returns time in milliseconds. + */ + uint64_t idleTimeSinceLastExecution() const; + + /* + * Set the idle threshold - minimum idle time before GC is permitted. + * Time is in milliseconds. Default is 100ms. + */ + void setIdleThresholdMs(uint64_t thresholdMs) { + idleThresholdMs_ = thresholdMs; + } + + uint64_t idleThresholdMs() const { + return idleThresholdMs_; + } + + /* + * Enable or disable idle-time-only GC mode. + */ + void setIdleGCEnabled(bool enabled) { + idleGCEnabled_ = enabled; + } + + bool isIdleGCEnabled() const { + return idleGCEnabled_; + } + + /* + * Check if a GC reason should bypass idle checking. + * Critical reasons (OOM-like pressure, nursery pressure, explicit requests) + * always proceed. + */ + static bool shouldBypassIdleCheck(JS::gcreason::Reason reason); + + /* + * Reset idle tracking state (used during GC or at shutdown). + */ + void reset(); + + private: + // Timestamp of the last JavaScript execution activity + mozilla::TimeStamp lastExecutionTime_; + + // Whether idle-time-only GC mode is enabled + mozilla::Atomic idleGCEnabled_; + + // Minimum idle time (in milliseconds) before GC is permitted + mozilla::Atomic idleThresholdMs_; + + // Whether the JS engine is currently executing + mozilla::Atomic isExecuting_; +}; + +} // namespace gc +} // namespace js + +#endif // gc_IdleGC_h diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index f1627e481e..5edb7cc5be 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -1478,6 +1478,36 @@ JS_SetGCParametersBasedOnAvailableMemory(JSContext* cx, uint32_t availMem) JS_SetGCParameter(cx, config[i].key, config[i].value); } +JS_PUBLIC_API(void) +JS_SetIdleGCEnabled(JSContext* cx, bool enabled) +{ + cx->gc.idleGCMgr().setIdleGCEnabled(enabled); +} + +JS_PUBLIC_API(bool) +JS_IsIdleGCEnabled(JSContext* cx) +{ + return cx->gc.idleGCMgr().isIdleGCEnabled(); +} + +JS_PUBLIC_API(void) +JS_SetIdleGCThreshold(JSContext* cx, uint64_t milliseconds) +{ + cx->gc.idleGCMgr().setIdleThresholdMs(milliseconds); +} + +JS_PUBLIC_API(uint64_t) +JS_GetIdleGCThreshold(JSContext* cx) +{ + return cx->gc.idleGCMgr().idleThresholdMs(); +} + +JS_PUBLIC_API(uint64_t) +JS_GetIdleTimeSinceLastExecution(JSContext* cx) +{ + return cx->gc.idleGCMgr().idleTimeSinceLastExecution(); +} + JS_PUBLIC_API(JSString*) JS_NewExternalString(JSContext* cx, const char16_t* chars, size_t length, diff --git a/js/src/jsapi.h b/js/src/jsapi.h index 85808d0da4..14c747ad37 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -1719,6 +1719,40 @@ JS_GetGCParameter(JSContext* cx, JSGCParamKey key); extern JS_PUBLIC_API(void) JS_SetGCParametersBasedOnAvailableMemory(JSContext* cx, uint32_t availMem); +/* + * Idle-time garbage collection control. + * These functions allow control over when GC occurs - specifically, whether + * GC should only run when the browser/application is not doing active JS work. + */ + +/** + * Enable or disable idle-time-only GC mode. + * When enabled, GC is deferred until the JS engine has been idle for the + * configured threshold period (default: 100ms). + */ +extern JS_PUBLIC_API(void) +JS_SetIdleGCEnabled(JSContext* cx, bool enabled); + +extern JS_PUBLIC_API(bool) +JS_IsIdleGCEnabled(JSContext* cx); + +/** + * Set the minimum idle time (in milliseconds) before GC is permitted. + * Use this to configure how long the JS engine must be inactive before + * pending GC work can proceed. + */ +extern JS_PUBLIC_API(void) +JS_SetIdleGCThreshold(JSContext* cx, uint64_t milliseconds); + +extern JS_PUBLIC_API(uint64_t) +JS_GetIdleGCThreshold(JSContext* cx); + +/** + * Get the current idle time since the last JS execution. + */ +extern JS_PUBLIC_API(uint64_t) +JS_GetIdleTimeSinceLastExecution(JSContext* cx); + /** * Create a new JSString whose chars member refers to external memory, i.e., * memory requiring application-specific finalization. diff --git a/js/src/jsgc.cpp b/js/src/jsgc.cpp index 34020282e0..467c7ea58e 100644 --- a/js/src/jsgc.cpp +++ b/js/src/jsgc.cpp @@ -214,6 +214,7 @@ #include "gc/FindSCCs.h" #include "gc/GCInternals.h" #include "gc/GCTrace.h" +#include "gc/IdleGC.h" #include "gc/Marking.h" #include "gc/Memory.h" #include "gc/Policy.h" @@ -6005,6 +6006,10 @@ GCRuntime::checkIfGCAllowedInCurrentState(JS::gcreason::Reason reason) if (rt->isBeingDestroyed() && !IsShutdownGC(reason)) return false; + // Check if the system is idle enough for GC, unless this is a critical GC + if (!IdleGCManager::shouldBypassIdleCheck(reason) && !idleGC.isIdleEnough()) + return false; + return true; } @@ -6171,6 +6176,18 @@ GCRuntime::notifyDidPaint() interFrameGC = false; } +void +GCRuntime::notifyJSExecutionStart() +{ + idleGC.notifyJSExecutionStart(); +} + +void +GCRuntime::notifyJSExecutionEnd() +{ + idleGC.notifyJSExecutionEnd(); +} + static bool ZonesSelected(JSRuntime* rt) { diff --git a/js/src/moz.build b/js/src/moz.build index faedaeaecf..500d00caac 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -172,6 +172,7 @@ main_deunified_sources = [ 'gc/Allocator.cpp', 'gc/Barrier.cpp', 'gc/GCTrace.cpp', + 'gc/IdleGC.cpp', 'gc/Iteration.cpp', 'gc/Marking.cpp', 'gc/Memory.cpp', diff --git a/js/src/vm/Runtime.cpp b/js/src/vm/Runtime.cpp index 00dc2916e9..969c161713 100644 --- a/js/src/vm/Runtime.cpp +++ b/js/src/vm/Runtime.cpp @@ -673,6 +673,12 @@ JSRuntime::traceSharedIntlData(JSTracer* trc) void JSRuntime::triggerActivityCallback(bool active) { + if (active) { + gc.notifyJSExecutionStart(); + } else { + gc.notifyJSExecutionEnd(); + } + if (!activityCallback) return; From 1d3dad153b80e063af821072a79c5db830a6a474 Mon Sep 17 00:00:00 2001 From: ownedbywuigi Date: Sun, 10 May 2026 12:48:33 -0700 Subject: [PATCH 27/30] Issue #3092 - Perform a minor GC on tab close --- js/src/jsgc.cpp | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/js/src/jsgc.cpp b/js/src/jsgc.cpp index 467c7ea58e..635bb7b3d5 100644 --- a/js/src/jsgc.cpp +++ b/js/src/jsgc.cpp @@ -5998,6 +5998,12 @@ GCRuntime::checkCanCallAPI() bool GCRuntime::checkIfGCAllowedInCurrentState(JS::gcreason::Reason reason) { + auto isTabCloseReason = [](JS::gcreason::Reason r) { + return r == JS::gcreason::PAGE_HIDE || + r == JS::gcreason::POST_COMPARTMENT || + r == JS::gcreason::NSJSCONTEXT_DESTROY; + }; + if (rt->mainThread.suppressGC) return false; @@ -6006,8 +6012,10 @@ GCRuntime::checkIfGCAllowedInCurrentState(JS::gcreason::Reason reason) if (rt->isBeingDestroyed() && !IsShutdownGC(reason)) return false; - // Check if the system is idle enough for GC, unless this is a critical GC - if (!IdleGCManager::shouldBypassIdleCheck(reason) && !idleGC.isIdleEnough()) + // Allow fast tab-close cleanup even when the runtime is otherwise busy. + if (!isTabCloseReason(reason) && + !IdleGCManager::shouldBypassIdleCheck(reason) && + !idleGC.isIdleEnough()) return false; return true; @@ -6087,8 +6095,16 @@ js::AutoEnqueuePendingParseTasksAfterGC::~AutoEnqueuePendingParseTasksAfterGC() SliceBudget GCRuntime::defaultBudget(JS::gcreason::Reason reason, int64_t millis) { + auto isTabCloseReason = [](JS::gcreason::Reason r) { + return r == JS::gcreason::PAGE_HIDE || + r == JS::gcreason::POST_COMPARTMENT || + r == JS::gcreason::NSJSCONTEXT_DESTROY; + }; + if (millis == 0) { - if (reason == JS::gcreason::ALLOC_TRIGGER) + if (isTabCloseReason(reason)) + millis = 3; + else if (reason == JS::gcreason::ALLOC_TRIGGER) millis = defaultSliceBudget(); else if (schedulingState.inHighFrequencyGCMode() && tunables.isDynamicMarkSliceEnabled()) millis = defaultSliceBudget() * IGC_MARK_SLICE_MULTIPLIER; From f0cba4122187326a36d38ddd926c49d2dd7f9217 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sun, 10 May 2026 21:11:17 -0400 Subject: [PATCH 28/30] Issue #3092 - Fix unsafe GC multithreading changes --- js/src/gc/GCRuntime.h | 6 +- js/src/gc/IdleGC.cpp | 68 ++++----- js/src/jsapi.h | 2 +- js/src/jsgc.cpp | 280 +++++++++--------------------------- js/src/vm/HelperThreads.cpp | 17 +++ 5 files changed, 119 insertions(+), 254 deletions(-) diff --git a/js/src/gc/GCRuntime.h b/js/src/gc/GCRuntime.h index a92ab66b5c..5301454f8e 100644 --- a/js/src/gc/GCRuntime.h +++ b/js/src/gc/GCRuntime.h @@ -686,10 +686,6 @@ class GCRuntime void requestMinorGC(JS::gcreason::Reason reason); - // Zone relocation for compacting GC (can be called from helper threads) - MOZ_MUST_USE bool relocateArenas(Zone* zone, JS::gcreason::Reason reason, - Arena*& relocatedListOut, SliceBudget& sliceBudget); - #ifdef DEBUG bool onBackgroundThread() { return helperState.onBackgroundThread(); } #endif // DEBUG @@ -988,6 +984,8 @@ class GCRuntime void endCompactPhase(JS::gcreason::Reason reason); void sweepTypesAfterCompacting(Zone* zone); void sweepZoneAfterCompacting(Zone* zone); + [[nodiscard]] bool relocateArenas(Zone* zone, JS::gcreason::Reason reason, + Arena*& relocatedListOut, SliceBudget& sliceBudget); void updateTypeDescrObjects(MovingTracer* trc, Zone* zone); void updateCellPointers(MovingTracer* trc, Zone* zone, AllocKinds kinds, size_t bgTaskCount); void updateAllCellPointers(MovingTracer* trc, Zone* zone); diff --git a/js/src/gc/IdleGC.cpp b/js/src/gc/IdleGC.cpp index ac5c0d3059..2e64819609 100644 --- a/js/src/gc/IdleGC.cpp +++ b/js/src/gc/IdleGC.cpp @@ -5,36 +5,36 @@ /** * IDLE-TIME GARBAGE COLLECTION SYSTEM - * + * * Overview * -------- * This system implements idle-time-only garbage collection, deferring GC work * to periods when the JavaScript engine is not actively processing code. * This improves perceived responsiveness by avoiding GC pauses during critical * execution windows. - * + * * Architecture * ----------- - * + * * The system consists of several key components: - * + * * 1. IdleGCManager (gc/IdleGC.h, gc/IdleGC.cpp) * - Tracks when the JS engine is executing vs. idle - * - Maintains timestamp of last execution activity + * - Maintains timestamp of last execution activity * - Checks if sufficient idle time has passed * - Configurable idle threshold (default: 100ms) * - Can determine which GC reasons should bypass idle checks - * + * * 2. GCRuntime Integration (gc/GCRuntime.h) * - Contains an IdleGCManager instance * - Provides public methods: notifyJSExecutionStart/End() * - Modified checkIfGCAllowedInCurrentState() to check idle status - * + * * 3. Activity Tracking (vm/Runtime.cpp) * - triggerActivityCallback() now notifies IdleGCManager * - Called when JS enters/exits request (execution boundary) * - Updated in jsapi.cpp's StartRequest/StopRequest functions - * + * * 4. Public API (jsapi.h, jsapi.cpp) * - JS_SetIdleGCEnabled() / JS_IsIdleGCEnabled() * - JS_SetIdleGCThreshold() / JS_GetIdleGCThreshold() @@ -43,18 +43,18 @@ * * Behavior * -------- - * + * * Normal GC Trigger (Idle GC Enabled): - * + * * 1. JS code executes → notifyJSExecutionStart() called * 2. JS code finishes → notifyJSExecutionEnd() called, timestamp recorded * 3. GC needed → checkIfGCAllowedInCurrentState() checks idle status * 4a. If idle >= threshold → GC proceeds normally * 4b. If still executing/idle < threshold → GC is deferred * 5. Once idle threshold is met, next GC request proceeds - * + * * Critical GC Triggers (Always Proceed): - * + * * The following GC reasons bypass idle checking: * - OUT_OF_MEMORY: Memory pressure conditions * - ALLOC_TRIGGER: Allocation threshold exceeded @@ -65,63 +65,63 @@ * - EVICT_NURSERY: Nursery eviction * - SHUTDOWN_CC, DESTROY_RUNTIME, LAST_DITCH: Shutdown GCs * - DESTROY_ZONE, COMPARTMENT_REVOKED: Zone/compartment destruction - * + * * Configuration * ------------- - * + * * Idle GC Enabled (default: true): * - Enables idle-time-only GC mode * - Can be toggled dynamically via JS_SetIdleGCEnabled() - * + * * Idle Threshold (default: 100ms): * - Minimum idle time before GC is permitted * - Configurable via JS_SetIdleGCThreshold(ms) * - Typical values: 50-200ms depending on application - * + * * Integration Examples * -------------------- - * + * * Browser Integration: - * + * * // When browser loads configuration * JS_SetIdleGCEnabled(cx, true); * JS_SetIdleGCThreshold(cx, 100); // 100ms idle threshold - * + * * // Monitor idle GC effectiveness (optional) * uint64_t idleTime = JS_GetIdleTimeSinceLastExecution(cx); * if (idleTime > JS_GetIdleGCThreshold(cx)) { * // System is idle, GC would be allowed if triggered * } - * + * * Disabling for Specific Scenarios: - * + * * // During initialization when nothing is "idle" yet * JS_SetIdleGCEnabled(cx, false); * // ... do initial setup ... * JS_SetIdleGCEnabled(cx, true); // Re-enable for normal operation - * + * * Performance Considerations * -------------------------- - * + * * Benefits: * - Reduced jank during active JS execution * - GC pauses moved to idle periods where users won't notice * - Especially effective for interactive applications * - Improves Time-to-Interactive and First Input Delay metrics - * + * * Tradeoffs: * - May accumulate more garbage before collection * - Requires predictable idle periods (not suitable for all workloads) * - Critical memory pressure GCs still proceed immediately - * + * * Tuning: * - Lower threshold (50ms) = more frequent GC, less memory overhead * - Higher threshold (200ms) = less GC overhead, more memory usage * - Optimal value depends on application's execution pattern - * + * * Testing * ------- - * + * * Unit Tests: * // Test idle detection * JS_SetIdleGCThreshold(cx, 100); @@ -132,30 +132,30 @@ * cx->runtime()->gc.notifyJSExecutionEnd(); * // ... wait 150ms ... * MOZ_ASSERT(cx->runtime()->gc.idleGCMgr().isIdleEnough()); - * + * * Integration Tests: * - Verify GC is deferred during active execution * - Verify GC proceeds after idle period * - Verify critical GC reasons bypass idle check * - Measure latency improvements - * + * * Implementation Notes * -------------------- - * + * * Thread Safety: * - IdleGCManager uses mozilla::Atomic for thread-safe state * - TimeStamp operations are atomic * - No additional locking needed beyond existing GC locks - * + * * Compatibility: * - Works with both incremental and non-incremental GC * - Compatible with generational GC * - Works with zone GC and full GC * - Respects existing GC suppression mechanisms - * + * * Future Enhancements * ------------------- - * + * * Potential improvements: * - Adaptive idle threshold based on historical GC times * - Per-zone idle configuration @@ -163,7 +163,7 @@ * - Metrics/telemetry for idle GC effectiveness * - Machine learning-based prediction of idle periods * - Cooperative GC scheduling with other subsystems - * + * */ #include "gc/IdleGC.h" diff --git a/js/src/jsapi.h b/js/src/jsapi.h index 14c747ad37..969ee4cd19 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -1747,7 +1747,7 @@ JS_SetIdleGCThreshold(JSContext* cx, uint64_t milliseconds); extern JS_PUBLIC_API(uint64_t) JS_GetIdleGCThreshold(JSContext* cx); -/** +/** * Get the current idle time since the last JS execution. */ extern JS_PUBLIC_API(uint64_t) diff --git a/js/src/jsgc.cpp b/js/src/jsgc.cpp index 635bb7b3d5..0aeb10d264 100644 --- a/js/src/jsgc.cpp +++ b/js/src/jsgc.cpp @@ -1571,7 +1571,7 @@ ArenaLists::prepareForIncrementalGC() { purge(); for (auto i : AllAllocKinds()) { - arenaLists[i].moveCursorToEnd(); + arenaLists[i].moveCursorToEnd(); } } @@ -3582,14 +3582,14 @@ ShouldCollectZone(Zone* zone, JS::gcreason::Reason reason) // Normally we collect all scheduled zones. if (reason != JS::gcreason::COMPARTMENT_REVIVED) return zone->isGCScheduled(); - + // If we are repeating a GC because we noticed dead compartments haven't // been collected, then only collect zones containing those compartments. for (CompartmentsInZoneIter comp(zone); !comp.done(); comp.next()) { if (comp->scheduledForDestruction) return true; } - + return false; } @@ -3655,7 +3655,7 @@ GCRuntime::beginMarkPhase(JS::gcreason::Reason reason, AutoLockForExclusiveAcces * on. If the value of keepAtoms() changes between GC slices, then we'll * cancel the incremental GC. See IsIncrementalGCSafe. */ - + if (isFull && !rt->keepAtoms()) { Zone* atomsZone = rt->atomsCompartment(lock)->zone(); if (atomsZone->isGCScheduled()) { @@ -3671,7 +3671,7 @@ GCRuntime::beginMarkPhase(JS::gcreason::Reason reason, AutoLockForExclusiveAcces /* * Ensure that after the start of a collection we don't allocate into any - * existing arenas, as this can cause unreachable things to be marked. + * existing arenas, as this can cause unreachable things to be marked. */ if (isIncremental) { for (GCZonesIter zone(rt); !zone.done(); zone.next()) @@ -3763,7 +3763,7 @@ GCRuntime::beginMarkPhase(JS::gcreason::Reason reason, AutoLockForExclusiveAcces bufferGrayRoots(); markCompartments(); } - + return true; } @@ -3802,9 +3802,9 @@ GCRuntime::markCompartments() */ /* Propagate the maybeAlive flag via cross-compartment edges. */ - + Vector workList; - + for (CompartmentsIter comp(rt, SkipAtoms); !comp.done(); comp.next()) { if (comp->maybeAlive) { if (!workList.append(comp)) @@ -3825,9 +3825,9 @@ GCRuntime::markCompartments() } } - + /* Set scheduleForDestruction based on maybeAlive. */ - + for (GCCompartmentsIter comp(rt); !comp.done(); comp.next()) { MOZ_ASSERT(!comp->scheduledForDestruction); if (!comp->maybeAlive && !rt->isAtomsCompartment(comp)) @@ -4524,8 +4524,10 @@ PrepareWeakCacheTasks(JSRuntime* rt) WeakCacheTaskVector out; for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) { for (JS::WeakCache* cache : zone->weakCaches_) { - if (!out.emplaceBack(rt, *cache)) - return out; + if (!out.emplaceBack(rt, *cache)) { + SweepWeakCachesFromMainThread(rt); + return WeakCacheTaskVector(); + } } } return out; @@ -4629,67 +4631,14 @@ GCRuntime::beginSweepingZoneGroup(AutoLockForExclusiveAccess& lock) // Cancel any active or pending off thread compilations. js::CancelOffThreadIonCompile(rt, JS::Zone::Sweep); - // Parallelize compartment and zone sweeping operations - struct CompartmentZoneCleanupTask : public GCParallelTaskHelper - { - JSRuntime* rt; - JSCompartment* comp; - Zone* zone; - - explicit CompartmentZoneCleanupTask(JSRuntime* r, JSCompartment* c, Zone* z) - : rt(r), comp(c), zone(z) - {} - - CompartmentZoneCleanupTask(CompartmentZoneCleanupTask&& other) - : GCParallelTaskHelper(mozilla::Move(other)), - rt(other.rt), - comp(other.comp), - zone(other.zone) - {} - - void run() { - FreeOp fop(rt); - if (comp) { - comp->sweepGlobalObject(&fop); - comp->sweepDebugEnvironments(); - comp->sweepJitCompartment(&fop); - comp->sweepTemplateObjects(); - } - if (zone) - zone->sweepWeakMaps(); - } - }; - - typedef Vector CleanupTaskVector; - CleanupTaskVector tasks; - - size_t taskCount = 0; - for (GCCompartmentGroupIter c(rt); !c.done(); c.next()) - taskCount++; - for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) - taskCount++; - - if (taskCount > 1 && tasks.reserve(taskCount)) { - for (GCCompartmentGroupIter c(rt); !c.done(); c.next()) - tasks.infallibleEmplaceBack(rt, c, nullptr); - for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) - tasks.infallibleEmplaceBack(rt, nullptr, zone); - - AutoLockHelperThreadState helperLock; - for (auto& task : tasks) - startTask(task, gcstats::PHASE_SWEEP_MISC, helperLock); - for (auto& task : tasks) - joinTask(task, gcstats::PHASE_SWEEP_MISC, helperLock); - } else { - for (GCCompartmentGroupIter c(rt); !c.done(); c.next()) { - c->sweepGlobalObject(&fop); - c->sweepDebugEnvironments(); - c->sweepJitCompartment(&fop); - c->sweepTemplateObjects(); - } - for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) - zone->sweepWeakMaps(); + for (GCCompartmentGroupIter c(rt); !c.done(); c.next()) { + c->sweepGlobalObject(&fop); + c->sweepDebugEnvironments(); + c->sweepJitCompartment(&fop); + c->sweepTemplateObjects(); } + for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) + zone->sweepWeakMaps(); // Bug 1071218: the following two methods have not yet been // refactored to work on a single zone-group at once. @@ -4702,57 +4651,29 @@ GCRuntime::beginSweepingZoneGroup(AutoLockForExclusiveAccess& lock) jit::JitRuntime::SweepJitcodeGlobalTable(rt); } - // Parallelize per-zone JIT cleanup and metadata sweeping - struct ZoneCleanupTask : public GCParallelTaskHelper { - JSRuntime* rt; - Zone* zone; - bool releaseObservedTypes; - - explicit ZoneCleanupTask(JSRuntime* r, Zone* z, bool releaseTypes) - : rt(r), zone(z), releaseObservedTypes(releaseTypes) - {} - - ZoneCleanupTask(ZoneCleanupTask&& other) - : GCParallelTaskHelper(mozilla::Move(other)), - rt(other.rt), - zone(other.zone), - releaseObservedTypes(other.releaseObservedTypes) - {} - - void run() { - FreeOp fop(rt); - zone->discardJitCode(&fop); - zone->beginSweepTypes(&fop, releaseObservedTypes && !zone->isPreservingCode()); - zone->sweepBreakpoints(&fop); - zone->sweepUniqueIds(&fop); - } - }; - - { - typedef Vector ZoneCleanupTaskVector; - ZoneCleanupTaskVector tasks; - size_t zoneCount = 0; + gcstats::AutoPhase apdc(stats, gcstats::PHASE_SWEEP_DISCARD_CODE); for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) - zoneCount++; + zone->discardJitCode(&fop); + } - if (zoneCount > 1 && tasks.reserve(zoneCount)) { - for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) - tasks.infallibleEmplaceBack(rt, zone, releaseObservedTypes); + { + gcstats::AutoPhase ap1(stats, gcstats::PHASE_SWEEP_TYPES); + gcstats::AutoPhase ap2(stats, gcstats::PHASE_SWEEP_TYPES_BEGIN); + for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) + zone->beginSweepTypes(&fop, releaseObservedTypes && !zone->isPreservingCode()); + } - AutoLockHelperThreadState helperLock; - for (auto& task : tasks) - startTask(task, gcstats::PHASE_SWEEP_DISCARD_CODE, helperLock); - for (auto& task : tasks) - joinTask(task, gcstats::PHASE_SWEEP_DISCARD_CODE, helperLock); - } else { - for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) { - zone->discardJitCode(&fop); - zone->beginSweepTypes(&fop, releaseObservedTypes && !zone->isPreservingCode()); - zone->sweepBreakpoints(&fop); - zone->sweepUniqueIds(&fop); - } - } + { + gcstats::AutoPhase ap(stats, gcstats::PHASE_SWEEP_BREAKPOINT); + for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) + zone->sweepBreakpoints(&fop); + } + + { + gcstats::AutoPhase ap(stats, gcstats::PHASE_SWEEP_BREAKPOINT); + for (GCZoneGroupIter zone(rt); !zone.done(); zone.next()) + zone->sweepUniqueIds(&fop); } } @@ -5104,7 +5025,7 @@ GCRuntime::performSweepActions(SliceBudget& budget, AutoLockForExclusiveAccess& // Reset phase index. sweepPhaseIndex = 0; - + endSweepingZoneGroup(); getNextZoneGroup(); if (!currentZoneGroup) @@ -5178,43 +5099,6 @@ GCRuntime::endSweepPhase(bool destroyingRuntime, AutoLockForExclusiveAccess& loc AssertNoWrappersInGrayList(rt); } -// Zone relocation task for parallel compaction -struct ZoneCompactionTask : public GCParallelTaskHelper -{ - JSRuntime* runtime; - Zone* zone; - JS::gcreason::Reason reason; - Arena* relocatedArenas; - bool relocateSucceeded; - - explicit ZoneCompactionTask(JSRuntime* rt, Zone* z, JS::gcreason::Reason r) - : runtime(rt), zone(z), reason(r), relocatedArenas(nullptr), relocateSucceeded(false) - {} - - ZoneCompactionTask(ZoneCompactionTask&& other) - : GCParallelTaskHelper(mozilla::Move(other)), - runtime(other.runtime), - zone(other.zone), - reason(other.reason), - relocatedArenas(nullptr), - relocateSucceeded(false) - {} - - void run() { - AutoSuppressProfilerSampling suppressSampling(runtime); - zone->setGCState(Zone::Compact); - SliceBudget unlimited = SliceBudget::unlimited(); - relocateSucceeded = runtime->gc.relocateArenas(zone, reason, relocatedArenas, unlimited); - zone->setGCState(Zone::Finished); - } - - Arena* takeRelocatedArenas() { - Arena* result = relocatedArenas; - relocatedArenas = nullptr; - return result; - } -}; - void GCRuntime::beginCompactPhase() { @@ -5244,56 +5128,22 @@ GCRuntime::compactPhase(JS::gcreason::Reason reason, SliceBudget& sliceBudget, Arena* relocatedArenas = nullptr; - // Collect zones to compact and parallelize when there are multiple zones - typedef Vector ZoneCompactionTaskVector; - ZoneCompactionTaskVector tasks; - - size_t zoneCount = 0; - for (Zone* zone = zonesToMaybeCompact.front(); zone; zone = zone->nextZone()) - zoneCount++; + while (!zonesToMaybeCompact.isEmpty()) { + // TODO: JSScripts can move. If the sampler interrupts the GC in the + // middle of relocating an arena, invalid JSScript pointers may be + // accessed. Suppress all sampling until a finer-grained solution can be + // found. See bug 1295775. + AutoSuppressProfilerSampling suppressSampling(rt); - // Use parallel compaction for multiple zones, fall back to serial for single zone - if (zoneCount > 1 && tasks.reserve(zoneCount)) { - for (Zone* zone = zonesToMaybeCompact.front(); zone; zone = zone->nextZone()) { - MOZ_ASSERT(zone->isGCFinished()); - tasks.infallibleEmplaceBack(rt, zone, reason); - } - - AutoLockHelperThreadState helperLock; - for (auto& task : tasks) - startTask(task, gcstats::PHASE_COMPACT_MOVE, helperLock); - for (auto& task : tasks) { - joinTask(task, gcstats::PHASE_COMPACT_MOVE, helperLock); - Arena* taskRelocated = task.takeRelocatedArenas(); - if (taskRelocated) { - Arena* tail = taskRelocated; - while (tail->next) - tail = tail->next; - tail->next = relocatedArenas; - relocatedArenas = taskRelocated; - } - } - - // Update pointers for all compacted zones - for (auto& task : tasks) - updatePointersToRelocatedCells(task.zone, lock); - - while (!zonesToMaybeCompact.isEmpty()) - zonesToMaybeCompact.removeFront(); - } else { - // Fall back to serial compaction for single zone or OOM - while (!zonesToMaybeCompact.isEmpty()) { - AutoSuppressProfilerSampling suppressSampling(rt); - Zone* zone = zonesToMaybeCompact.front(); - MOZ_ASSERT(zone->isGCFinished()); - zone->setGCState(Zone::Compact); - if (relocateArenas(zone, reason, relocatedArenas, sliceBudget)) - updatePointersToRelocatedCells(zone, lock); - zone->setGCState(Zone::Finished); - zonesToMaybeCompact.removeFront(); - if (sliceBudget.isOverBudget()) - break; - } + Zone* zone = zonesToMaybeCompact.front(); + MOZ_ASSERT(zone->isGCFinished()); + zone->setGCState(Zone::Compact); + if (relocateArenas(zone, reason, relocatedArenas, sliceBudget)) + updatePointersToRelocatedCells(zone, lock); + zone->setGCState(Zone::Finished); + zonesToMaybeCompact.removeFront(); + if (sliceBudget.isOverBudget()) + break; } if (ShouldProtectRelocatedArenas(reason)) @@ -5729,7 +5579,7 @@ gc::AbortReason gc::IsIncrementalGCUnsafe(JSRuntime* rt) { MOZ_ASSERT(!rt->mainThread.suppressGC); - + if (rt->keepAtoms()) return gc::AbortReason::KeepAtomsSet; @@ -5750,7 +5600,7 @@ GCRuntime::budgetIncrementalGC(JS::gcreason::Reason reason, SliceBudget& budget, else if (mode != JSGC_MODE_INCREMENTAL) unsafeReason = gc::AbortReason::ModeChange; } - + if (unsafeReason != AbortReason::None) { resetIncrementalGC(unsafeReason, lock); budget.makeUnlimited(); @@ -5758,7 +5608,7 @@ GCRuntime::budgetIncrementalGC(JS::gcreason::Reason reason, SliceBudget& budget, return; } - + if (isTooMuchMalloc()) { budget.makeUnlimited(); @@ -6025,15 +5875,15 @@ bool GCRuntime::shouldRepeatForDeadZone(JS::gcreason::Reason reason) { MOZ_ASSERT_IF(reason == JS::gcreason::COMPARTMENT_REVIVED, !isIncremental); - + if (!isIncremental || isIncrementalGCInProgress()) return false; - + for (CompartmentsIter c(rt, SkipAtoms); !c.done(); c.next()) { if (c->scheduledForDestruction) return true; } - + return false; } @@ -6055,13 +5905,13 @@ GCRuntime::collect(bool nonincrementalByAPI, SliceBudget budget, JS::gcreason::R do { poked = false; bool wasReset = gcCycle(nonincrementalByAPI, budget, reason); - + bool repeatForDeadZone = false; if (poked && cleanUpEverything) { /* Need to re-schedule all zones for GC. */ JS::PrepareForFullGC(rt->contextFromMainThread()); - + } else if (shouldRepeatForDeadZone(reason) && !wasReset) { /* * This code makes an extra effort to collect compartments that we @@ -6071,7 +5921,7 @@ GCRuntime::collect(bool nonincrementalByAPI, SliceBudget budget, JS::gcreason::R repeatForDeadZone = true; reason = JS::gcreason::COMPARTMENT_REVIVED; } - + /* * If we reset an existing GC, we need to start a new one. Also, we diff --git a/js/src/vm/HelperThreads.cpp b/js/src/vm/HelperThreads.cpp index a7b5c1ae1f..d224d4e830 100644 --- a/js/src/vm/HelperThreads.cpp +++ b/js/src/vm/HelperThreads.cpp @@ -477,6 +477,23 @@ js::CancelOffThreadParses(JSRuntime* rt) HelperThreadState().wait(lock, GlobalHelperThreadState::CONSUMER); } + // Clean up any parse tasks which haven't been finished by the main thread. + GlobalHelperThreadState::ParseTaskVector& finished = + HelperThreadState().parseFinishedList(lock); + while (true) { + bool found = false; + for (size_t i = 0; i < finished.length(); i++) { + ParseTask* task = finished[i]; + if (task->runtimeMatches(rt)) { + found = true; + AutoUnlockHelperThreadState unlock(lock); + HelperThreadState().cancelParseTask(rt->contextFromMainThread(), + task->kind, task); + } + } + if (!found) + break; + } } bool From e9826f5559c9c492831043694f08e80747c47326 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Sun, 10 May 2026 22:17:56 -0400 Subject: [PATCH 29/30] Issue #3092 - Safely parallelize GC background finalization Parallelize background finalization across zones within each finalize phase while preserving phase ordering and falling back to serial work when no helper thread is idle. Make compacting GC pointer updates schedule background work per zone and only when enough background-safe arenas exist. --- js/src/gc/GCRuntime.h | 4 +- js/src/jsgc.cpp | 217 ++++++++++++++++++++++++++++++++++++------ js/src/jsgc.h | 1 + 3 files changed, 194 insertions(+), 28 deletions(-) diff --git a/js/src/gc/GCRuntime.h b/js/src/gc/GCRuntime.h index 5301454f8e..89da938017 100644 --- a/js/src/gc/GCRuntime.h +++ b/js/src/gc/GCRuntime.h @@ -974,6 +974,8 @@ class GCRuntime void sweepZones(FreeOp* fop, bool lastGC); void decommitAllWithoutUnlocking(const AutoLockGC& lock); void startDecommit(); + bool sweepBackgroundFinalizePhaseInParallel(ZoneList& zones, const FinalizePhase& phase, + Arena** emptyArenas); void queueZonesForBackgroundSweep(ZoneList& zones); void sweepBackgroundThings(ZoneList& zones, LifoAlloc& freeBlocks); void assertBackgroundSweepingFinished(); @@ -987,7 +989,7 @@ class GCRuntime [[nodiscard]] bool relocateArenas(Zone* zone, JS::gcreason::Reason reason, Arena*& relocatedListOut, SliceBudget& sliceBudget); void updateTypeDescrObjects(MovingTracer* trc, Zone* zone); - void updateCellPointers(MovingTracer* trc, Zone* zone, AllocKinds kinds, size_t bgTaskCount); + void updateCellPointers(MovingTracer* trc, Zone* zone, AllocKinds kinds); void updateAllCellPointers(MovingTracer* trc, Zone* zone); void updatePointersToRelocatedCells(Zone* zone, AutoLockForExclusiveAccess& lock); void protectAndHoldArenas(Arena* arenaList); diff --git a/js/src/jsgc.cpp b/js/src/jsgc.cpp index 0aeb10d264..48cd51f3b5 100644 --- a/js/src/jsgc.cpp +++ b/js/src/jsgc.cpp @@ -2118,7 +2118,7 @@ ArenasToUpdate::next(AutoLockHelperThreadState& lock) // Find the next arena to update. // // This iterates through the GC thing kinds filtered by shouldProcessKind(), - // and then through thea arenas of that kind. All state is held in the + // and then through the arenas of that kind. All state is held in the // object and we just return when we find an arena. for (; kind < AllocKind::LIMIT; kind = nextAllocKind(kind)) { @@ -2208,21 +2208,12 @@ UpdatePointersTask::run() } // namespace gc } // namespace js -static const size_t MinCellUpdateBackgroundTasks = 2; +static const size_t MinCellUpdateBackgroundTasks = 1; static const size_t MaxCellUpdateBackgroundTasks = 8; -static size_t -CellUpdateBackgroundTaskCount() -{ - if (!CanUseExtraThreads()) - return 0; - - size_t targetTaskCount = HelperThreadState().cpuCount / 2; - return Min(Max(targetTaskCount, MinCellUpdateBackgroundTasks), MaxCellUpdateBackgroundTasks); -} - static bool -CanUpdateKindInBackground(AllocKind kind) { +CanUpdateKindInBackground(AllocKind kind) +{ // We try to update as many GC things in parallel as we can, but there are // kinds for which this might not be safe: // - we assume JSObjects that are foreground finalized are not safe to @@ -2234,6 +2225,34 @@ CanUpdateKindInBackground(AllocKind kind) { return true; } +static size_t +CountBackgroundUpdateArenas(Zone* zone, AllocKinds kinds) +{ + size_t arenaCount = 0; + for (AllocKind kind : kinds) { + MOZ_ASSERT(CanUpdateKindInBackground(kind)); + for (Arena* arena = zone->arenas.getFirstArena(kind); arena; arena = arena->next) + arenaCount++; + } + return arenaCount; +} + +static size_t +CellUpdateBackgroundTaskCount(Zone* zone, AllocKinds kinds) +{ + if (!CanUseExtraThreads() || kinds.isEmpty()) + return 0; + + size_t arenaCount = CountBackgroundUpdateArenas(zone, kinds); + if (arenaCount < UpdatePointersTask::MaxArenasToProcess * 2) + return 0; + + size_t targetTaskCount = HelperThreadState().cpuCount / 2; + size_t workTaskCount = arenaCount / UpdatePointersTask::MaxArenasToProcess; + targetTaskCount = Min(targetTaskCount, workTaskCount); + return Min(Max(targetTaskCount, MinCellUpdateBackgroundTasks), MaxCellUpdateBackgroundTasks); +} + static AllocKinds ForegroundUpdateKinds(AllocKinds kinds) { @@ -2254,10 +2273,15 @@ GCRuntime::updateTypeDescrObjects(MovingTracer* trc, Zone* zone) } void -GCRuntime::updateCellPointers(MovingTracer* trc, Zone* zone, AllocKinds kinds, size_t bgTaskCount) +GCRuntime::updateCellPointers(MovingTracer* trc, Zone* zone, AllocKinds kinds) { - AllocKinds fgKinds = bgTaskCount == 0 ? kinds : ForegroundUpdateKinds(kinds); + MOZ_ASSERT(trc); + + AllocKinds fgKinds = ForegroundUpdateKinds(kinds); AllocKinds bgKinds = kinds - fgKinds; + size_t bgTaskCount = CellUpdateBackgroundTaskCount(zone, bgKinds); + if (bgTaskCount == 0) + fgKinds = kinds; ArenasToUpdate fgArenas(zone, fgKinds); ArenasToUpdate bgArenas(zone, bgKinds); @@ -2352,15 +2376,13 @@ GCRuntime::updateAllCellPointers(MovingTracer* trc, Zone* zone) { AutoDisableProxyCheck noProxyCheck(rt); // These checks assert when run in parallel. - size_t bgTaskCount = CellUpdateBackgroundTaskCount(); - - updateCellPointers(trc, zone, UpdatePhaseMisc, bgTaskCount); + updateCellPointers(trc, zone, UpdatePhaseMisc); // Update TypeDescrs before all other objects as typed objects access these // objects when we trace them. updateTypeDescrObjects(trc, zone); - updateCellPointers(trc, zone, UpdatePhaseObjects, bgTaskCount); + updateCellPointers(trc, zone, UpdatePhaseObjects); } /* @@ -2666,6 +2688,17 @@ ArenaLists::backgroundFinalize(FreeOp* fop, Arena* listHead, Arena** empty) lists->backgroundFinalizeState[thingKind] = BFS_DONE; } +void +ArenaLists::backgroundFinalizePhase(FreeOp* fop, const FinalizePhase& phase, Arena** empty) +{ + for (auto kind : phase.kinds) { + Arena* arenas = arenaListsToSweep[kind]; + MOZ_RELEASE_ASSERT(uintptr_t(arenas) != uintptr_t(-1)); + if (arenas) + backgroundFinalize(fop, arenas, empty); + } +} + void ArenaLists::queueForegroundObjectsForSweep(FreeOp* fop) { @@ -3004,6 +3037,138 @@ js::gc::BackgroundDecommitTask::run() } } +class BackgroundFinalizeTask : public GCParallelTaskHelper +{ + Zone* zone_; + const FinalizePhase* phase_; + Arena* emptyArenas_; + + BackgroundFinalizeTask(const BackgroundFinalizeTask&) = delete; + + public: + BackgroundFinalizeTask(Zone* zone, const FinalizePhase* phase) + : zone_(zone), + phase_(phase), + emptyArenas_(nullptr) + {} + + BackgroundFinalizeTask(BackgroundFinalizeTask&& other) + : GCParallelTaskHelper(mozilla::Move(other)), + zone_(other.zone_), + phase_(other.phase_), + emptyArenas_(other.emptyArenas_) + { + other.emptyArenas_ = nullptr; + } + + void run() { + AutoSetThreadIsSweeping threadIsSweeping; + finalize(); + } + + void runAlreadySweeping() { +#ifdef DEBUG + MOZ_ASSERT(CurrentThreadIsGCSweeping()); +#endif + finalize(); + } + + private: + void finalize() { + FreeOp fop(nullptr); + zone_->arenas.backgroundFinalizePhase(&fop, *phase_, &emptyArenas_); + } + + public: + Arena* takeEmptyArenas() { + Arena* empty = emptyArenas_; + emptyArenas_ = nullptr; + return empty; + } +}; + +using BackgroundFinalizeTaskVector = + Vector; + +static size_t +IdleHelperThreadCount(const AutoLockHelperThreadState&) +{ + if (!HelperThreadState().threads) + return 0; + + size_t idle = 0; + for (const auto& thread : *HelperThreadState().threads) { + if (thread.idle()) + idle++; + } + return idle; +} + +static void +PrependArenaList(Arena** head, Arena* arenas) +{ + if (!arenas) + return; + + Arena* tail = arenas; + while (tail->next) + tail = tail->next; + tail->next = *head; + *head = arenas; +} + +bool +GCRuntime::sweepBackgroundFinalizePhaseInParallel(ZoneList& zones, const FinalizePhase& phase, + Arena** emptyArenas) +{ + if (!CanUseExtraThreads()) + return false; + + size_t zoneCount = 0; + for (Zone* zone = zones.front(); zone; zone = zone->nextZone()) + zoneCount++; + + if (zoneCount < 2) + return false; + + BackgroundFinalizeTaskVector tasks; + if (!tasks.reserve(zoneCount)) + return false; + + for (Zone* zone = zones.front(); zone; zone = zone->nextZone()) + tasks.infallibleEmplaceBack(zone, &phase); + + size_t tasksStarted = 0; + + { + AutoLockHelperThreadState helperLock; + + // sweepBackgroundThings() itself runs as a GC helper task. Do not queue + // nested GC parallel tasks unless at least one other helper is idle. + if (IdleHelperThreadCount(helperLock) == 0) + return false; + + for (; tasksStarted < tasks.length(); tasksStarted++) { + if (!tasks[tasksStarted].startWithLockHeld(helperLock)) + break; + } + + { + AutoUnlockHelperThreadState unlock(helperLock); + for (size_t i = tasksStarted; i < tasks.length(); i++) + tasks[i].runAlreadySweeping(); + } + + for (size_t i = 0; i < tasksStarted; i++) + tasks[i].joinWithLockHeld(helperLock); + } + + for (auto& task : tasks) + PrependArenaList(emptyArenas, task.takeEmptyArenas()); + + return true; +} + void GCRuntime::sweepBackgroundThings(ZoneList& zones, LifoAlloc& freeBlocks) { @@ -3016,14 +3181,12 @@ GCRuntime::sweepBackgroundThings(ZoneList& zones, LifoAlloc& freeBlocks) Arena* emptyArenas = nullptr; FreeOp fop(nullptr); for (unsigned phase = 0 ; phase < ArrayLength(BackgroundFinalizePhases) ; ++phase) { - for (Zone* zone = zones.front(); zone; zone = zone->nextZone()) { - for (auto kind : BackgroundFinalizePhases[phase].kinds) { - Arena* arenas = zone->arenas.arenaListsToSweep[kind]; - MOZ_RELEASE_ASSERT(uintptr_t(arenas) != uintptr_t(-1)); - if (arenas) - ArenaLists::backgroundFinalize(&fop, arenas, &emptyArenas); - } - } + const FinalizePhase& finalizePhase = BackgroundFinalizePhases[phase]; + if (sweepBackgroundFinalizePhaseInParallel(zones, finalizePhase, &emptyArenas)) + continue; + + for (Zone* zone = zones.front(); zone; zone = zone->nextZone()) + zone->arenas.backgroundFinalizePhase(&fop, finalizePhase, &emptyArenas); } AutoLockGC lock(rt); diff --git a/js/src/jsgc.h b/js/src/jsgc.h index 521dea05c6..f6d3208d9e 100644 --- a/js/src/jsgc.h +++ b/js/src/jsgc.h @@ -804,6 +804,7 @@ class ArenaLists bool foregroundFinalize(FreeOp* fop, AllocKind thingKind, SliceBudget& sliceBudget, SortedArenaList& sweepList); + void backgroundFinalizePhase(FreeOp* fop, const FinalizePhase& phase, Arena** empty); static void backgroundFinalize(FreeOp* fop, Arena* listHead, Arena** empty); // When finalizing arenas, whether to keep empty arenas on the list or From 6f47a2b0dafedff04af2b4f0433a798cd96478ee Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Fri, 15 May 2026 18:17:17 -0400 Subject: [PATCH 30/30] Fix JS shell module hook build --- js/src/shell/js.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp index 2c3d7f7a1d..d3463a9ff5 100644 --- a/js/src/shell/js.cpp +++ b/js/src/shell/js.cpp @@ -3239,6 +3239,15 @@ static const JSClass sandbox_class = { &sandbox_classOps }; +enum GlobalAppSlot { + GlobalAppSlotModuleMetadataHook, + GlobalAppSlotModuleDynamicImportHook, + GlobalAppSlotCount +}; + +static_assert(GlobalAppSlotCount <= JSCLASS_GLOBAL_APPLICATION_SLOTS, + "global application slots overflow"); + static void SetStandardCompartmentOptions(JS::CompartmentOptions& options) { @@ -4067,7 +4076,7 @@ ParseModule(JSContext* cx, unsigned argc, Value* vp) const char16_t* chars = stableChars.twoByteRange().begin().get(); JS::SourceBufferHolder srcBuf(chars, scriptContents->length(), - SourceBufferHolder::NoOwnership); + JS::SourceBufferHolder::NoOwnership); RootedObject module(cx, frontend::CompileModule(cx, options, srcBuf)); if (!module) @@ -4278,7 +4287,7 @@ AbortDynamicModuleImport(JSContext* cx, unsigned argc, Value* vp) RootedString specifier(cx, args[1].toString()); Rooted promise(cx, &args[2].toObject().as()); - cx->setPendingException(args[3]); + cx->setPendingException(args[3], nullptr); return js::FinishDynamicModuleImport(cx, args[0], specifier, promise); } @@ -8287,7 +8296,7 @@ main(int argc, char** argv, char** envp) JS::SetModuleResolveHook(cx->runtime(), ShellModuleResolveHook); JS::SetModuleDynamicImportHook(cx, ShellModuleDynamicImportHook); - JS::SetModuleMetadataHook(cx, ShellModuleMetadataHook); + JS::SetModuleMetadataHook(cx, CallModuleMetadataHook); result = Shell(cx, &op, envp);