osu!stable / osu!lazer Barline Position Calculations

osu!taiko barlines are generated automatically from red timing point times, beat lengths, and time signatures. osu!stable and osu!lazer use the same measure interval, but differ in their initial starting positions, timing point boundaries, floating-point error correction, and integer conversion. Consequently, the same beatmap may show a 1 ms barline offset or a doubled barline in only one client.

Shared behavior
Repeatedly add the measure length
barLength = beatLength × meter is added repeatedly to the current time.
Specific to stable
Convert to integer when drawing
The repeatedly accumulated double is truncated with conv.i4.
Specific to lazer
Correct values near integers
A value within 1e-7 ms of an integer is corrected to that integer and retained as a double.

1. Research Method and Sources

osu!stable
Reverse engineering osu!.exe

Because the osu!stable source code is not public, the taiko barline-generation path was traced through the disassembled IL of the osu!.exe build under investigation. The core routine is method 060020D7 in that build. The investigation covered the timing point float64 values, repeated measure-length addition, the position of conv.i4, and the condition used to switch to the next red timing point.

Method IDs and obfuscated names are specific to the build under investigation.

osu!lazer
Official GitHub source code

The following links are pinned to the commits examined for this investigation.

Scope: “Barline” on this page means the automatically generated barline that travels across the osu!taiko playfield. It is generated through a different path from the beat and measure lines drawn on the editor timeline.

2. Terms and Assumptions

TermMeaning
redTimeTime of a red line (an uninherited timing point)
beatLengthLength of one beat stored in the red timing point, in milliseconds
meterNumerator of the time signature; normally 4 for 4/4
barLengthLength of one measure, calculated as beatLength × meter
tCurrent candidate barline time; internally a double in both clients
OmitFirstBarLineFlag that omits the first barline at a red timing point; effects bit 3 (value 8) in an .osu file
barLength = beatLength × meter

3. osu!stable Barline Generation Formula

stable 1Initial starting position

stable retrieves only uninherited timing points and sorts them by time. The first candidate t is found by normalizing the first red timing point time to a positive remainder of the first measure length.

barLength = firstRed.beatLength × firstRed.meter

t = firstRed.time
    - trunc(firstRed.time / barLength) × barLength

if (t < 0):
    t += barLength

trunc truncates toward zero. For an ordinary positive measure length, the result is 0 <= t < barLength.

stable 2Repeated addition, red-line switching, and integer conversion

redLines = uninherited timing points
limit = end time of the last HitObject + 1
i = 0
t = initial starting position

while (t <= limit):
    currentRed = redLines[i]

    if not (t <= currentRed.time
            and currentRed.OmitFirstBarLine):
        drawTime = conv.i4(t)
        add a barline at drawTime

    barLength =
        currentRed.beatLength × currentRed.meter

    if (barLength < 0.001):
        switch to the next red line
        t = time of the next red line
    else:
        t += barLength

        if (a next red line exists
            and t >= time of the next red line):
            i += 1
            t = redLines[i].time
Key stable behavior: Even when a barline is mathematically located at an integer, if repeated addition produces N - ε, conv.i4 truncates it to N - 1.

4. osu!lazer Barline Generation Formula

lazer 1Earliest generation time and starting position

generationStartTime = min(0, firstHitTime)
barLength =
    timingPoint.BeatLength
    × timingPoint.TimeSignature.Numerator

if (timingPoint.Time > generationStartTime):
    startTime = timingPoint.Time
else:
    barCount = ceil(
        (generationStartTime - timingPoint.Time)
        / barLength
    )
    startTime =
        timingPoint.Time + barCount × barLength

if (timingPoint.OmitFirstBarLine):
    startTime += barLength

lazer 2Repeated addition, error correction, and section endpoint

endTime =
    a next red line exists
        ? time of the next red line
        : end of the last HitObject + 1 + barLength

for (double t = startTime;
     Precision.AlmostBigger(endTime, t);
     t += barLength):

    roundedTime =
        Math.Round(t, MidpointRounding.AwayFromZero)

    if (abs(t - roundedTime) <= 1e-7):
        t = roundedTime

    add a barline at the double value t
Key lazer behavior: An error just below an integer, such as 121037.99999999991, is corrected to 121038. On the other hand, because the section endpoint is included, both adjacent sections may generate a barline at the same time.

5. Differences Between osu!stable and osu!lazer

Itemosu!stableosu!lazer
Measure lengthbeatLength × meterBeatLength × TimeSignature.Numerator
AdvancementRepeatedly add the measure lengthRepeatedly add the measure length
Initial starting positionNormalize the first red timing point to a positive remainderFirst aligned position at or after min(0, firstHitTime)
Near-integer correctionNoneCorrect when within 1e-7 ms of an integer
Final time valueTruncate toward zero with conv.i4Retain as a double
Candidate matching the next red lineSwitch to the new section before drawingMay generate at both the preceding section’s endpoint and the new section’s start
Final generation rangeEnd of the last HitObject + 1 msEnd of the last HitObject + 1 ms + one measure
Omit first barlineSuppress with a drawing condition at the red-line startAdvance the starting time by one measure

6. Example: A stable Barline 1 ms Before the Resnap Time

USAO - Glitch in My System (polytone Edit) [Charlotte's Inner Oni]

Test beatmap: USAO - Glitch in My System (polytone Edit) [Charlotte's Inner Oni]

redTime = 1038
beatLength = 297.029702970297
meter = 4
barLength = 1188.118811881188...

Theoretical position at 02:01.038

Mathematically, 101 measures after the red timing point at 1038 is 121038 ms, but stable’s repeated-addition result falls just below the integer.

stable barline:
    t = 1038
    add barLength to t 101 times

    rawBarlineTime = 121037.99999999991
    conv.i4(rawBarlineTime) = 121037
    rendered barline time = 02:01.037

stable’s resnap command uses a separate process and calculates the time directly by multiplying the selected snap index. For this 1/1 snap, the result is:

stable resnap:
    snapTime = redTime + 404 × beatLength
    snapTime = 121038
    conv.i4(snapTime) = 121038
    resnap time = 02:01.038
stable barline121037
stable resnap121038
DifferenceBarline is −1 ms

Does the same behavior occur in osu!lazer?

Not in this example. lazer also reaches 121037.99999999991 during calculation, but its distance from the integer is approximately 8.73e-11 ms, which is within the correction tolerance.

lazer barline:
    rawBarlineTime = 121037.99999999991
    roundedTime = 121038
    abs(rawBarlineTime - roundedTime) <= 1e-7
    correct t to 121038
    StartTime = 121038
lazer does not truncate the barline time to an integer; it corrects tiny errors immediately before or after an integer back to that integer. Therefore, it does not produce a barline 1 ms early through the same N - ε → N - 1 mechanism as stable.

7. Example: One Barline in stable, Two in lazer

CS4W - Complementary Contrast (miyagishima) [Inner Oni]

Test beatmap: CS4W - Complementary Contrast (miyagishima) [Inner Oni]

preceding red line:
    992,400,4,1,0,70,1,8
    omit first barline = true

target red line:
    144992,400,4,1,0,100,1,1
    omit first barline = false

barLength = 400 × 4 = 1600
144992 - 992 = 144000 = 90 measures

Because the preceding red timing point omits its first barline, its section generates at 2592, 4192, ... 143392, 144992.

stableSwitch to the next red line first

143392 + 1600 = 144992
144992 >= next red line at 144992
→ switch to the next red line before drawing it
  as part of the preceding section
→ generate one barline at 144992 from the new red line
From preceding section0
From new section1
stable total1

lazerGenerate both the section endpoint and the new section start

preceding section:
    endTime = 144992
    t = 144992
    Precision.AlmostBigger(144992, 144992) = true
    → one barline from the section beginning at 992

new section:
    startTime = 144992
    omit first barline = false
    → one barline from the section beginning at 144992
From preceding section1
From new section1
lazer total2
This doubled barline is not caused by floating-point error. It results from lazer’s boundary condition including the endpoint of the preceding section while the new timing section also generates at the same time. There is no duplicate-removal step after generation.

8. Example: Two Barlines in stable, One in lazer

Halozy - Sakura Saku Utopia (Hata no Kokoro) [Sakura~ Sakura~]
first red line:
    -221,419.58041958042,4,1,0,40,1,0

next red line:
    1457,419.58041958042,4,1,0,80,1,0

first HitObject:
    408 ms

barLength = 419.58041958042 × 4
          = 1678.32167832168 ms

How it occurs

When the first red timing point is at a negative time, both clients find the first non-negative time aligned with that timing point’s measure cycle. This page calls that time the initial candidate. In this example, the initial candidate at 1457.32167832168 ms is approximately 0.322 ms after the next red timing point at 1457 ms.

ClientHow the initial candidate is handledResult in this example
osu!stableDraw before checking the boundary, then convert with conv.i4Generate 1457.321... → 1457 ms
osu!lazerUse the next red timing point as the section endpoint and compare as double before drawingExclude because 1457.321... > 1457
Difference between the clients: stable draws the initial candidate before checking the next red timing point. lazer checks whether the initial candidate is within the section ending at the next red timing point before drawing it.

stableThe internal time moves backward after the initial candidate

initial candidate:
    -221 + 1678.32167832168
    = 1457.32167832168

1. Draw the initial candidate first
   conv.i4(1457.32167832168) = 1457
   → one barline from the red line at -221 ms

2. Add the measure length
   t = 3135.64335664336
   t >= next red line at 1457
   → switch to the next red line and move t back to 1457

3. Generate one barline at 1457 ms
   from the next red line
From first red line1
From next red line1
stable total2

After drawing the initial candidate, stable adds the measure length and checks for a timing-point switch using the next candidate. When the next red timing point is before the initial candidate, stable first generates the initial candidate and then moves its internal time backward to the next red timing point. This produces the unusual time decrease in generation order.

If OmitFirstBarLine is disabled on the next red timing point, its starting barline is also drawn. In this example, conv.i4 truncates the initial candidate from 1457.321... → 1457 ms, so it overlaps the 1457 ms barline from the next red timing point exactly.

Condition for an exact doubled barline: The raw initial-candidate value does not need to equal the next red timing point exactly. The two barlines overlap after integer conversion whenever conv.i4(initial candidate) = next red-line time.

Test beatmapMoving the first red line to −218 ms

Moving only the first red timing point from -221 ms to -218 ms does not change the fact that the initial candidate lies after the next red timing point. It does, however, change the exact time of the barline from the preceding section.

first red line:
    -218,419.58041958042,4,1,0,40,1,0

initial candidate:
    -218 + 1678.32167832168
    = 1460.32167832168

from preceding section:
    conv.i4(1460.32167832168) = 1460

from next red line:
    conv.i4(1457) = 1457
From preceding section1460 ms
From next red line1457 ms
Interval in stable3 ms

stable’s internal generation order becomes 1460 → 1457 ms, decreasing by 3 ms. The lines appear close together on the playfield, but mathematically they are not two barlines at the same time.

lazerExclude the candidate beyond the preceding section’s endpoint

generationStartTime = min(0, 408) = 0

first red-line section:
    barCount = ceil((0 - (-221)) / 1678.32167832168)
             = 1
    startTime = -221 + 1 × 1678.32167832168
              = 1457.32167832168
    endTime = next red line = 1457

    Precision.AlmostBigger(1457, 1457.32167832168)
    = false
    → no barline is generated from the preceding section

next red-line section:
    startTime = 1457
    → generate one barline at 1457 ms
From first red line0
From next red line1
lazer total1
Summary
stable truncates repeated-addition results with conv.i4, so a barline can appear 1 ms before the resnap time.
lazer corrects errors close to integers and retains the time as a double, preventing a 1 ms-early barline caused by the same mechanism.
On the other hand, because lazer includes the endpoint of a red timing section in its generation range, the preceding and new sections can generate barlines at the same time, resulting in one barline in stable but two in lazer.
Conversely, stable draws the initial candidate normalized from a negative first red timing point once without checking it against the next red timing point. If that candidate is at or after the next red timing point, stable generates one extra barline that lazer does not. If its time after integer conversion matches the next red timing point, the result is an exact doubled barline.