diff --git a/osu.Framework.Tests/Layout/TestSceneContainerLayout.cs b/osu.Framework.Tests/Layout/TestSceneContainerLayout.cs index 754f84eb5e..81cd0577d0 100644 --- a/osu.Framework.Tests/Layout/TestSceneContainerLayout.cs +++ b/osu.Framework.Tests/Layout/TestSceneContainerLayout.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Layout; using osu.Framework.Testing; @@ -498,6 +499,42 @@ public void TestNonAutoSizingParentDoesNotInvalidateSizeDependenciesFromChild() AddAssert("still valid", () => isValid); } + /// + /// Tests that changing Masking property will invalidate child masking bounds. + /// + [Test] + public void TestChildMaskingInvalidationOnMaskingChange() + { + Container parent = null; + Container child = null; + RectangleF childMaskingBounds = new RectangleF(); + RectangleF actualChildMaskingBounds = new RectangleF(); + + AddStep("createTest", () => + { + Child = parent = new Container + { + Size = new Vector2(100), + Child = child = new Container + { + RelativeSizeAxes = Axes.Both + } + }; + }); + + AddAssert("Parent's masking is off", () => parent.Masking == false); + AddStep("Save masking bounds", () => + { + childMaskingBounds = parent.ChildMaskingBounds; + actualChildMaskingBounds = child.ChildMaskingBounds; + }); + AddAssert("Parent and child have the same masking bounds", () => childMaskingBounds == actualChildMaskingBounds); + AddStep("Enable parent masking", () => parent.Masking = true); + AddAssert("Parent's ChildMaskingBounds has changed", () => childMaskingBounds != parent.ChildMaskingBounds); + AddAssert("Child's masking bounds has changed", () => actualChildMaskingBounds != child.ChildMaskingBounds); + AddAssert("Parent and child have the same masking bounds", () => parent.ChildMaskingBounds == child.ChildMaskingBounds); + } + private partial class TestBox1 : Box { public override bool RemoveWhenNotAlive => false; diff --git a/osu.Framework/Graphics/Containers/BufferedContainer.cs b/osu.Framework/Graphics/Containers/BufferedContainer.cs index 5a83d0b87a..aa9aa210a5 100644 --- a/osu.Framework/Graphics/Containers/BufferedContainer.cs +++ b/osu.Framework/Graphics/Containers/BufferedContainer.cs @@ -271,16 +271,16 @@ private void load(ShaderManager shaders) protected override DrawNode CreateDrawNode() => new BufferedContainerDrawNode(this, sharedData); - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) + public override bool UpdateSubTreeMasking() { - bool result = base.UpdateSubTreeMasking(source, maskingBounds); + bool result = base.UpdateSubTreeMasking(); childrenUpdateVersion = updateVersion; return result; } - protected override RectangleF ComputeChildMaskingBounds(RectangleF maskingBounds) => ScreenSpaceDrawQuad.AABBFloat; // Make sure children never get masked away + protected override RectangleF ComputeChildMaskingBounds() => ScreenSpaceDrawQuad.AABBFloat; // Make sure children never get masked away private Vector2 lastScreenSpaceSize; diff --git a/osu.Framework/Graphics/Containers/CompositeDrawable.cs b/osu.Framework/Graphics/Containers/CompositeDrawable.cs index 2c775e1d0f..b113bfb35e 100644 --- a/osu.Framework/Graphics/Containers/CompositeDrawable.cs +++ b/osu.Framework/Graphics/Containers/CompositeDrawable.cs @@ -58,6 +58,7 @@ protected CompositeDrawable() childrenSizeDependencies.Validate(); AddLayout(childrenSizeDependencies); + AddLayout(childMaskingBoundsBacking); } /// @@ -958,12 +959,10 @@ private void updateChild(Drawable c) /// Updates all masking calculations for this and its . /// This occurs post- to ensure that all updates have taken place. /// - /// The parent that triggered this update on this . - /// The that defines the masking bounds. /// Whether masking calculations have taken place. - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) + public override bool UpdateSubTreeMasking() { - if (!base.UpdateSubTreeMasking(source, maskingBounds)) + if (!base.UpdateSubTreeMasking()) return false; if (IsMaskedAway) @@ -974,10 +973,8 @@ public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBou if (RequiresChildrenUpdate) { - var childMaskingBounds = ComputeChildMaskingBounds(maskingBounds); - for (int i = 0; i < aliveInternalChildren.Count; i++) - aliveInternalChildren[i].UpdateSubTreeMasking(this, childMaskingBounds); + aliveInternalChildren[i].UpdateSubTreeMasking(); } return true; @@ -996,9 +993,15 @@ protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) /// /// Computes the to be used as the masking bounds for all . /// - /// The that defines the masking bounds for this . /// The to be used as the masking bounds for . - protected virtual RectangleF ComputeChildMaskingBounds(RectangleF maskingBounds) => Masking ? RectangleF.Intersect(maskingBounds, ScreenSpaceDrawQuad.AABBFloat) : maskingBounds; + protected virtual RectangleF ComputeChildMaskingBounds() => Masking ? RectangleF.Intersect(ComputeMaskingBounds(), ScreenSpaceDrawQuad.AABBFloat) : ComputeMaskingBounds(); + + private readonly LayoutValue childMaskingBoundsBacking = new LayoutValue(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence); + + /// + /// The to be used as the masking bounds for all . + /// + public RectangleF ChildMaskingBounds => childMaskingBoundsBacking.IsValid ? childMaskingBoundsBacking : childMaskingBoundsBacking.Value = ComputeChildMaskingBounds(); /// /// Invoked after and state checks have taken place, @@ -1478,7 +1481,10 @@ protected set return; masking = value; - Invalidate(Invalidation.DrawNode); + // DrawInfo invalidation will propagate masking bounds changes in the sub-tree. + // While this can invalidate other layouts, there are rarely any use cases of enabling/disabling masking "on the fly" + // so this won't hurt performance under normal circumstances. + Invalidate(Invalidation.DrawNode | Invalidation.DrawInfo); } } diff --git a/osu.Framework/Graphics/Containers/DelayedLoadWrapper.cs b/osu.Framework/Graphics/Containers/DelayedLoadWrapper.cs index db123a852d..852da366a2 100644 --- a/osu.Framework/Graphics/Containers/DelayedLoadWrapper.cs +++ b/osu.Framework/Graphics/Containers/DelayedLoadWrapper.cs @@ -7,7 +7,6 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Extensions.PolygonExtensions; -using osu.Framework.Graphics.Primitives; using osu.Framework.Layout; using osu.Framework.Threading; @@ -211,9 +210,9 @@ protected override bool OnInvalidate(Invalidation invalidation, InvalidationSour return result; } - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) + public override bool UpdateSubTreeMasking() { - bool result = base.UpdateSubTreeMasking(source, maskingBounds); + bool result = base.UpdateSubTreeMasking(); // We can accurately compute intersections - the scheduled reset is no longer required. isIntersectingResetDelegate?.Cancel(); @@ -230,7 +229,7 @@ public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBou // The first condition is an intersection against the hierarchy, including any parents that may be masking this wrapper. // It is the same calculation as Drawable.IsMaskedAway, however IsMaskedAway is optimised out for some CompositeDrawables (which this wrapper is). // The second condition is an exact intersection against the optimising container, which further optimises rotated AABBs where the wrapper content is not visible. - IsIntersecting = maskingBounds.IntersectsWith(ScreenSpaceDrawQuad.AABBFloat) + IsIntersecting = ComputeMaskingBounds().IntersectsWith(ScreenSpaceDrawQuad.AABBFloat) && OptimisingContainer?.ScreenSpaceDrawQuad.Intersects(ScreenSpaceDrawQuad) != false; isIntersectingCache.Validate(); diff --git a/osu.Framework/Graphics/Drawable.cs b/osu.Framework/Graphics/Drawable.cs index 93d701a4eb..eeecf7f27c 100644 --- a/osu.Framework/Graphics/Drawable.cs +++ b/osu.Framework/Graphics/Drawable.cs @@ -495,22 +495,34 @@ public virtual bool UpdateSubTree() return true; } + /// + /// Computes the masking bounds of this . + /// + /// The that defines the masking bounds. + public virtual RectangleF ComputeMaskingBounds() + { + if (HasProxy) + return proxy.ComputeMaskingBounds(); + + if (parent == null) + return ScreenSpaceDrawQuad.AABBFloat; + + return parent.ChildMaskingBounds; + } + private RectangleF? lastMaskingBounds; /// /// Updates all masking calculations for this . /// This occurs post- to ensure that all updates have taken place. /// - /// The parent that triggered this update on this . - /// The that defines the masking bounds. /// Whether masking calculations have taken place. - public virtual bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) + public virtual bool UpdateSubTreeMasking() { if (!IsPresent) return false; - if (HasProxy && source != proxy) - return false; + var maskingBounds = ComputeMaskingBounds(); if (!maskingBacking.IsValid || lastMaskingBounds != maskingBounds) { diff --git a/osu.Framework/Graphics/Drawable_ProxyDrawable.cs b/osu.Framework/Graphics/Drawable_ProxyDrawable.cs index 10daa3ebc9..bb8f76aada 100644 --- a/osu.Framework/Graphics/Drawable_ProxyDrawable.cs +++ b/osu.Framework/Graphics/Drawable_ProxyDrawable.cs @@ -3,7 +3,6 @@ #nullable disable -using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; namespace osu.Framework.Graphics @@ -60,13 +59,7 @@ internal override DrawNode GenerateDrawNodeSubtree(ulong frame, int treeIndex, b // We do not want to receive updates. That is the business of the original drawable. public override bool IsPresent => false; - public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) - { - if (Original.IsDisposed) - return false; - - return Original.UpdateSubTreeMasking(this, maskingBounds); - } + public override bool UpdateSubTreeMasking() => true; private class ProxyDrawNode : DrawNode { diff --git a/osu.Framework/Platform/GameHost.cs b/osu.Framework/Platform/GameHost.cs index badd1f40ce..fd87b73076 100644 --- a/osu.Framework/Platform/GameHost.cs +++ b/osu.Framework/Platform/GameHost.cs @@ -477,7 +477,7 @@ protected virtual void UpdateFrame() TypePerformanceMonitor.NewFrame(); Root.UpdateSubTree(); - Root.UpdateSubTreeMasking(Root, Root.ScreenSpaceDrawQuad.AABBFloat); + Root.UpdateSubTreeMasking(); using (var buffer = DrawRoots.GetForWrite()) buffer.Object = Root.GenerateDrawNodeSubtree(frameCount, buffer.Index, false);