Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Game crash when changing addition sample bank for multiple objects #28629

Closed
catsstuffs opened this issue Jun 27, 2024 · 10 comments · Fixed by #29933
Closed

Game crash when changing addition sample bank for multiple objects #28629

catsstuffs opened this issue Jun 27, 2024 · 10 comments · Fixed by #29933
Assignees
Labels
area:editor missing details Can't move forward without more details from the reporter type:reliability

Comments

@catsstuffs
Copy link

catsstuffs commented Jun 27, 2024

Type

Crash to desktop

Bug description

The game crashes after selecting multiple objects with different addition, then attempting changing the addition sample bank on a slider end with a hitsound. You can reproduce like in the video by doing CTRL-A to a map with multiple banks for additions, changing the addition bank of a slider end, deselecting, then adding a hitsound to the slider end. I've not been able to get it to consistently reproduce either, so I'm not sure what direction to give there

Screenshots or videos

2024-06-26.21-14-44.mp4

Version

2024.625.2-lazer

Logs

compressed-logs.zip

@catsstuffs
Copy link
Author

image
After more testing, I'm getting mixed results where on some maps it will end up just changing the banks to the last letter I input on the addition sample box instead of crashing :o

@bdach bdach self-assigned this Jun 27, 2024
@bdach
Copy link
Collaborator

bdach commented Jun 27, 2024

Crash in the logs:

2024-06-27 02:15:20 [verbose]: Too many unhandled exceptions, crashing out.
2024-06-27 02:15:20 [error]: An unhandled error has occurred.
2024-06-27 02:15:20 [error]: System.NullReferenceException: Object reference not set to an instance of an object.
2024-06-27 02:15:20 [error]: at osu.Game.Rulesets.Osu.Objects.Drawables.DrawableOsuHitObject.<UpdateInitialTransforms>g__applyDim|15_0(Drawable piece) in /home/runner/work/osu-auth-client/osu-auth-client/osu/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs:line 101
2024-06-27 02:15:20 [error]: at osu.Game.Rulesets.Objects.Drawables.DrawableHitObject.UpdateState(ArmedState newState, Boolean force) in /home/runner/work/osu-auth-client/osu-auth-client/osu/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs:line 492
2024-06-27 02:15:20 [error]: at osu.Game.Rulesets.Osu.Objects.Drawables.DrawableSliderTail.SuppressHitAnimations() in /home/runner/work/osu-auth-client/osu-auth-client/osu/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs:line 135
2024-06-27 02:15:20 [error]: at osu.Framework.Graphics.Drawable.UpdateSubTree()
2024-06-27 02:15:20 [error]: at osu.Framework.Graphics.Containers.CompositeDrawable.UpdateSubTree()

Looking at sentry this doesn't to be specific to changing samples, or even the editor. I see several instances of this from different stacks. More likely that the method in question is just generally unsafe.

After more testing, I'm getting mixed results where on some maps it will end up just changing the banks to the last letter I input on the addition sample box instead of crashing :o

I can't reproduce this. Please provide a video of this showing, preferably with the link to the map(s) that exhibit this behaviour.

@bdach bdach added area:editor type:reliability missing details Can't move forward without more details from the reporter labels Jun 27, 2024
@ppy-sentryintegration
Copy link

Sentry issue: OSU-133H

@bdach
Copy link
Collaborator

bdach commented Jun 27, 2024

I guess for the crash I would do this...

diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index 5f5deca1ba..f71d0d1d62 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -6,6 +6,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using JetBrains.Annotations;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
@@ -73,6 +74,7 @@ protected override void OnFree()
             ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
         }
 
+        [ItemCanBeNull]
         protected virtual IEnumerable<Drawable> DimmablePieces => Enumerable.Empty<Drawable>();
 
         protected override void UpdateInitialTransforms()
@@ -91,7 +93,7 @@ protected override void UpdateInitialTransforms()
                     drawableObjectPiece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
                     drawableObjectPiece.ApplyCustomUpdateState += applyDimToDrawableHitObject;
                 }
-                else
+                else if (piece?.IsLoaded == true)
                     applyDim(piece);
             }
 

but I can't reproduce this crashing for one, and secondly it's a bit of a dicey one. I'm pretty sure it should be safe because DrawableHitObjects.LoadComplete() calls UpdateState(force: true), but it'll be difficult to ascertain in the general...

@ppy/team-client thoughts on this?

@peppy
Copy link
Member

peppy commented Jun 27, 2024

hmm, what would you say to this solution instead?

diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 7d707dea6c..760e15190c 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -325,6 +325,9 @@ public ProxyableSkinnableDrawable(ISkinComponentLookup lookup, Func<ISkinCompone
 
         internal void SuppressHitAnimations()
         {
+            if (!IsLoaded)
+                return;
+
             UpdateState(ArmedState.Idle, true);
             UpdateComboColour();
 
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 02d0ebee83..69ad126f7b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -375,6 +375,9 @@ private partial class DefaultSliderBody : PlaySliderBody
 
         internal void SuppressHitAnimations()
         {
+            if (!IsLoaded)
+                return;
+
             UpdateState(ArmedState.Idle, true);
             HeadCircle.SuppressHitAnimations();
             TailCircle.SuppressHitAnimations();
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
index 42abf41d6f..053ca25398 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
@@ -132,6 +132,9 @@ protected override void OnApply()
 
         internal void SuppressHitAnimations()
         {
+            if (!IsLoaded)
+                return;
+
             UpdateState(ArmedState.Idle, true);
             UpdateComboColour();
 

I believe this is specific to the new flow, where SuppressHitAnimations is called by a parent drawable potentially a frame before the child is fully loaded. So under normal circumstances things should never break (and the check you added in UpdateInitialTransforms is nor required).

@bdach
Copy link
Collaborator

bdach commented Jun 27, 2024

I believe this is specific to the new flow

Except it isn't. Check "merged issues" under the sentry link above, you'll see more stacks than that.

1719478113
1719478082
1719478008

The last one is especially freaky. I haven't seen that one before, not sure how that is even possible.

@peppy
Copy link
Member

peppy commented Jun 27, 2024

Okay that's very scary. I think I'd want to understand how these can happen before deciding on a fix..

@peppy
Copy link
Member

peppy commented Jun 27, 2024

The only way I can see the last of those screenshots happening is a DrawableSlider being loaded without its nested objects being added yet. Assuming nested objects aren't just being skipped completely, this suggests OnApply is being run after LoadComplete... which I guess is feasible since there's quite a few calls directly to Apply(), with some suspicious ones in the editor.

@catsstuffs
Copy link
Author

catsstuffs commented Jun 27, 2024

I could recreate the crash and the issue from the 2nd screenshot on a map I made, and on this map. Another difficulty had no issues changing the addition banks, oddly enough. I've attached two videos of me doing so on all three, and the map on gofile (github hates olz)

compressed3.mp4
compressed2.mp4

@catsstuffs
Copy link
Author

if the link to download the map ever dies, let me know and i can attempt to reupload it ^^

bdach added a commit to bdach/osu that referenced this issue Sep 19, 2024
…allbacks

Should fix ppy#28629.

First of all, to support the claim that this does fix the issue -
reproduction is rather difficult, but I believe I found a way to
maximise the chances of it reproducing by performing the following
steps:

1. Apply the following diff:

diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index eacd2b3e75..4c00da031a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -6,6 +6,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Threading;
 using JetBrains.Annotations;
 using osu.Framework.Allocation;
 using osu.Framework.Bindables;
@@ -95,6 +96,8 @@ public DrawableSlider([CanBeNull] Slider s = null)
         [BackgroundDependencyLoader]
         private void load()
         {
+            Thread.Sleep(100);
+
             tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both };

             AddRangeInternal(new Drawable[]

2. Download https://osu.ppy.sh/beatmapsets/1470790#osu/3023028 and open
   it in the editor.

3. Select all objects using Ctrl-A. Yes, it'll take a while, especially
   so with the patch above.

4. Rotate the selection by any amount using the right toolbox.

5. Press undo. The game should crash. If it doesn't spam redo and undo
   until it does.

Now to explain what the fix is.

In the issue thread I spent a considerable time hemming and hawing about
which of the dimmable pieces was null, which was a complete miss and a
failure at reading. Let's see the stack trace again:

	2024-06-27 02:15:20 [error]: at osu.Game.Rulesets.Osu.Objects.Drawables.DrawableOsuHitObject.<UpdateInitialTransforms>g__applyDim|15_0(Drawable piece) in /home/runner/work/osu-auth-client/osu-auth-client/osu/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs:line 101

Line 101, you say? What could be null here?

	https://github.com/ppy/osu/blob/bd8addfb5f71568479d2c037d1b6e811de6e7fe6/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs#L101

Okay, what's `InitialLifetimeOffset`, then?

	https://github.com/ppy/osu/blob/bd8addfb5f71568479d2c037d1b6e811de6e7fe6/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs#L108

Yes, that's right. It's `HitObject` that is null here.

Now why does *that* happen? First, let's note that all stacks where this
died went through `UpdateState()`, which means that the problematic
`applyDim()` calls had to be `ApplyCustomUpdateState` event callbacks.
Meaning that the pieces where `HitObject` was null were DHOs themselves.

Recall that parent DHOs and child DHOs are pooled separately. Therefore,
there is no guarantee that any parent and child DHOs will remain
associated with each other for the entire duration of a gameplay
session; it is quite the contrary, and nobody should rely on that.
Unfortunately for us, adding a `applyDimToDrawableHitObject` callback to
a child object's `ApplyCustomUpdateState` *implicitly creates* such an
association, because it ends up allocating a closure that captures
`this` (meaning the parent in this context).

Therefore, this now creates a situation where a child DHO can attempt to
read state from a former parent DHO which can be in an indeterminate
state, and in fact, when this crashes, the former parent DHO is most
likely not even in use - hence the null `HitObject`.

Thus, the fix is to clear the association by unsubscribing from the
event when nested objects are cleared.

My hypothesis why the reproduction scenario is like it is, is that both
the sleep and the increased pressure on the pool (by way of selecting
all objects and therefore forcing the DHOs to be materialised beyond
pool capacity) increases the likelihood of getting a crosslink.
When pool pressure is low, it is much more likely that a parent DHO
*will* get the same child DHO again on re-application, even though
that is not guaranteed.

Just as an additional detail, note that the sentry issue for this lists
the "first seen" version as 2024.312.0, which is the release that
included ppy#27401 which would be directly
responsible for this mess.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:editor missing details Can't move forward without more details from the reporter type:reliability
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants