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

Add maximum dimensions limit to skinnable gameplay elements #24706

Merged
merged 23 commits into from Sep 26, 2023

Conversation

frenzibyte
Copy link
Member

RFC. This applies to osu! hit circles, taiko hits (drum rolls are already limited), and catch hit objects (fruits, bananas, and droplets). Mania is not included as I'm not even sure how to go about it (if I should).

From the get go, I was planning on re-using SkinnableDrawable's "size confinement mode" logic, but it's not quite usable for the cases in here for two reasons:

  1. Most cases don't create SkinnableDrawable because they're already inside a per-skin component and they fetch the texture directly from the skin.
  2. SkinnableDrawable's size confinement mode doesn't handle texture elements smaller than the standard size (there's a bit of an argument over that one).

So I came up with the idea of a simple Drawable class that wraps around a sprite and downscales it if it exceeds the specified maximum dimensions or the draw size of a specified "target" drawable. However, that resulted in few issues with osu! hit circles, as they apply upscaling to the circle & overlay pieces on hit, which cannot work properly with the class in mind (see discord).

Therefore, I switched to applying the maximum dimensions limit at a texture level instead, by a helper method that tinkers with Texture.ScaleAdjust to downscale the texture if it exceeds maximum dimensions.

Opening this pull request as draft because it is mostly for feedback, and because I haven't thoroughly tested it or compared it with #23025 yet.

@bdach
Copy link
Collaborator

bdach commented Sep 4, 2023

I have several questions...

  1. Most cases don't create SkinnableDrawable because they're already inside a per-skin component and they fetch the texture directly from the skin.

Did you try and see if they can be made SkinnableDrawables? Basically I'm quite loath of providing two ways of doing one thing.

2. SkinnableDrawable's size confinement mode doesn't handle texture elements smaller than the standard size (there's a bit of an argument over that one).

Kinda weird to PR this without reaching a consensus on that first... Basically I can't find a thread on this now, but I recall somebody mentioning somewhere that if you make a really small taiko circle texture, what will happen is that on maps with significant note overlap, the circles due to the shrinkage will overlap less, resulting in easier reading. I think that was the rationale (can we summon like a taiko player or something? maybe i'll try the difficulty channel on discord...)

Other than that:

  • Do we have any degree of confidence that the coverage of elements you have in this pull is in any way comprehensive? As in, how was it determined that only these specific elements need touching?
  • Do you have resources that you can provide for testing this? I guess the assets provided in this pull are one, but being able to get a full skin for comparison & crosscheck against stable would be nice.

@@ -100,6 +105,20 @@ IEnumerable<Texture> getTextures(ISkin skin)
string getFrameName(int frameIndex) => $"{componentName}{animationSeparator}{frameIndex}";
}

public static Texture? GetTextureWithMaxSize(this ISkin source, string componentName, Vector2 maxSize, WrapMode wrapModeS = WrapMode.None, WrapMode wrapModeT = WrapMode.None)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More to the code side of things, why is this a separate method, but GetAnimation() gets an optional maxSize param? Should be one or the other all the way.

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done some code mashing and removed this overload / new method. See if it reads better to you.

Note that I did this as a prelim requirement. I haven't yet assessed whether the overall direction is correct yet.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just quickly going over source without getting too deep into it, definitely seems much closer to what I'd expect to see.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done some code mashing and removed this overload / new method. See if it reads better to you.

It would've been better presenting the change as a proposal first before pushing. I'm not in favour with the new code as it means every skin implementation must respect the maxSize parameter & perform the same exact code of trimming the texture, which is better achieved with an extension method rather than providing that as part of the core method itself, especially since the "maximum size" logic can be executed outside of the core method. Not to mention that this massively increases the diff of this PR.

Both of the reasons above are precisely why I haven't considered doing this.

As an alternative, I wouldn't mind something like skin.GetTexture().WithMaximumSize() or just keep everything as is and rename GetTextureWithMaxSize to GetTexture. Whichever feels better (I would prefer the first, because it's very explicit and reads well in code).

Copy link
Collaborator

@bdach bdach Sep 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it means every skin implementation must respect the maxSize parameter & perform the same exact code of trimming the texture

I don't get why this is a concern... If maxSize is null then the method is a no-op, first of all, so there's no performance consideration there. Secondly, we probably want the max size logic in all implementations for future-proofing. (We were even musing briefly in twitch chat about what happens when user skins are not legacy-texture-based but markup-drawable-based in the future, but dismissed that as unimportant for now.) The diff size concern is like basically irrelevant to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the change has already been pushed, and if you're both on board with it then I wouldn't bother to mind it.

@bdach
Copy link
Collaborator

bdach commented Sep 4, 2023

I recall somebody mentioning somewhere that if you make a really small taiko circle texture, what will happen is that on maps with significant note overlap, the circles due to the shrinkage will overlap less, resulting in easier reading

Asked about this on discord and the general response seems to be that yes it's a problem but probably not one that needs dealing with every urgently. So I guess this point can be dismissed as not hugely relevant.

@peppy
Copy link
Sponsor Member

peppy commented Sep 5, 2023

Probably needs to be added to more elements (for example taiko-roll-end, taiko-roll-middle).

@peppy
Copy link
Sponsor Member

peppy commented Sep 5, 2023

One issue with doing this at the texture level is that in the edge case that a skin element is oversize (ie. due to empty transparent padding), it will be downscaled rather than just-work. That may have been one of the original reasons to do it the way I had it.

@pull-request-size pull-request-size bot added size/L and removed size/M labels Sep 5, 2023
@peppy peppy self-assigned this Sep 5, 2023
@peppy
Copy link
Sponsor Member

peppy commented Sep 5, 2023

Do you have resources that you can provide for testing this? I guess the assets provided in this pull are one, but being able to get a full skin for comparison & crosscheck against stable would be nice.

I think a better approach to test this is to roll back all the resource changes, then create a standalone autoplay gameplay test which has a custom skin provider that upscales every element (except whitelisted ones) to 4x normal scale or something stupid.

I'd explicitly avoid using 2x scaling, as that may trigger @2x edge cases. So if anything, something like 2.4 or 3.

That way we can go through and find the elements easily which haven't had their max dimensions set yet, and we also have a whitelist of all elements we want to allow being larger than expected.

I think this kind of testing is required regardless of the solution, and it would allow better experimentation with potential alternatives.

@frenzibyte want to take a look at this if you agree with the direction?

Copy link
Sponsor Member

@peppy peppy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As commented.

@frenzibyte
Copy link
Member Author

frenzibyte commented Sep 5, 2023

I have several questions...

  1. Most cases don't create SkinnableDrawable because they're already inside a per-skin component and they fetch the texture directly from the skin.

Did you try and see if they can be made SkinnableDrawables? Basically I'm quite loath of providing two ways of doing one thing.

It's possible to replace each use case with a SkinnableSprite/SkinnableAnimation, but I was hesitant of doing that as it means defining a SkinnableDrawable inside of a SkinnableDrawable implementation, which doesn't really make sense because 99% of the inner SkinnableDrawable's functionality (updating on skin change) is moot because it's handled by the outer SkinnableDrawable.

If anything, I would remove the SizeConfinementMode thing from SkinnableDrawable and wrap each usage of that enum with the new container. To me, that looks like the logical step towards this.

  1. SkinnableDrawable's size confinement mode doesn't handle texture elements smaller than the standard size (there's a bit of an argument over that one).

Kinda weird to PR this without reaching a consensus on that first... Basically I can't find a thread on this now, but I recall somebody mentioning somewhere that if you make a really small taiko circle texture, what will happen is that on maps with significant note overlap, the circles due to the shrinkage will overlap less, resulting in easier reading. I think that was the rationale (can we summon like a taiko player or something? maybe i'll try the difficulty channel on discord...)

I believed the consensus has already been reached on peppy's message that "smaller is always allowed"? Well, I suppose the OP doesn't clarify that well.

Other than that:

  • Do we have any degree of confidence that the coverage of elements you have in this pull is in any way comprehensive? As in, how was it determined that only these specific elements need touching?

I've opened this PR to discuss the direction first before making sure this covers all elements. I've already pointed this out in OP at the very end.

  • Do you have resources that you can provide for testing this? I guess the assets provided in this pull are one, but being able to get a full skin for comparison & crosscheck against stable would be nice.

Sure, I can prepare one once this PR reaches the testing state, but the assets provided were meant to be enough to provide samples of how the logic for the direction works in practice.

@@ -47,13 +48,13 @@ private void load(ISkinSource skin, OsuColour colours)
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Texture = skin.GetTexture("taiko-roll-end", WrapMode.ClampToEdge, WrapMode.ClampToEdge),
Texture = skin.GetTexture("taiko-roll-end", new Vector2(128, 256), WrapMode.ClampToEdge, WrapMode.ClampToEdge),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intentionally haven't included osu!taiko drum rolls because they're already being resized in the Sprite definition (notice RSA = Both & FillMode = Fit).

@@ -100,6 +105,20 @@ IEnumerable<Texture> getTextures(ISkin skin)
string getFrameName(int frameIndex) => $"{componentName}{animationSeparator}{frameIndex}";
}

public static Texture? GetTextureWithMaxSize(this ISkin source, string componentName, Vector2 maxSize, WrapMode wrapModeS = WrapMode.None, WrapMode wrapModeT = WrapMode.None)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done some code mashing and removed this overload / new method. See if it reads better to you.

It would've been better presenting the change as a proposal first before pushing. I'm not in favour with the new code as it means every skin implementation must respect the maxSize parameter & perform the same exact code of trimming the texture, which is better achieved with an extension method rather than providing that as part of the core method itself, especially since the "maximum size" logic can be executed outside of the core method. Not to mention that this massively increases the diff of this PR.

Both of the reasons above are precisely why I haven't considered doing this.

As an alternative, I wouldn't mind something like skin.GetTexture().WithMaximumSize() or just keep everything as is and rename GetTextureWithMaxSize to GetTexture. Whichever feels better (I would prefer the first, because it's very explicit and reads well in code).

@frenzibyte
Copy link
Member Author

Do you have resources that you can provide for testing this? I guess the assets provided in this pull are one, but being able to get a full skin for comparison & crosscheck against stable would be nice.

I think a better approach to test this is to roll back all the resource changes, then create a standalone autoplay gameplay test which has a custom skin provider that upscales every element (except whitelisted ones) to 4x normal scale or something stupid.

I'd explicitly avoid using 2x scaling, as that may trigger @2x edge cases. So if anything, something like 2.4 or 3.

That way we can go through and find the elements easily which haven't had their max dimensions set yet, and we also have a whitelist of all elements we want to allow being larger than expected.

I think this kind of testing is required regardless of the solution, and it would allow better experimentation with potential alternatives.

@frenzibyte want to take a look at this if you agree with the direction?

Definitely, I can look into that. It will get a bit funny with elements that are fine to be scaled (e.g. HUD elements), but I'll play with the test to exclude those and see how it goes.

@peppy
Copy link
Sponsor Member

peppy commented Sep 6, 2023

As an alternative, I wouldn't mind something like skin.GetTexture().WithMaximumSize() or just keep everything as is and rename GetTextureWithMaxSize to GetTexture. Whichever feels better (I would prefer the first, because it's very explicit and reads well in code).

Sure, that works better and requires less code changes. (I was almost going to suggest a TextureRetrievalArgs class since there's already so many arguments on the methods).

Feel free to undo my change and apply that, or just revert it.

Definitely, I can look into that. It will get a bit funny with elements that are fine to be scaled (e.g. HUD elements), but I'll play with the test to exclude those and see how it goes.

That's what I meant by a whitelist. It means we will be sure we haven't missed any elements we care about.


namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneGameplayElementDimensions : TestSceneAllRulesetPlayers
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What on earth is this naming..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I...don't know?

@peppy

This comment was marked as resolved.

Copy link
Sponsor Member

@peppy peppy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As commented.

@frenzibyte frenzibyte marked this pull request as ready for review September 19, 2023 18:07
@frenzibyte

This comment was marked as resolved.

@peppy
Copy link
Sponsor Member

peppy commented Sep 20, 2023

I managed to fix the crash, it was something weird with my local caches.

@peppy peppy self-requested a review September 20, 2023 03:37
@@ -41,14 +42,14 @@ public OsuLegacySkinTransformer(ISkin skin)
return this.GetAnimation("sliderscorepoint", false, false);

case OsuSkinComponents.SliderFollowCircle:
var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true);
var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: new Vector2(308f));
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this 308 come from?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wiki, maximum effective follow circle size (https://osu.ppy.sh/wiki/en/Skinning/osu%21#slider, see sliderfollowcircle.png).

Comment on lines +115 to +116
// use scale adjust property for downscaling the texture in order to meet the specified maximum dimensions.
texture.ScaleAdjust *= Math.Max(texture.DisplayWidth / maxSize.X, texture.DisplayHeight / maxSize.Y);
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to have to be very conscious about this, to ensure we don't have any local usages overriding ScaleAdjust themselves.

I've checked all usages in the project right now and it seems okay. For now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a second thought, an alternative way to achieve this could be to have something like a "maximum texture size" property in SkinnableSprite, and if the sprite exceeds that property then the scale of it is adjusted accordingly.

That might be better as to avoid tinkering around with low properties like Texture.ScaleAdjust. Also saves from having to add extra properties to GetTexture/GetAnimation. @peppy thoughts? would it be worth the effort?

@peppy
Copy link
Sponsor Member

peppy commented Sep 20, 2023

I know for a fact that some of these limitations are going to cause skins to break, and will need to be relaxed. For instance, there are skins which add glows to hitobjects, which we previously allowed. In fact, I'm pretty sure there was a previous iteration in lazer where we were limiting things and I fixed such a case.

The only way to figure out sane limits will be to push out these changes and wait for users to report breakage, then decide how much wiggle-room we give skins for exceeding the "correct" dimensions.

@@ -47,13 +48,13 @@ private void load(ISkinSource skin, OsuColour colours)
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Texture = skin.GetTexture("taiko-roll-end", WrapMode.ClampToEdge, WrapMode.ClampToEdge),
Texture = skin.GetTexture("taiko-roll-end", WrapMode.ClampToEdge, WrapMode.ClampToEdge)?.WithMaximumSize(new Vector2(128, 256)),
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these 256 based, not 128?

Also adjusting this doesn't affect the test scene at all?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhh...9d17539

And yes, adjusting doesn't affect because the sprite is resized regardless of the texture, see #24706 (comment).

using osuTK;

namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
public partial class LegacyDropletPiece : LegacyCatchHitObjectPiece
{
private static readonly Vector2 droplet_max_size = new Vector2(100);
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does 100 come from? The template skin is 82 x 103 (@1x).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to say it's correct, but I think I picked 100 as it's closest to 103...I'll change to 82x103

@peppy peppy self-requested a review September 26, 2023 07:32
@peppy
Copy link
Sponsor Member

peppy commented Sep 26, 2023

Will reiterate that this will 100% cause some kind of user skin regressions, which we'll deal with based on feedback.

I know for a fact that some of these limitations are going to cause skins to break, and will need to be relaxed. For instance, there are skins which add glows to hitobjects, which we previously allowed. In fact, I'm pretty sure there was a previous iteration in lazer where we were limiting things and I fixed such a case.

The only way to figure out sane limits will be to push out these changes and wait for users to report breakage, then decide how much wiggle-room we give skins for exceeding the "correct" dimensions.

@peppy peppy merged commit 3b85a63 into ppy:master Sep 26, 2023
11 checks passed
@peppy
Copy link
Sponsor Member

peppy commented Sep 26, 2023

Turns out hot reload was not working, and my scale change in the test scene was not applying correctly in all cases.

As a result, the test scene is quite broken. But in addition, I missed quite a few elements which weren't correctly limited by this PR. Things like follow points (which can be used to reveal object positions in flashlight) need special consideration.

@frenzibyte did you want to look into these cases again?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

Ensure all gameplay elements have maximum dimensions
3 participants