Skip to content
9 changes: 8 additions & 1 deletion osu.Android/Input/AndroidKeyboardHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ public class AndroidKeyboardHandler : InputHandler
{ Keycode.Period, Key.Period }, { Keycode.Slash, Key.Slash },
}.ToFrozenDictionary();

// Cached enabled state. Enabled.Value reads through the Bindable<bool> property
// chain; a volatile field gives the OS dispatch thread a direct read that is also
// cross-thread correct.
private volatile bool cachedEnabled = true;

public AndroidKeyboardHandler()
{
Enabled.Default = true;
Expand All @@ -65,13 +70,15 @@ public override bool Initialize(GameHost host)
if (!base.Initialize(host))
return false;

Enabled.BindValueChanged(v => cachedEnabled = v.NewValue, true);

return true;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HandleKeyEvent(KeyEvent e)
{
if (!Enabled.Value) return false;
if (!cachedEnabled) return false;

if (e.KeyCode == Keycode.Back || e.KeyCode == Keycode.Home || e.KeyCode == Keycode.Menu ||
e.KeyCode == Keycode.VolumeUp || e.KeyCode == Keycode.VolumeDown || e.KeyCode == Keycode.VolumeMute ||
Expand Down
9 changes: 8 additions & 1 deletion osu.Android/Input/AndroidMouseHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ public class AndroidMouseHandler : InputHandler, IHasCursorSensitivity
// Avoids a BindableDouble read per MotionEvent.
private volatile float cachedSensitivity = 1f;

// Cached enabled state. Enabled.Value reads through the Bindable<bool> property
// chain; a volatile field gives the OS dispatch thread a direct read that is also
// cross-thread correct (changes written on the Update thread are immediately
// visible here via the volatile acquire/release semantics).
private volatile bool cachedEnabled = true;

public AndroidMouseHandler()
{
Enabled.Default = true;
Expand All @@ -66,14 +72,15 @@ public override bool Initialize(GameHost host)
return false;

Sensitivity.BindValueChanged(v => cachedSensitivity = (float)v.NewValue, true);
Enabled.BindValueChanged(v => cachedEnabled = v.NewValue, true);

return true;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HandleMotionEvent(MotionEvent e)
{
if (!Enabled.Value) return false;
if (!cachedEnabled) return false;

if (e.ActionMasked == MotionEventActions.Scroll)
{
Expand Down
8 changes: 7 additions & 1 deletion osu.Android/Input/AndroidStylusHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ public class AndroidStylusHandler : InputHandler, ITabletHandler
private bool useRotation;
private float cachedPressureThreshold;

// Cached enabled state. Enabled.Value reads through the Bindable<bool> property
// chain; a volatile field gives the OS dispatch thread a direct read that is also
// cross-thread correct.
private volatile bool cachedEnabled = true;

// Cached tablet bounds — updated whenever `tablet.Value` is reassigned. Avoids
// three bindable reads + property accesses per historical pointer sample in the
// hot path. A local-field comparison is a single un-locked memory read.
Expand Down Expand Up @@ -178,6 +183,7 @@ public override bool Initialize(GameHost host)

Rotation.BindValueChanged(_ => updateCachedTransform());
PressureThreshold.BindValueChanged(v => cachedPressureThreshold = v.NewValue, true);
Enabled.BindValueChanged(v => cachedEnabled = v.NewValue, true);

// Force one initial cache population so `areaWidth` / `outWidth` are non-zero
// before the very first MotionEvent arrives (BindValueChanged above only fires
Expand Down Expand Up @@ -285,7 +291,7 @@ private void updateCachedTransform()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HandleMotionEvent(MotionEvent e)
{
if (!Enabled.Value) return false;
if (!cachedEnabled) return false;

// Cache ActionMasked once: each `e.ActionMasked` access is a JNI call into
// MotionEvent#getActionMasked. On a busy stylus drag the previous code did
Expand Down
18 changes: 15 additions & 3 deletions osu.Android/OsuGameAndroid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,13 @@ public partial class OsuGameAndroid : OsuGame
private IntPtr adpfDrawSession;
private IntPtr adpfUpdateSession;

// Cached GameThread references stored when ADPF sessions are created. Avoids walking
// Host?.DrawThread? / Host?.UpdateThread? / Host?.InputThread? on every frame callback
// (3 levels of nullable dereference at 120 Hz × 3 threads = 360 null checks/s → 0).
private osu.Framework.Threading.GameThread? adpfDrawThread;
private osu.Framework.Threading.GameThread? adpfUpdateThread;
private osu.Framework.Threading.GameThread? adpfInputThread;

// Input thread ADPF session. The input thread may poll at ~100 kHz (one cycle ≈ 10 µs),
// which is far finer than the ADPF reporting granularity. Instead of calling
// reportActualWorkDuration once per poll (which would flood the ADPF API), we accumulate
Expand Down Expand Up @@ -603,6 +610,9 @@ protected override void LoadComplete()
// Silent no-op on API < 35 (resolved via dlsym).
OboeAudioBridge.nADPFSetPreferPowerEfficiency(adpfDrawSession, 0);
Logger.Log($"[osu!] ADPF session created for Draw thread (target={targetNs / 1_000_000.0:F2}ms)", LoggingTarget.Performance);
// Cache the thread reference now (we're already on the Draw thread)
// so per-frame callbacks avoid the Host?.DrawThread? nullable chain.
adpfDrawThread = Host!.DrawThread;
// Subscribe per-frame reporting now that the session handle is valid.
// FrameCompleted fires on the Draw thread itself, so reading
// Host.DrawThread.Clock.ElapsedFrameTime is thread-safe.
Expand All @@ -622,6 +632,7 @@ protected override void LoadComplete()
{
OboeAudioBridge.nADPFSetPreferPowerEfficiency(adpfUpdateSession, 0);
Logger.Log($"[osu!] ADPF session created for Update thread (target={targetNs / 1_000_000.0:F2}ms)", LoggingTarget.Performance);
adpfUpdateThread = Host!.UpdateThread;
Host!.UpdateThread!.FrameCompleted += onUpdateFrameCompleted;
}
}
Expand All @@ -643,6 +654,7 @@ protected override void LoadComplete()
{
OboeAudioBridge.nADPFSetPreferPowerEfficiency(adpfInputSession, 0);
Logger.Log($"[osu!] ADPF session created for Input thread (target={targetNs / 1_000_000.0:F2}ms)", LoggingTarget.Performance);
adpfInputThread = Host!.InputThread;
Host!.InputThread!.FrameCompleted += onInputFrameCompleted;
}
}
Expand Down Expand Up @@ -1607,7 +1619,7 @@ private void onDrawFrameCompleted()

try
{
double elapsedMs = Host?.DrawThread?.Clock.ElapsedFrameTime ?? 0;
double elapsedMs = adpfDrawThread?.Clock.ElapsedFrameTime ?? 0;
if (elapsedMs > 0)
OboeAudioBridge.nADPFReportActualDuration(adpfDrawSession, (long)(elapsedMs * 1_000_000.0));
}
Expand All @@ -1624,7 +1636,7 @@ private void onUpdateFrameCompleted()

try
{
double elapsedMs = Host?.UpdateThread?.Clock.ElapsedFrameTime ?? 0;
double elapsedMs = adpfUpdateThread?.Clock.ElapsedFrameTime ?? 0;
if (elapsedMs > 0)
OboeAudioBridge.nADPFReportActualDuration(adpfUpdateSession, (long)(elapsedMs * 1_000_000.0));
}
Expand All @@ -1644,7 +1656,7 @@ private void onInputFrameCompleted()

try
{
double elapsedMs = Host?.InputThread?.Clock.ElapsedFrameTime ?? 0;
double elapsedMs = adpfInputThread?.Clock.ElapsedFrameTime ?? 0;
if (elapsedMs <= 0) return;

inputAdpfAccumulatedMs += elapsedMs;
Expand Down
18 changes: 13 additions & 5 deletions osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
Expand Down Expand Up @@ -116,12 +115,21 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current)
if (previousIsland.DeltaCount == island.DeltaCount)
effectiveRatio *= 0.5;

var islandCount = islandCounts.FirstOrDefault(x => x.Island.Equals(island));
int islandCountIndex = -1;
(Island Island, int Count) islandCount = default;

if (islandCount != default)
for (int k = 0; k < islandCounts.Count; k++)
{
int countIndex = islandCounts.IndexOf(islandCount);
if (islandCounts[k].Island.Equals(island))
{
islandCountIndex = k;
islandCount = islandCounts[k];
break;
}
}

if (islandCountIndex >= 0)
{
// only add island to island counts if they're going one after another
if (previousIsland.Equals(island))
islandCount.Count++;
Expand All @@ -130,7 +138,7 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current)
double power = DifficultyCalculationUtils.Logistic(island.Delta, maxValue: 2.75, multiplier: 0.24, midpointOffset: 58.33);
effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power));

islandCounts[countIndex] = (islandCount.Island, islandCount.Count);
islandCounts[islandCountIndex] = (islandCount.Island!, islandCount.Count);
}
else
{
Expand Down
14 changes: 12 additions & 2 deletions osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
Expand Down Expand Up @@ -63,7 +62,18 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnly
// Apply reduced small circle bonus because flow aim difficulty on small circles doesn't scale as hard as jumps
distanceBonus *= Math.Sqrt(osuCurrObj.SmallCircleBonus);

if (mods.OfType<OsuModAutopilot>().Any())
bool hasAutopilot = false;

for (int i = 0; i < mods.Count; i++)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
{
if (mods[i] is OsuModAutopilot)
{
hasAutopilot = true;
break;
}
}

if (hasAutopilot)
distanceBonus = 0;

// Base difficulty with all bonuses
Expand Down
70 changes: 61 additions & 9 deletions osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
Expand Down Expand Up @@ -51,10 +50,36 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
if (beatmap.HitObjects.Count == 0)
return new OsuDifficultyAttributes { Mods = mods };

var aim = skills.OfType<Aim>().Single(a => a.IncludeSliders);
var aimWithoutSliders = skills.OfType<Aim>().Single(a => !a.IncludeSliders);
var speed = skills.OfType<Speed>().Single();
var flashlight = skills.OfType<Flashlight>().SingleOrDefault();
Aim? aim = null;
Aim? aimWithoutSliders = null;
Speed? speed = null;
Flashlight? flashlight = null;

for (int i = 0; i < skills.Length; i++)
{
switch (skills[i])
{
case Aim a when a.IncludeSliders:
aim = a;
break;

case Aim a:
aimWithoutSliders = a;
break;

case Speed s:
speed = s;
break;

case Flashlight f:
flashlight = f;
break;
}
}

ArgumentNullException.ThrowIfNull(aim);
ArgumentNullException.ThrowIfNull(aimWithoutSliders);
ArgumentNullException.ThrowIfNull(speed);

double speedNotes = speed.RelevantNoteCount();

Expand All @@ -74,9 +99,25 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
double approachRate = CalculateRateAdjustedApproachRate(beatmap.Difficulty.ApproachRate, clockRate);
double overallDifficulty = CalculateRateAdjustedOverallDifficulty(beatmap.Difficulty.OverallDifficulty, clockRate);

int hitCircleCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
int hitCircleCount = 0, sliderCount = 0, spinnerCount = 0;

for (int i = 0; i < beatmap.HitObjects.Count; i++)
{
switch (beatmap.HitObjects[i])
{
case HitCircle:
hitCircleCount++;
break;

case Slider:
sliderCount++;
break;

case Spinner:
spinnerCount++;
break;
}
}

int totalHits = beatmap.HitObjects.Count;

Expand Down Expand Up @@ -186,7 +227,18 @@ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clo
new Speed(mods)
};

if (mods.Any(h => h is OsuModFlashlight))
bool hasFlashlight = false;

for (int i = 0; i < mods.Length; i++)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
{
if (mods[i] is OsuModFlashlight)
{
hasFlashlight = true;
break;
}
}

if (hasFlashlight)
skills.Add(new Flashlight(mods));

return skills.ToArray();
Expand Down
17 changes: 14 additions & 3 deletions osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using osu.Framework.Graphics;
using osu.Framework.Utils;
Expand Down Expand Up @@ -308,7 +307,7 @@ private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, floa
var previousActions = previousFrame.Actions;

// If a button is already held, then we simply alternate
if (previousActions.Any())
if (previousActions.Count > 0)
{
// Force alternation if we have the same button. Otherwise we can just keep the naturally to us assigned button.
if (previousActions.Contains(action))
Expand All @@ -330,7 +329,7 @@ private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, floa
var frame = (OsuReplayFrame)Frames[j];

// Don't affect frames which stop pressing a button!
if (j < Frames.Count - 1 || frame.Actions.SequenceEqual(previousActions))
if (j < Frames.Count - 1 || actionsEqual(frame.Actions, previousActions))
{
frame.Actions.Clear();
frame.Actions.Add(action);
Expand Down Expand Up @@ -395,6 +394,18 @@ private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, floa
AddFrameToReplay(endFrame);
}

private static bool actionsEqual(List<OsuAction> a, List<OsuAction> b)
{
if (a.Count != b.Count) return false;

for (int i = 0; i < a.Count; i++)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
{
if (a[i] != b[i]) return false;
}

return true;
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
}

#endregion

private class OsuKeyUpReplayFrame : OsuReplayFrame
Expand Down
3 changes: 1 addition & 2 deletions osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.

using System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.StateChanges;
using osu.Framework.Utils;
using osu.Game.Replays;
Expand All @@ -17,7 +16,7 @@ public OsuFramedReplayInputHandler(Replay replay)
{
}

protected override bool IsImportant(OsuReplayFrame frame) => frame.Actions.Any();
protected override bool IsImportant(OsuReplayFrame frame) => frame.Actions.Count > 0;

protected override void CollectReplayInputs(List<IInput> inputs)
{
Expand Down
Loading
Loading