diff --git a/src/bms/player/beatoraja/skin/Skin.java b/src/bms/player/beatoraja/skin/Skin.java index 40353bdc7..f55481aba 100644 --- a/src/bms/player/beatoraja/skin/Skin.java +++ b/src/bms/player/beatoraja/skin/Skin.java @@ -98,6 +98,8 @@ public class Skin { public long pcntPrepare; public long pcntDraw; + private VsyncSmoother vsyncSmoother; + public Skin(SkinHeader header) { this.header = header; Resolution org = header.getSourceResolution(); @@ -212,6 +214,12 @@ public void prepare(MainState state) { objectarray = objects.toArray(SkinObject.class); option.clear(); + if (state.main.getConfig().isVsync()) { + vsyncSmoother = new VsyncSmoother(); + } else { + vsyncSmoother = null; + } + for(SkinObject obj : objects) { obj.load(); } @@ -261,15 +269,17 @@ public void drawAllObjects(SpriteBatch sprite, MainState state) { } final long microtime = state.timer.getNowMicroTime(); + final long smoothedNowTime = (vsyncSmoother != null) + ? vsyncSmoother.smoothTime(state.timer.getNowTime()) + : state.timer.getNowTime(); if (MainController.debug) { if (nextpreparetime <= microtime) { tempmap.forEach((c,l) -> Arrays.fill(l, 1, 6, 0L)); - final long time = state.timer.getNowTime(); var startPrepare = System.nanoTime(); for (SkinObject obj : objectarray) { var objPrepare = System.nanoTime(); - obj.prepare(time, state); + obj.prepare(smoothedNowTime, state); tempmap.get(obj.getClass())[1] += (System.nanoTime() - objPrepare); } pcntPrepare = (System.nanoTime() - startPrepare) / 1000; @@ -298,9 +308,8 @@ public void drawAllObjects(SpriteBatch sprite, MainState state) { } else { if (nextpreparetime <= microtime) { - final long time = state.timer.getNowTime(); for (SkinObject obj : objectarray) { - obj.prepare(time, state); + obj.prepare(smoothedNowTime, state); } nextpreparetime += ((microtime - nextpreparetime) / prepareduration + 1) * prepareduration; diff --git a/src/bms/player/beatoraja/skin/VsyncSmoother.java b/src/bms/player/beatoraja/skin/VsyncSmoother.java new file mode 100644 index 000000000..ca72ebe67 --- /dev/null +++ b/src/bms/player/beatoraja/skin/VsyncSmoother.java @@ -0,0 +1,77 @@ +// Copyright (C) 2025 Park Hyunwoo +// +// This file is part of beatoraja. +// +// beatoraja is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// beatoraja is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with beatoraja. If not, see . + +package bms.player.beatoraja.skin; + +import com.badlogic.gdx.Gdx; + +public class VsyncSmoother { + private float targetTime = 0; // Output value + + public void reset() { + targetTime = 0; + } + + /** + * Smooths out [newTime] to more evenly-spaced values. + * + * @param newTime Raw input time in milliseconds + * @return Smoothed out time. + */ + public long smoothTime(long newTime) { + float timeInterval = 1000.0f / Gdx.graphics.getDisplayMode().refreshRate; + + // Detect frame skip and advance targetTime + // Note: this might be due to display HZ change, but this will be averaged out in `averageDelta` as + // the time passes by, and `targetTime` will follow up. + + // newTime = [true display time] - α + // we expect α to be in range of [0, averageDelta) + // where targetTime tries to estimate [true display time] - α. (There is no way to expect α as I know of) + // + // let targetTime = [true display time] - β + // abs(α - β) < averageDelta (must) -> if (a - b) > averageDelta → miss + + // targetTime ~= [previous display time] - β + // newTime - targetTime = ([true display time] - α) - ([previous display time] - β) + // = averageDelta + (β - α) <= averageDelta + averageDelta + // if this inequality misses, we assume that the frame skip occurred + if (newTime - targetTime >= timeInterval * 2.02f) { + // Allow additional 0.02f to prevent false positive + long frameSkips = (long) (Math.floor((newTime - targetTime) / timeInterval) - 1); + targetTime += timeInterval * frameSkips; + } + + // Some beatoraja code just spams a bunch of `time=0` before starting. + if (timeInterval == 0) { + return newTime; + } + + float timeDiff = targetTime - newTime - timeInterval; + float targetTimeDiff = timeInterval; + if (timeDiff < -timeInterval) { + targetTimeDiff += Math.min(timeInterval, (-timeDiff - timeInterval) / 2 + 0.05f * timeInterval); + } else if (timeDiff < timeInterval) { + targetTimeDiff -= timeDiff * 0.05f; + // Do nothing + } else { + targetTimeDiff -= Math.min(timeInterval, (timeDiff - timeInterval) / 2 + 0.05f * timeInterval); + } + targetTime += targetTimeDiff; + return (long) Math.round(targetTime); + } +}