// SPDX-License-Identifier: Unlicense

#include "hud.h"

#include "font.h"
#include "geometry.h"
#include "global.h"
#include "opengl.h"
#include "shader.h"
#include "song.h"
#include "version.h"

#ifdef PLATFORM_OPENXR_ANDROID
#include <openxr/openxr.h>
#endif // PLATFORM_OPENXR_ANDROID

#ifdef SHR3D_WINDOW_SDL
#include <SDL.h>
#endif // SHR3D_WINDOW_SDL

static void tickHideCursorWhenNotMoving()
{
  bool showCursor = true;

  static i32 lastInputCursorPosX;
  static i32 lastInputCursorPosY;
  static f32 lastCursorMovement;

  if (Global::inputHideMenu.toggled && lastInputCursorPosX == Global::inputCursorPosX && lastInputCursorPosY == Global::inputCursorPosY)
  {
    lastCursorMovement += TimeNS_To_Seconds(Global::frameDelta);
    showCursor = lastCursorMovement < Const::cursorHideCursorTimeout;
  }
  else
  {
    lastCursorMovement = 0.0f;
    lastInputCursorPosX = Global::inputCursorPosX;
    lastInputCursorPosY = Global::inputCursorPosY;
  }

#ifdef SHR3D_WINDOW_SDL
  SDL_ShowCursor(showCursor);
#endif // SHR3D_WINDOW_SDL
#ifdef SHR3D_WINDOW_WIN32
  if (!showCursor)
    SetCursor(nullptr);
#endif // SHR3D_WINDOW_WIN32
}

static bool checkAndHandleClickInTimeline(const f32 left, const f32 right, const f32 top, const f32 bottom)
{
  if (Global::inputCursorPosX >= left && Global::inputCursorPosX <= right && Global::inputCursorPosY >= top && Global::inputCursorPosY <= bottom)
  {
    if (Global::inputLmb.pressed)
    {
      const f32 progress = f32(Global::inputCursorPosX - left) / f32(right - left);
      ASSERT(progress >= 0.0f);
      ASSERT(progress <= 1.0f);

      Global::musicTimeElapsedNS = TimeNS(progress * f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd)) - Global::songInfos[Global::selectedSongIndex].shredDelayBegin;

#ifdef SHR3D_MUSIC_STRETCHER
      Global::musicPlaybackPosition = i64(musicTimeElapsedToPlaybackPositionNonStretched(Global::musicTimeElapsedNS, f32(sampleRate())) * Global::musicStretchRatio);
#else // SHR3D_MUSIC_STRETCHER
      Global::musicPlaybackPosition = i64(TimeNS_To_Seconds(Global::musicTimeElapsedNS) * f32(sampleRate()));
#endif // SHR3D_MUSIC_STRETCHER
    }

    return true;
  }
  return false;
}

static void tickTimeline(const ArrangementIndex selectedArrangementIndex, const i32 resolutionWidth, const i32 resolutionHeight)
{
  bool showIBeamCursor = false;

  if (Global::selectedSongIndex >= 0 && Global::inputHideMenu.toggled)
  {
    if (Settings::hudTimelineTone)
    {
      const f32 left = resolutionWidth * 0.5f * (1.0f + Settings::hudTimelineToneX - Settings::hudTimelineToneScaleX);
      const f32 right = resolutionWidth * 0.5f * (1.0f + Settings::hudTimelineToneX + Settings::hudTimelineToneScaleX);
      const f32 top = resolutionHeight * 0.5f * (1.0f - (Settings::hudTimelineToneY + Settings::hudTimelineToneScaleY));
      const f32 bottom = resolutionHeight * 0.5f * (1.0f - Settings::hudTimelineToneY);

      showIBeamCursor = checkAndHandleClickInTimeline(left, right, top, bottom);
    }
    if (Settings::hudTimelineLevel)
    {
      if (Global::songTracks[selectedArrangementIndex].levels.size() >= 1)
      {
        const f32 left = resolutionWidth * 0.5f * (1.0f + Settings::hudTimelineLevelX - Settings::hudTimelineLevelScaleX);
        const f32 right = resolutionWidth * 0.5f * (1.0f + Settings::hudTimelineLevelX + Settings::hudTimelineLevelScaleX);
        const f32 top = resolutionHeight * 0.5f * (1.0f - Settings::hudTimelineLevelY);
        const f32 bottom = resolutionHeight * 0.5f * (1.0f - Settings::hudTimelineLevelY + Settings::hudTimelineLevelScaleY);

        showIBeamCursor = checkAndHandleClickInTimeline(left, right, top, bottom);
      }
    }
  }

#ifdef SHR3D_WINDOW_SDL
  if (showIBeamCursor)
  {
    static SDL_Cursor* iBeamCurser = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM);
    SDL_SetCursor(iBeamCurser);
  }
  else
    SDL_SetCursor(Global::defaultCursor);
#endif // SHR3D_WINDOW_SDL
}

static void tickArrangementSwitch(Hud::Ctx& ctx, const ArrangementIndex selectedArrangementIndex)
{
  if (Global::selectedSongIndex >= 0 && selectedArrangementIndex >= 0)
  {
    static SongIndex songIndexLastFrame = Global::selectedSongIndex;

    if (songIndexLastFrame != Global::selectedSongIndex || ctx.arrangementIndexLastFrame != selectedArrangementIndex)
      ctx.arrangementSwitchTime = Global::time_;

    songIndexLastFrame = Global::selectedSongIndex;
    ctx.arrangementIndexLastFrame = selectedArrangementIndex;
  }
}

void Hud::tick(Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const i32 resolutionWidth, const i32 resolutionHeight)
{
#ifdef SHR3D_COOP
  if (!Global::inputCoop.toggled) // TODO: cursor flickers on second window. Disable hiding cursor.
    tickHideCursorWhenNotMoving();
#endif // SHR3D_COOP
  tickTimeline(selectedArrangementIndex, resolutionWidth, resolutionHeight);
  tickArrangementSwitch(ctx, selectedArrangementIndex);
}

static void drawArrangementSwitch(const TimeNS arrangementSwitchTime, const ArrangementIndex selectedArrangementIndex, const i32 resolutionWidth, const i32 resolutionHeight)
{
  if (Global::selectedSongIndex >= 0 && selectedArrangementIndex >= 0)
  {
    const f32 timeElapsed = TimeNS_To_Seconds(Global::time_ - arrangementSwitchTime);

    if (Const::hudDrawArrangementSwitchEndDuration < timeElapsed)
      return;

    f32 alpha = 1.0f;
    if (Const::hudDrawArrangementSwitchFadeInDuration > timeElapsed)
      alpha = timeElapsed / (Const::hudDrawArrangementSwitchFadeInDuration);
    else if (Const::hudDrawArrangementSwitchFadeOutDuration < timeElapsed)
      alpha = (timeElapsed - Const::hudDrawArrangementSwitchEndDuration) / (Const::hudDrawArrangementSwitchFadeOutDuration - Const::hudDrawArrangementSwitchEndDuration);

#ifdef SHR3D_FONT_BITMAP
    GL(glUseProgram(Shader::fontScreen));
    GL(glUniform4f(Shader::fontScreenUniformColor, Settings::hudArrangementSwitchColor.r, Settings::hudArrangementSwitchColor.g, Settings::hudArrangementSwitchColor.b, Settings::hudArrangementSwitchColor.a * alpha));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
    GL(glUseProgram(Shader::fontMSDFScreen));
    GL(glUniform4f(Shader::fontMSDFScreenUniformColor, Settings::hudArrangementSwitchColor.r, Settings::hudArrangementSwitchColor.g, Settings::hudArrangementSwitchColor.b, Settings::hudArrangementSwitchColor.a * alpha));
    GL(glUniform1f(Shader::fontMSDFScreenUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

    {
      const f32 scaleX = Settings::hudArrangementSwitchScaleX * f32(Font1::textureWidth(Global::songInfos[Global::selectedSongIndex].arrangementInfos[selectedArrangementIndex].arrangementName.c_str())) * Font1::fontScaleFactor / f32(resolutionWidth);
      const f32 scaleY = Settings::hudArrangementSwitchScaleY * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
      Font1::draw(Global::songInfos[Global::selectedSongIndex].arrangementInfos[selectedArrangementIndex].arrangementName.c_str(), Settings::hudArrangementSwitchX, Settings::hudArrangementSwitchY, 0.0f, scaleX, scaleY);
    }

    GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
  }
}

static void drawSfxTone(const i32 resolutionWidth, const i32 resolutionHeight)
{
  const f32 timeElapsed = TimeNS_To_Seconds(Global::time_ - Global::sfxToneTime);

  if (Const::hudDrawSfxToneEndDuration < timeElapsed)
    return;

  f32 alpha = 1.0f;
  if (Const::hudDrawSfxToneFadeInDuration > timeElapsed)
    alpha = timeElapsed / (Const::hudDrawSfxToneFadeInDuration);
  else if (Const::hudDrawSfxToneFadeOutDuration < timeElapsed)
    alpha = (timeElapsed - Const::hudDrawSfxToneEndDuration) / (Const::hudDrawSfxToneFadeOutDuration - Const::hudDrawSfxToneEndDuration);

#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontScreen));
  GL(glUniform4f(Shader::fontScreenUniformColor, Settings::hudToneSwitchColor.r, Settings::hudToneSwitchColor.g, Settings::hudToneSwitchColor.b, Settings::hudToneSwitchColor.a * alpha));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFScreen));
  GL(glUniform4f(Shader::fontMSDFScreenUniformColor, Settings::hudToneSwitchColor.r, Settings::hudToneSwitchColor.g, Settings::hudToneSwitchColor.b, Settings::hudToneSwitchColor.a * alpha));
  GL(glUniform1f(Shader::fontMSDFScreenUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

  {
    char8_t text[64];
    sprintf(reinterpret_cast<char*>(text), "Tone Switch [B%2d|T%2d+%d]", (Global::activeSfxToneIndex + (Global::activeSfxToneIndex >= 0 ? Global::sfxToneAutoSwitchOffset : -Global::sfxToneAutoSwitchOffset)) / Const::sfxToneTonesPerBank, (abs(Global::activeSfxToneIndex)) % Const::sfxToneTonesPerBank, Global::sfxToneAutoSwitchOffset);
    const f32 scaleX = Settings::hudToneSwitchScale[0] * f32(Font1::textureWidth(text)) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 scaleY = Settings::hudToneSwitchScale[0] * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
    Font1::draw(text, Settings::hudToneSwitchX - scaleX, Settings::hudToneSwitchY[0], 0.0f, scaleX, scaleY);
  }
#ifdef SHR3D_SFX
  {
    const std::u8string& currentToneName = Global::sfxToneNames[Global::activeSfxToneIndex + (Global::activeSfxToneIndex >= 0 ? Global::sfxToneAutoSwitchOffset : -Global::sfxToneAutoSwitchOffset)];
    if (currentToneName.size() != 0)
    {
      const f32 scaleX = Settings::hudToneSwitchScale[1] * f32(Font1::textureWidth(currentToneName.c_str())) * Font1::fontScaleFactor / f32(resolutionWidth);
      const f32 scaleY = Settings::hudToneSwitchScale[1] * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
      Font1::draw(currentToneName.c_str(), Settings::hudToneSwitchX - scaleX, Settings::hudToneSwitchY[1], 0.0f, scaleX, scaleY);
    }
  }
#endif // SHR3D_SFX
  GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
}

static void drawSections(const ArrangementIndex selectedArrangementIndex)
{
  if (Settings::hudTimelineTone)
  {
    const f32 left = Settings::hudTimelineToneX + 2.0f * -0.5f * Settings::hudTimelineToneScaleX;
    const f32 right = Settings::hudTimelineToneX + 2.0f * 0.5f * Settings::hudTimelineToneScaleX;
    const f32 top = Settings::hudTimelineToneY + Settings::hudTimelineToneScaleY;
    const f32 bottom = Settings::hudTimelineToneY;

    // for sprites triangleStrip: 4 Verts + UV. Format: x,y,z,u,v
    const GLfloat v[] = {
      left , top, 0.3f, 0.0f, 1.0f,
      left, bottom, 0.3f, 0.0f, 0.0f,
      right, top, 0.3f, 1.0f, 1.0f,
      right, bottom, 0.3f, 1.0f, 0.0f,
    };

    GL(glUseProgram(Shader::timelineToneScreen));

    const f32 progress = f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::musicTimeElapsedNS) / f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd);
    GL(glUniform1f(Shader::timelineToneScreenUniformProgress, progress));

    f32 tonePos[100];
    vec4 toneColor[102];
    ASSERT(Global::songTracks[selectedArrangementIndex].tones.size() < ARRAY_SIZE(tonePos));
    toneColor[0] = Settings::hudTimelineToneColor[0];
    toneColor[1] = Settings::hudTimelineToneColor[1];
    {
      i32 i = 0;
      for (; i < Global::songTracks[selectedArrangementIndex].tones.size(); ++i)
      {
        ASSERT(Global::songTracks[selectedArrangementIndex].tones[i].id >= 0);
        ASSERT(Global::songTracks[selectedArrangementIndex].tones[i].id <= 9);
        tonePos[i] = f32(Global::songTracks[selectedArrangementIndex].tones[i].timeNS) / f32(Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd);
        toneColor[i + 2] = Settings::hudTimelineToneColor[Global::songTracks[selectedArrangementIndex].tones[i].id + 1];
      }
      for (; i < ARRAY_SIZE(tonePos); ++i)
      {
        tonePos[i] = 1.0f;
      }
    }

    GL(glUniform1fv(Shader::timelineToneScreenUniformTonePos, ARRAY_SIZE(tonePos), tonePos));
    GL(glUniform4fv(Shader::timelineToneScreenUniformToneColor, ARRAY_SIZE(toneColor), &toneColor[0].r));

    GL(glBufferData(GL_ARRAY_BUFFER, sizeof(v), v, GL_STATIC_DRAW));
    GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, 4));
  }
  if (Settings::hudTimelineLevel)
  {

    i32 overallMaxLevel = 0;
    for (const Song::LeveledSection& leveledSections : Global::songTracks[selectedArrangementIndex].leveledSections)
      if (leveledSections.maxLevel > overallMaxLevel)
        overallMaxLevel = leveledSections.maxLevel;

    for (i32 i = 0; i < i32(Global::songTracks[selectedArrangementIndex].leveledSections.size()); ++i)
    {
      const Song::LeveledSection& leveledSections = Global::songTracks[selectedArrangementIndex].leveledSections[i];

      f32 begin = TimeNS_To_Seconds(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + leveledSections.startTimeNS) / TimeNS_To_Seconds(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd);
      f32 end = TimeNS_To_Seconds(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + leveledSections.endTimeNS) / TimeNS_To_Seconds(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd);
      f32 level = f32(leveledSections.maxLevel) / f32(overallMaxLevel);

      const f32 left = Settings::hudTimelineLevelX + 2.0f * (begin - 0.5f) * Settings::hudTimelineLevelScaleX + Settings::hudTimelineLevelSpacing * 0.01f;
      const f32 right = Settings::hudTimelineLevelX + 2.0f * (end - 0.5f) * Settings::hudTimelineLevelScaleX - Settings::hudTimelineLevelSpacing * 0.01f;

      f32 top;
      f32 bottom;
      if (Settings::hudTimelineLevelFlipY)
      {
        top = Settings::hudTimelineLevelY - Settings::hudTimelineLevelScaleY + (level * Settings::hudTimelineLevelScaleY);
        bottom = Settings::hudTimelineLevelY - Settings::hudTimelineLevelScaleY;
      }
      else
      {
        top = Settings::hudTimelineLevelY;
        bottom = Settings::hudTimelineLevelY - (level * Settings::hudTimelineLevelScaleY);
      }

      // for sprites triangleStrip: 4 Verts + UV. Format: x,y,z,u,v
      const GLfloat v[] = {
        left , top, 0.4f, 0.0f, Settings::hudTimelineLevelFlipY ? 1.0f : 0.0f,
        left, bottom, 0.4f, 0.0f, Settings::hudTimelineLevelFlipY ? 0.0f : 1.0f,
        right, top, 0.4f, 1.0f, Settings::hudTimelineLevelFlipY ? 1.0f : 0.0f,
        right, bottom, 0.4f, 1.0f, Settings::hudTimelineLevelFlipY ? 0.0f : 1.0f,
      };

      GL(glUseProgram(Shader::timelineLevelScreen));

      const f32 progress = map_(Global::musicTimeElapsedNS, leveledSections.startTimeNS, leveledSections.endTimeNS, 0.0f, 1.0f);
      GL(glUniform1f(Shader::timelineLevelScreenUniformProgress, progress));

      const f32 selectedLevel = f32(Global::songLevels[selectedArrangementIndex].sectionLevels[i]) / f32(leveledSections.maxLevel);
      GL(glUniform1f(Shader::timelineLevelScreenUniformSelectedLevel, selectedLevel));

      GL(glUniform4f(Shader::timelineLevelScreenUniformColor, Settings::hudTimelineLevelColor[0].r, Settings::hudTimelineLevelColor[0].g, Settings::hudTimelineLevelColor[0].b, Settings::hudTimelineLevelColor[0].a));
      GL(glUniform4f(Shader::timelineLevelScreenUniformColor2, Settings::hudTimelineLevelColor[1].r, Settings::hudTimelineLevelColor[1].g, Settings::hudTimelineLevelColor[1].b, Settings::hudTimelineLevelColor[1].a));
      GL(glUniform4f(Shader::timelineLevelScreenUniformColor3, Settings::hudTimelineLevelColor[2].r, Settings::hudTimelineLevelColor[2].g, Settings::hudTimelineLevelColor[2].b, Settings::hudTimelineLevelColor[2].a));
      GL(glUniform4f(Shader::timelineLevelScreenUniformColor4, Settings::hudTimelineLevelColor[3].r, Settings::hudTimelineLevelColor[3].g, Settings::hudTimelineLevelColor[3].b, Settings::hudTimelineLevelColor[3].a));
      GL(glUniform4f(Shader::timelineLevelScreenUniformColor5, Settings::hudTimelineLevelColor[4].r, Settings::hudTimelineLevelColor[4].g, Settings::hudTimelineLevelColor[4].b, Settings::hudTimelineLevelColor[4].a));
      GL(glUniform4f(Shader::timelineLevelScreenUniformColor6, Settings::hudTimelineLevelColor[5].r, Settings::hudTimelineLevelColor[5].g, Settings::hudTimelineLevelColor[5].b, Settings::hudTimelineLevelColor[5].a));
      GL(glUniform4f(Shader::timelineLevelScreenUniformColor7, Settings::hudTimelineLevelColor[6].r, Settings::hudTimelineLevelColor[6].g, Settings::hudTimelineLevelColor[6].b, Settings::hudTimelineLevelColor[6].a));
      GL(glUniform4f(Shader::timelineLevelScreenUniformColor8, Settings::hudTimelineLevelColor[7].r, Settings::hudTimelineLevelColor[7].g, Settings::hudTimelineLevelColor[7].b, Settings::hudTimelineLevelColor[7].a));

      GL(glBufferData(GL_ARRAY_BUFFER, sizeof(v), v, GL_STATIC_DRAW));
      GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, 4));
    }
  }
#ifdef SHR3D_MUSIC_STRETCHER

  if (Settings::hudTimelineMusicStretcher && Global::musicPlaybackBufferLR[0] != Global::musicDoubleBuffer[to_underlying_(Global::musicDoubleBufferStatus) % 2])
  {
    const f32 left = Settings::hudTimelineMusicStretcherX + 2.0f * -0.5f * Settings::hudTimelineMusicStretcherScaleX;
    const f32 right = Settings::hudTimelineMusicStretcherX + 2.0f * 0.5f * Settings::hudTimelineMusicStretcherScaleX;
    const f32 top = Settings::hudTimelineMusicStretcherY + Settings::hudTimelineMusicStretcherScaleY;
    const f32 bottom = Settings::hudTimelineMusicStretcherY;

    // for sprites triangleStrip: 4 Verts + UV. Format: x,y,z,u,v
    const GLfloat v[] = {
      left , top, 0.2f, 0.0f, 1.0f,
      left, bottom, 0.2f, 0.0f, 0.0f,
      right, top, 0.2f, 1.0f, 1.0f,
      right, bottom, 0.2f, 1.0f, 0.0f,
    };

    GL(glUseProgram(Shader::timelineMusicStretcherScreen));

    const f32 progress0 = f32(Global::musicStretcherInputCurrent) / f32(Global::musicBufferLength);
    GL(glUniform1f(Shader::timelineMusicStretcherScreenUniformProgress0, progress0));

    const f32 progress1 = f32(Global::musicStretcherInputBegin) / f32(Global::musicBufferLength);
    GL(glUniform1f(Shader::timelineMusicStretcherScreenUniformProgress1, progress1));

    GL(glUniform4f(Shader::timelineMusicStretcherScreenUniformColor, Settings::hudTimelineMusicStretcherColor[0].r, Settings::hudTimelineMusicStretcherColor[0].g, Settings::hudTimelineMusicStretcherColor[0].b, Settings::hudTimelineMusicStretcherColor[0].a));
    GL(glUniform4f(Shader::timelineMusicStretcherScreenUniformColor2, Settings::hudTimelineMusicStretcherColor[1].r, Settings::hudTimelineMusicStretcherColor[1].g, Settings::hudTimelineMusicStretcherColor[1].b, Settings::hudTimelineMusicStretcherColor[1].a));

    GL(glBufferData(GL_ARRAY_BUFFER, sizeof(v), v, GL_STATIC_DRAW));
    GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, 4));
  }
#endif // SHR3D_MUSIC_STRETCHER
}

static void drawQuickRepeater()
{
  if (Global::quickRepeaterBeginTimeNS == I64::max)
    return;

  const f32 begin = f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::quickRepeaterBeginTimeNS) / f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd);
  const f32 end = Global::quickRepeaterEndTimeNS == I64::max
    ? f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::musicTimeElapsedNS) / f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd)
    : f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::quickRepeaterEndTimeNS) / f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd);

  GL(glUseProgram(Shader::timelineSimpleScreen));

  const f32 progress = Global::quickRepeaterEndTimeNS == I64::max
    ? 1.0f
    : map_(Global::musicTimeElapsedNS, Global::quickRepeaterBeginTimeNS, Global::quickRepeaterEndTimeNS, 0.0f, 1.0f);
  GL(glUniform1f(Shader::timelineSimpleScreenUniformProgress, progress));

  {
    const f32 left = Settings::hudTimelineQuickRepeaterX + -1.0f + (begin - 0.02f) * 2.0f;
    const f32 right = Settings::hudTimelineQuickRepeaterX + -1.0f + begin * 2.0f;

    f32 top;
    f32 bottom;
    if (Settings::hudTimelineQuickRepeaterFlipY)
    {
      top = Settings::hudTimelineQuickRepeaterY + 0.5f * Settings::hudTimelineQuickRepeaterScaleY;
      bottom = Settings::hudTimelineQuickRepeaterY;
    }
    else
    {
      top = Settings::hudTimelineQuickRepeaterY;
      bottom = Settings::hudTimelineQuickRepeaterY - 0.5f * Settings::hudTimelineQuickRepeaterScaleY;
    }

    const f32 posZ = 0.2f;

    const GLfloat v[] = {
      left, Settings::hudTimelineQuickRepeaterFlipY ? bottom : top, posZ, 0.0f, 0.0f,
      right, bottom, posZ, 1.0f, Settings::hudTimelineQuickRepeaterFlipY ? 0.0f : 1.0f,
      right, top, posZ, 1.0f, Settings::hudTimelineQuickRepeaterFlipY ? 1.0f : 0.0f,
    };

    GL(glUniform4f(Shader::timelineSimpleScreenUniformColor, Settings::hudTimelineQuickRepeaterColor[1].r, Settings::hudTimelineQuickRepeaterColor[1].g, Settings::hudTimelineQuickRepeaterColor[1].b, Settings::hudTimelineQuickRepeaterColor[1].a));
    GL(glUniform4f(Shader::timelineSimpleScreenUniformColor2, Settings::hudTimelineQuickRepeaterColor[1].r, Settings::hudTimelineQuickRepeaterColor[1].g, Settings::hudTimelineQuickRepeaterColor[1].b, Settings::hudTimelineQuickRepeaterColor[1].a));

    GL(glBufferData(GL_ARRAY_BUFFER, sizeof(v), v, GL_STATIC_DRAW));
    GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, 3));
  }
  {
    const f32 left = Settings::hudTimelineQuickRepeaterX + -1.0f + begin * 2.0f;
    const f32 right = Settings::hudTimelineQuickRepeaterX + -1.0f + end * 2.0f;

    f32 top;
    f32 bottom;
    if (Settings::hudTimelineQuickRepeaterFlipY)
    {
      top = Settings::hudTimelineQuickRepeaterY + 0.25f * Settings::hudTimelineQuickRepeaterScaleY;
      bottom = Settings::hudTimelineQuickRepeaterY;

    }
    else
    {
      top = Settings::hudTimelineQuickRepeaterY;
      bottom = Settings::hudTimelineQuickRepeaterY - 0.25f * Settings::hudTimelineQuickRepeaterScaleY;
    }
    const f32 posZ = 0.2f;

    const GLfloat v[] = {
      left , top, posZ, 0.0f, Settings::hudTimelineQuickRepeaterFlipY ? 0.5f : 0.0f,
      left, bottom, posZ, 0.0f, Settings::hudTimelineQuickRepeaterFlipY ? 0.0f : 0.5f,
      right, top, posZ, 1.0f, Settings::hudTimelineQuickRepeaterFlipY ? 0.5f : 0.0f,
      right, bottom, posZ, 1.0f, Settings::hudTimelineQuickRepeaterFlipY ? 0.0f : 0.5f,
    };

    GL(glUniform4f(Shader::timelineSimpleScreenUniformColor, Settings::hudTimelineQuickRepeaterColor[0].r, Settings::hudTimelineQuickRepeaterColor[0].g, Settings::hudTimelineQuickRepeaterColor[0].b, Settings::hudTimelineQuickRepeaterColor[0].a));
    GL(glUniform4f(Shader::timelineSimpleScreenUniformColor2, Settings::hudTimelineQuickRepeaterColor[1].r, Settings::hudTimelineQuickRepeaterColor[1].g, Settings::hudTimelineQuickRepeaterColor[1].b, Settings::hudTimelineQuickRepeaterColor[1].a));

    GL(glBufferData(GL_ARRAY_BUFFER, sizeof(v), v, GL_STATIC_DRAW));
    GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, 4));
  }
  if (Global::quickRepeaterEndTimeNS != I64::max)
  {
    const f32 left = Settings::hudTimelineQuickRepeaterX + -1.0f + end * 2.0f;
    const f32 right = Settings::hudTimelineQuickRepeaterX + -1.0f + (end + 0.02f) * 2.0f;
    f32 top;
    f32 bottom;
    if (Settings::hudTimelineQuickRepeaterFlipY)
    {
      top = Settings::hudTimelineQuickRepeaterY + 0.5f * Settings::hudTimelineQuickRepeaterScaleY;
      bottom = Settings::hudTimelineQuickRepeaterY;
    }
    else
    {
      top = Settings::hudTimelineQuickRepeaterY;
      bottom = Settings::hudTimelineQuickRepeaterY - 0.5f * Settings::hudTimelineQuickRepeaterScaleY;
    }
    const f32 posZ = 0.2f;

    const GLfloat v[] = {
      left , top, posZ, 0.0f, Settings::hudTimelineQuickRepeaterFlipY ? 1.0f : 0.0f,
      left, bottom, posZ, 0.0f, Settings::hudTimelineQuickRepeaterFlipY ? 0.0f : 1.0f,
      right, Settings::hudTimelineQuickRepeaterFlipY ? bottom : top, posZ, 1.0f, 0.0f,
    };

    GL(glUniform1f(Shader::timelineSimpleScreenUniformProgress, 0.0f));

    GL(glUniform4f(Shader::timelineSimpleScreenUniformColor, Settings::hudTimelineQuickRepeaterColor[0].r, Settings::hudTimelineQuickRepeaterColor[0].g, Settings::hudTimelineQuickRepeaterColor[0].b, Settings::hudTimelineQuickRepeaterColor[0].a));
    GL(glUniform4f(Shader::timelineSimpleScreenUniformColor2, Settings::hudTimelineQuickRepeaterColor[0].r, Settings::hudTimelineQuickRepeaterColor[0].g, Settings::hudTimelineQuickRepeaterColor[0].b, Settings::hudTimelineQuickRepeaterColor[0].a));

    GL(glBufferData(GL_ARRAY_BUFFER, sizeof(v), v, GL_STATIC_DRAW));
    GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, 3));
  }
}

static void drawSongInfo(const i32 resolutionWidth, const i32 resolutionHeight)
{
  if (timeNS_From_Seconds(Const::hudDrawSongInfoEndDuration) < Global::musicTimeElapsedNS)
    return;

  if (timeNS_From_Seconds(Const::hudDrawSongInfoStartDuration) > Global::musicTimeElapsedNS)
    return;

  f32 alpha = 1.0f;
  if (timeNS_From_Seconds(Const::hudDrawSongInfoFadeInDuration) > Global::musicTimeElapsedNS)
    alpha = f32(Global::musicTimeElapsedNS - timeNS_From_Seconds(Const::hudDrawSongInfoStartDuration)) / f32(timeNS_From_Seconds(Const::hudDrawSongInfoFadeInDuration) - timeNS_From_Seconds(Const::hudDrawSongInfoStartDuration));
  else if (timeNS_From_Seconds(Const::hudDrawSongInfoFadeOutDuration) < Global::musicTimeElapsedNS)
    alpha = f32(Global::musicTimeElapsedNS - timeNS_From_Seconds(Const::hudDrawSongInfoEndDuration)) / f32(timeNS_From_Seconds(Const::hudDrawSongInfoFadeOutDuration) - timeNS_From_Seconds(Const::hudDrawSongInfoEndDuration));

#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontScreen));
  const GLint uniformColor = Shader::fontScreenUniformColor;
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFScreen));
  GL(glUniform1f(Shader::fontMSDFScreenUniformScreenPxRange, Const::fontMsdfScreenPxRange));
  const GLint uniformColor = Shader::fontMSDFScreenUniformColor;
#endif // SHR3D_FONT_MSDF

  {
    GL(glUniform4f(uniformColor, Settings::hudSongInfoColor[0].r, Settings::hudSongInfoColor[0].g, Settings::hudSongInfoColor[0].b, Settings::hudSongInfoColor[0].a * alpha));

    const f32 scaleX = Settings::hudSongInfoScale[0] * f32(Font1::textureWidth(Global::songInfos[Global::selectedSongIndex].songName.c_str())) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 scaleY = Settings::hudSongInfoScale[0] * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
    Font1::draw(Global::songInfos[Global::selectedSongIndex].songName.c_str(), Settings::hudSongInfoX - scaleX, Settings::hudSongInfoY[0], 0.0f, scaleX, scaleY);
  }
  {
    GL(glUniform4f(uniformColor, Settings::hudSongInfoColor[1].r, Settings::hudSongInfoColor[1].g, Settings::hudSongInfoColor[1].b, Settings::hudSongInfoColor[1].a * alpha));

    const f32 scaleX = Settings::hudSongInfoScale[1] * f32(Font1::textureWidth(Global::songInfos[Global::selectedSongIndex].artistName.c_str())) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 scaleY = Settings::hudSongInfoScale[1] * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
    Font1::draw(Global::songInfos[Global::selectedSongIndex].artistName.c_str(), Settings::hudSongInfoX - scaleX, Settings::hudSongInfoY[1], 0.0f, scaleX, scaleY);
  }
  GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
}

static i32 drawLyricsUsedText(const i32 resolutionWidth, const i32 resolutionHeight, const GLint colorUniform, const i32 line0Begin, const i32 lineUsed)
{
  i32 textureWidth = 0;

  if (line0Begin <= lineUsed)
  { // draw used text
    char8_t usedText[4096];
    i32 outLetters = 0;
    for (i32 i = line0Begin; i <= lineUsed; ++i)
    {
      const Song::Vocal& vocal = Global::songVocals[i];

      i32 inLetters = 0;
      while (vocal.lyric[inLetters] != '\0' && vocal.lyric[inLetters] != '+')
      {
        if (vocal.lyric[inLetters] != '-')
        {
          usedText[outLetters] = vocal.lyric[inLetters];
          ++outLetters;
        }
        ++inLetters;
      }
      if (inLetters == 0)
        return 0;
      if (vocal.lyric[inLetters - 1] != '-')
      {
        usedText[outLetters] = ' ';
        ++outLetters;
      }
    }

    usedText[outLetters] = '\0';

    GL(glUniform4f(colorUniform, Settings::hudLyricsColor[0].r, Settings::hudLyricsColor[0].g, Settings::hudLyricsColor[0].b, Settings::hudLyricsColor[0].a));

    {
      textureWidth = Font1::textureWidth(usedText);
      const f32 scaleX = Settings::hudLyricsScale * textureWidth * Font1::fontScaleFactor / f32(resolutionWidth);
      const f32 scaleY = Settings::hudLyricsScale * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
      Font1::draw(usedText, Settings::hudLyricsX + scaleX, Settings::hudLyricsY[0], 0.0f, scaleX, scaleY);
    }
  }

  return textureWidth;
}

static i32 drawLyricsActiveText(const i32 resolutionWidth, const i32 resolutionHeight, const GLint colorUniform, const i32 line0Active, const i32 usedTextureWidth)
{
  i32 textureWidth = 0;

  { // draw active text
    char8_t line0[4096];

    i32 inLetters = 0;
    i32 outLetters = 0;
    {
      const Song::Vocal& vocal = Global::songVocals[line0Active];

      while (vocal.lyric[inLetters] != '\0')
      {
        if (vocal.lyric[inLetters] != '-')
        {
          line0[outLetters] = vocal.lyric[inLetters];
          ++outLetters;
        }
        ++inLetters;
      }
      ASSERT(inLetters >= 1);
      if (vocal.lyric[inLetters - 1] == '+')
      {
        --outLetters;
      }

      if (outLetters == 0)
        return 0;

      line0[outLetters] = '\0';
    }

    GL(glUniform4f(colorUniform, Settings::hudLyricsColor[1].r, Settings::hudLyricsColor[1].g, Settings::hudLyricsColor[1].b, Settings::hudLyricsColor[1].a));

    {
      const f32 offsetX = Settings::hudLyricsScale * 2.0f * usedTextureWidth * Font1::fontScaleFactor / f32(resolutionWidth);
      textureWidth = Font1::textureWidth(line0);
      const f32 scaleX = Settings::hudLyricsScale * f32(textureWidth) * Font1::fontScaleFactor / f32(resolutionWidth);
      const f32 scaleY = Settings::hudLyricsScale * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
      Font1::draw(line0, Settings::hudLyricsX + scaleX + offsetX, Settings::hudLyricsY[0], 0.0f, scaleX, scaleY);
    }

    if (Global::songVocals[line0Active].lyric[Global::songVocals[line0Active].lyric.size() - 1] != '-')
      textureWidth += Font1::textureWidth(u8" ");//++line0Cur;
  }

  return textureWidth;
}

static void drawLyricsUnusedText(const i32 resolutionWidth, const i32 resolutionHeight, const GLint colorUniform, i32 lineUnused, i32 line0End, const i32 usedTextureWidth)
{
  char8_t line0[4096];

  i32 outLetters = 0;
  for (i32 i = lineUnused; i < line0End; ++i)
  {
    const Song::Vocal& vocal = Global::songVocals[i];

    i32 inLetters = 0;
    while (vocal.lyric[inLetters] != '\0')
    {
      if (vocal.lyric[inLetters] != '-')
      {
        line0[outLetters] = vocal.lyric[inLetters];
        ++outLetters;
      }
      ++inLetters;
    }
    ASSERT(inLetters >= 1);
    if (vocal.lyric[inLetters - 1] != '-')
    {
      line0[outLetters] = ' ';
      ++outLetters;
    }
  }
  const Song::Vocal& vocal = Global::songVocals[line0End];
  i32 j = 0;
  while (vocal.lyric[j] != '+' && vocal.lyric[j] != '\0')
  {
    line0[outLetters + j] = vocal.lyric[j];
    ++j;
  }
  line0[outLetters + j] = '\0';
  outLetters += j;

  GL(glUniform4f(colorUniform, Settings::hudLyricsColor[2].r, Settings::hudLyricsColor[2].g, Settings::hudLyricsColor[2].b, Settings::hudLyricsColor[2].a));

  {
    const f32 offsetX = Settings::hudLyricsScale * 2.0f * f32(usedTextureWidth) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 scaleX = Settings::hudLyricsScale * Font1::textureWidth(line0) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 scaleY = Settings::hudLyricsScale * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
    Font1::draw(line0, Settings::hudLyricsX + scaleX + offsetX, Settings::hudLyricsY[0], 0.0f, scaleX, scaleY);
  }
}

static void drawLyricsNextLine(const i32 resolutionWidth, const i32 resolutionHeight, const GLint colorUniform, i32 line0End)
{
  i32 line1End = 0;
  for (i32 i = line0End + 1; i < Global::songVocals.size(); ++i)
  {
    const Song::Vocal& vocal = Global::songVocals[i];

    if (vocal.lyric[vocal.lyric.size() - 1] == '+')
    {
      line1End = i;
      break;
    }
  }
  if (line1End == 0)
    return; // There is no next line.

  char8_t line1[4096];
  i32 line1Cur = 0;

  for (i32 i = line0End + 1; i < line1End; ++i)
  {
    const Song::Vocal& vocal = Global::songVocals[i];

    i32 inLetters = 0;
    i32 outLetters = 0;
    while (vocal.lyric[inLetters] != '\0')
    {
      if (vocal.lyric[inLetters] != '-')
      {
        line1[line1Cur + outLetters] = vocal.lyric[inLetters];
        ++outLetters;
      }
      ++inLetters;
    }
    if (vocal.lyric[inLetters - 1] != '-')
    {
      line1[line1Cur + outLetters] = ' ';
      ++line1Cur;
    }
    line1Cur += outLetters;
  }

  const Song::Vocal& vocal = Global::songVocals[line1End];
  i32 inLetters = 0;
  i32 outLetters = 0;
  while (vocal.lyric[inLetters] != '+')
  {
    if (vocal.lyric[inLetters] != '-')
    {
      line1[line1Cur + outLetters] = vocal.lyric[inLetters];
      ++outLetters;
    }
    ++inLetters;
  }

  if (line1Cur + outLetters == 0)
    return;

  line1[line1Cur + outLetters] = '\0';

  GL(glUniform4f(colorUniform, Settings::hudLyricsColor[2].r, Settings::hudLyricsColor[2].g, Settings::hudLyricsColor[2].b, Settings::hudLyricsColor[2].a));
  {
    const i32 textureWidth = Font1::textureWidth(line1);
    const f32 scaleX = Settings::hudLyricsScale * f32(textureWidth) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 scaleY = Settings::hudLyricsScale * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
    Font1::draw(line1, Settings::hudLyricsX + scaleX, Settings::hudLyricsY[1], 0.0f, scaleX, scaleY);
  }
}

static void drawLyrics(const i32 resolutionWidth, const i32 resolutionHeight)
{
  if (Global::songVocals.size() == 0)
    return;

  // hide lyrics on intro and outro
  if (Global::songVocals[0].timeNS - Global::musicTimeElapsedNS > Const::hudLyricsHideIntroOutroDuration)
    return;
  if (Const::hudLyricsHideIntroOutroDuration < Global::musicTimeElapsedNS - Global::songVocals[Global::songVocals.size() - 1].timeNS - Global::songVocals[Global::songVocals.size() - 1].lengthNS)
    return;

  i32 lineBegin = 0;
  i32 lineUsed = -1;
  i32 lineActive = -1;
  i32 lineEnd = i32(Global::songVocals.size()) - 1;

  for (i32 i = lineBegin; i <= lineEnd; ++i)
  {
    const Song::Vocal& vocal = Global::songVocals[i];

    if (vocal.timeNS <= Global::musicTimeElapsedNS)
    {
      if (Global::musicTimeElapsedNS < vocal.timeNS + vocal.lengthNS)
        lineActive = i;
      else
        lineUsed = i;
    }

    if (vocal.lyric[vocal.lyric.size() - 1] == u8'+')
    {
      const Song::Vocal& vocalNext = Global::songVocals[i + 1];
      if (Global::musicTimeElapsedNS < vocalNext.timeNS)
      {
        lineEnd = i;
        break;
      }
      if (i == lineEnd)
        break;
      lineBegin = i + 1;
    }
  }

#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontScreen));
  const GLuint colorUniform = Shader::fontScreenUniformColor;
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFScreen));
  const GLuint colorUniform = Shader::fontMSDFScreenUniformColor;
  GL(glUniform1f(Shader::fontMSDFScreenUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

  i32 textureWidth = 0;

  if (lineUsed != -1)
    textureWidth += drawLyricsUsedText(resolutionWidth, resolutionHeight, colorUniform, lineBegin, lineUsed);

  if (lineActive != -1)
    textureWidth += drawLyricsActiveText(resolutionWidth, resolutionHeight, colorUniform, lineActive, textureWidth);

  {
    const i32 lineUnused = max_(lineActive, lineUsed);
    if (lineUnused != lineEnd)
      drawLyricsUnusedText(resolutionWidth, resolutionHeight, colorUniform, lineUnused + 1, lineEnd, textureWidth);
  }
  drawLyricsNextLine(resolutionWidth, resolutionHeight, colorUniform, lineEnd);

  GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
}

static void drawScore(const i32 resolutionWidth, const i32 resolutionHeight)
{
  f32 integer;
  const f32 progress = modf(Global::score / 3.0f, &integer);
  const i32 scoreMultiplier = clamp(i32(integer) + 1, 1, 99);

  {
    GL(glUseProgram(Shader::scoreScreen));

    GL(glUniform1f(Shader::scoreScreenUniformProgress, progress));

    {
      const f32 top = 0.66f;
      const f32 bottom = 0.63f;
      const f32 posZ = 0.25f;

      // for sprites triangleStrip: 4 Verts + UV. Format: x,y,z,u,v
      const GLfloat v[] = {
        0.72f , top, posZ, 0.0f, 1.0f,
        0.72f, bottom, posZ, 0.0f, 0.0f,
        0.94f, top, posZ, 1.0f, 1.0f,
        0.94f, bottom, posZ, 1.0f, 0.0f,
      };

      GL(glUniform4f(Shader::scoreScreenUniformColor, Settings::hudScoreColor[0].r, Settings::hudScoreColor[0].g, Settings::hudScoreColor[0].b, Settings::hudScoreColor[0].a));
      GL(glUniform4f(Shader::scoreScreenUniformColor2, Settings::hudScoreColor[1].r, Settings::hudScoreColor[1].g, Settings::hudScoreColor[1].b, Settings::hudScoreColor[1].a));

      GL(glBufferData(GL_ARRAY_BUFFER, sizeof(v), v, GL_STATIC_DRAW));
      GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, 4));
    }
  }
  {
#ifdef SHR3D_FONT_BITMAP
    GL(glUseProgram(Shader::fontScreen));
    GL(glUniform4f(Shader::fontScreenUniformColor, Settings::hudScoreColor[2].r, Settings::hudScoreColor[2].g, Settings::hudScoreColor[2].b, Settings::hudScoreColor[2].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
    GL(glUseProgram(Shader::fontMSDFScreen));
    GL(glUniform4f(Shader::fontMSDFScreenUniformColor, Settings::hudScoreColor[2].r, Settings::hudScoreColor[2].g, Settings::hudScoreColor[2].b, Settings::hudScoreColor[2].a));
    GL(glUniform1f(Shader::fontMSDFScreenUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

    char8_t scoreMultiplierStr[16];
    sprintf(reinterpret_cast<char*>(scoreMultiplierStr), "x%d", scoreMultiplier);
    Font1::draw(scoreMultiplierStr, 0.88f, 0.70f, 0.0f, 0.06f, 0.06f);

#ifdef SHR3D_FONT_BITMAP
    GL(glUniform4f(Shader::fontScreenUniformColor, Settings::hudScoreColor[3].r, Settings::hudScoreColor[3].g, Settings::hudScoreColor[3].b, Settings::hudScoreColor[3].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
    GL(glUniform4f(Shader::fontMSDFScreenUniformColor, Settings::hudScoreColor[3].r, Settings::hudScoreColor[3].g, Settings::hudScoreColor[3].b, Settings::hudScoreColor[3].a));
#endif // SHR3D_FONT_MSDF

    const i32 totalScore = i32(Global::score * f32(scoreMultiplier));
    char8_t totalScoreStr[16];
    sprintf(reinterpret_cast<char*>(totalScoreStr), "%d", totalScore);
    const f32 scaleX = 2.5f * f32(Font1::textureWidth(totalScoreStr)) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 scaleY = 2.5f * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
    Font1::draw(totalScoreStr, 0.938f - scaleX, 0.58f, 0.0f, scaleX, scaleY);
  }
  GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
}

static void drawNewHighscore(const i32 resolutionWidth, const i32 resolutionHeight)
{
  static bool drawNewHighscore = false;

  if (!drawNewHighscore)
    drawNewHighscore = Global::newHighscore >= Global::oldHighscore;

  if (!drawNewHighscore)
    return;

  constexpr f32 waitCalculationDuration = 1.0f;
  constexpr f32 calcNewHighscoreDuration = 2.0f;
  constexpr f32 showNewHighscoreDuration = 1.0f;
  constexpr f32 fadeOutDuration = 1.0f;

  static f32 showTime = 0.0f;
  showTime += TimeNS_To_Seconds(Global::frameDelta);

#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontRainbowScreen));
  GL(glUniform1f(Shader::fontRainbowScreenUniformTime, showTime));
  const GLint uniformAlpha = Shader::fontRainbowScreenUniformAlpha;
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFRainbowScreen));
  GL(glUniform1f(Shader::fontMSDFRainbowScreenUniformTime, showTime));
  const GLint uniformAlpha = Shader::fontMSDFRainbowScreenUniformAlpha;
#endif // SHR3D_FONT_MSDF

  if (showTime < waitCalculationDuration)
  {
    GL(glUniform1f(uniformAlpha, 1.0f));
    const f32 scaleX = 4.5f * f32(Font1::textureWidth(u8"New Highscore!")) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 scaleY = 4.5f * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
    Font1::draw(u8"New Highscore!", 0.0f, 0.60f, 0.0f, scaleX, scaleY);
  }
  else if (showTime < waitCalculationDuration + calcNewHighscoreDuration)
  {
    GL(glUniform1f(uniformAlpha, 1.0f));
    {
      const f32 scaleX = 4.5f * f32(Font1::textureWidth(u8"New Highscore!")) * Font1::fontScaleFactor / f32(resolutionWidth);
      const f32 scaleY = 4.5f * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
      Font1::draw(u8"New Highscore!", 0.0f, 0.60f, 0.0f, scaleX, scaleY);
    }
    {
      const f32 timeDelta = fmodf((showTime - waitCalculationDuration) / calcNewHighscoreDuration, 1.0f);
      const f32 incHighscore = Global::oldHighscore + (Global::newHighscore - Global::oldHighscore) * sin(PI_2 * timeDelta);
      char8_t highscoreStr[6];
      sprintf(reinterpret_cast<char*>(highscoreStr), "%.1f", incHighscore * 100.0f);
      const f32 scaleX = 7.0f * f32(Font1::textureWidth(highscoreStr)) * Font1::fontScaleFactor / f32(resolutionWidth);
      const f32 scaleY = 7.0f * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
      Font1::draw(highscoreStr, 0.0f, 0.40f, 0.0f, scaleX, scaleY);
    }
  }
  else if (showTime < waitCalculationDuration + calcNewHighscoreDuration + showNewHighscoreDuration)
  {
    GL(glUniform1f(uniformAlpha, 1.0f));
    {
      const f32 scaleX = 4.5f * f32(Font1::textureWidth(u8"New Highscore!")) * Font1::fontScaleFactor / f32(resolutionWidth);
      const f32 scaleY = 4.5f * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
      Font1::draw(u8"New Highscore!", 0.0f, 0.60f, 0.0f, scaleX, scaleY);
    }
    {
      char8_t highscoreStr[6];
      sprintf(reinterpret_cast<char*>(highscoreStr), "%.1f", Global::newHighscore * 100.0f);
      const f32 scaleX = 7.0f * f32(Font1::textureWidth(highscoreStr)) * Font1::fontScaleFactor / f32(resolutionWidth);
      const f32 scaleY = 7.0f * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
      Font1::draw(highscoreStr, 0.0f, 0.40f, 0.0f, scaleX, scaleY);
    }
  }
  else if (showTime < waitCalculationDuration + calcNewHighscoreDuration + showNewHighscoreDuration + fadeOutDuration)
  {
    const f32 fade = showTime - (waitCalculationDuration + calcNewHighscoreDuration + showNewHighscoreDuration);
    const f32 alpha = 1.0f - (fade / fadeOutDuration);
    GL(glUniform1f(uniformAlpha, alpha));
    {
      const f32 scaleX = 4.5f * f32(Font1::textureWidth(u8"New Highscore!")) * Font1::fontScaleFactor / f32(resolutionWidth);
      const f32 scaleY = 4.5f * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
      Font1::draw(u8"New Highscore!", 0.0f, 0.60f, 0.0f, scaleX, scaleY);
    }
    {
      char8_t highscoreStr[6];
      sprintf(reinterpret_cast<char*>(highscoreStr), "%.1f", Global::newHighscore * 100.0f);
      const f32 scaleX = 7.0f * f32(Font1::textureWidth(highscoreStr)) * Font1::fontScaleFactor / f32(resolutionWidth);
      const f32 scaleY = 7.0f * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);
      Font1::draw(highscoreStr, 0.0f, 0.40f, 0.0f, scaleX, scaleY);
    }
  }
  else
  {
    drawNewHighscore = false;
  }

  GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
}

static void drawToneSwitchTimer2(const i32 resolutionWidth, const i32 resolutionHeight, const Song::Tone& tone, const f32 diff)
{
#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontScreen));
  GL(glUniform4f(Shader::fontScreenUniformColor, Settings::hudToneSwitchHintColor.r, Settings::hudToneSwitchHintColor.g, Settings::hudToneSwitchHintColor.b, Settings::hudToneSwitchHintColor.a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFScreen));
  GL(glUniform4f(Shader::fontMSDFScreenUniformColor, Settings::hudToneSwitchHintColor.r, Settings::hudToneSwitchHintColor.g, Settings::hudToneSwitchHintColor.b, Settings::hudToneSwitchHintColor.a));
  GL(glUniform1f(Shader::fontMSDFScreenUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

  const f32 scaleY = 2.0f * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);

  {
    const f32 scaleX = 2.0f * f32(Font1::textureWidth(u8"Tone 1.0 now")) * Font1::fontScaleFactor / f32(resolutionWidth);
    char8_t text[64];

    if (diff >= 0.0f)
    {
      sprintf(reinterpret_cast<char*>(text), "Tone %d %2.1f", tone.id, TimeNS_To_Seconds(tone.timeNS - Global::musicTimeElapsedNS));
    }
    else
    {
      sprintf(reinterpret_cast<char*>(text), "Tone %d now", tone.id);
    }
    Font1::draw(text, 0.0f, 0.60f, 0.0f, scaleX, scaleY);
  }

  if (tone.name.size() >= 1)
  {
    const f32 scaleX = 4.5f * f32(Font1::textureWidth(tone.name.c_str())) / f32(resolutionWidth);
    Font1::draw(tone.name.c_str(), 0.0f, 0.45f, 0.0f, scaleX, scaleY);
  }

  GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
}

static void drawToneSwitchTimer(const ArrangementIndex selectedArrangementIndex, const i32 resolutionWidth, const i32 resolutionHeight)
{
  if (i32(Global::songTracks[selectedArrangementIndex].tones.size()) != 0)
  { // songTrack.tones starts with the first tone switch. Show hint when song starts and selected tone is not 0.
    if (Global::songTracks[selectedArrangementIndex].tones[0].timeNS > Global::musicTimeElapsedNS)
    {
      for (i32 i = i32(Global::songTracks[selectedArrangementIndex].tones.size()) - 1; i >= 0; --i)
      {
        const Song::Tone& tone = Global::songTracks[selectedArrangementIndex].tones[i];

        if (tone.id == 0)
        {
          if (tone.id != Global::activeSfxToneIndex % 10)
          {
            drawToneSwitchTimer2(resolutionWidth, resolutionHeight, tone, -1.0f);
          }
          break;
        }
      }
    }
  }

  for (i32 i = i32(Global::songTracks[selectedArrangementIndex].tones.size()) - 1; i >= 0; --i)
  { // show hint for normal tone switches
    const Song::Tone& tone = Global::songTracks[selectedArrangementIndex].tones[i];

    const TimeNS diff = tone.timeNS - Global::musicTimeElapsedNS;
    if (diff < timeNS_From_Seconds(Const::hudDrawToneSwitchTimerDuration))
    {
      if (tone.id != Global::activeSfxToneIndex % 10)
      {
        drawToneSwitchTimer2(resolutionWidth, resolutionHeight, tone, TimeNS_To_Seconds(diff));
      }
      break;
    }
  }
}

static void drawUnMuteHint(const i32 resolutionWidth, const i32 resolutionHeight)
{
  static f32 timeMod = 0.0f;
  timeMod += TimeNS_To_Seconds(Global::frameDelta);
  timeMod = fmodf(timeMod, 1.0f);

  //#ifdef PLATFORM_OPENXR_ANDROID
  //  GL(glUseProgram(Shader::fontRainbowWorld));
  //
  //  GL(glUniform1f(Shader::fontRainbowWorldUniformViewDistance, Settings::highwayViewDistance));
  //  GL(glUniform1f(Shader::fontRainbowWorldUniformFadeFarDistance, Settings::highwayFadeFarDistance));
  //
  //  GL(glUniform1f(Shader::fontRainbowWorldUniformTime, timeMod));
  //  GL(glUniform1f(Shader::fontRainbowWorldUniformAlpha, 1.0f));
  //
  //  XrFrameState frameState{ XR_TYPE_FRAME_STATE };
  //  XrSpaceLocation spaceLocation{ XR_TYPE_SPACE_LOCATION };
  //  xrLocateSpace(Global::localSpace, Global::appSpace, frameState.predictedDisplayTime, &spaceLocation);
  //  mat4 rotationMatrix = vec::createMatFromQuaternion(reinterpret_cast<const vec4&>(spaceLocation.pose.orientation));
  //  mat4 translationMatrix = vec::createTranslationMat_deprecated(spaceLocation.pose.position.x, spaceLocation.pose.position.y, spaceLocation.pose.position.z);
  //  mat4 model = vec::multiply(translationMatrix, rotationMatrix);
  //  mat4 mvp = vec::multiply(Global::xrViewProjectionMat, model);
  //  GL(glUniformMatrix4fv(Shader::fontRainbowWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
  //
  //  const f32 scaleX = 0.0045f * f32(Font1::charWidth * (sizeof("Press Pause to unmute") - 1));
  //  const f32 scaleY = 0.0045f * f32(Font1::charHeight);
  //
  //  Font1::draw("Press Pause to unmute", 0.0f, 0.81f, 0.0f, scaleX, scaleY);
  //#else // PLATFORM_OPENXR_ANDROID

#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontRainbowScreen));
  GL(glUniform1f(Shader::fontRainbowScreenUniformTime, timeMod));
  GL(glUniform1f(Shader::fontRainbowScreenUniformAlpha, 1.0f));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFRainbowScreen));
  GL(glUniform1f(Shader::fontMSDFRainbowScreenUniformTime, timeMod));
  GL(glUniform1f(Shader::fontMSDFRainbowScreenUniformAlpha, 1.0f));
  GL(glUniform1f(Shader::fontMSDFRainbowScreenUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

  const f32 scaleX = 3.0f * f32(Font1::textureWidth(u8"Press Pause to unmute")) * Font1::fontScaleFactor / f32(resolutionWidth);
  const f32 scaleY = 3.0f * f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);

  Font1::draw(u8"Press Pause to unmute", 0.0f, 0.81f, 0.0f, scaleX, scaleY);
  //#endif // PLATFORM_OPENXR_ANDROID

  GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
}

static void drawDebugInfo(const i32 resolutionWidth, const i32 resolutionHeight)
{
  char8_t text[256];

#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontScreen));
  GL(glUniform4f(Shader::fontScreenUniformColor, 1.0f, 1.0f, 1.0f, 1.0f));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFScreen));
  GL(glUniform4f(Shader::fontMSDFScreenUniformColor, 1.0f, 1.0f, 1.0f, 1.0f));
  GL(glUniform1f(Shader::fontMSDFScreenUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

  const f32 scaleY = f32(Font1::charHeight) * Font1::fontScaleFactor / f32(resolutionHeight);

  const f32 rowHeight = 2.0f * Font1::fontScaleFactor;
  f32 rowOffsetAccum;

  {
    sprintf(reinterpret_cast<char*>(text), "Framerate %.0f", 1.0f / TimeNS_To_Seconds(Global::frameDelta));
    const f32 scaleX = f32(Font1::textureWidth(text)) * Font1::fontScaleFactor / f32(resolutionWidth);
    Font1::draw(text, scaleX - 1.00f, 1.00f - scaleY, 0.0f, scaleX, scaleY);
    rowOffsetAccum = rowHeight;
  }

  {
    sprintf(reinterpret_cast<char*>(text), "Cursor X %d Y %d", Global::inputCursorPosX, Global::inputCursorPosY);
    const f32 scaleX = f32(Font1::textureWidth(text)) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 offsetY = rowOffsetAccum * f32(Font1::charHeight) / f32(resolutionHeight);
    Font1::draw(text, scaleX - 1.00f, 1.00f - scaleY - offsetY, 0.0f, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }

  //  {
  //#ifndef SHR3D_OPENXR
  //    sprintf(reinterpret_cast<char*>(text), "Camera %.2f %.2f %.2f", Global::viewProjectionMat.m03, Global::viewProjectionMat.m13, Global::viewProjectionMat.m23);
  //#else // SHR3D_OPENXR
  //    sprintf(reinterpret_cast<char*>(text), "Camera %.2f %.2f %.2f", Global::viewMat.m03, Global::viewMat.m13, Global::viewMat.m23);
  //#endif // SHR3D_OPENXR
  //    const f32 scaleX = f32(Font1::textureWidth(text)) * Font1::fontScaleFactor / f32(resolutionWidth);
  //    const f32 offsetY = rowOffsetAccum * f32(Font1::charHeight) / f32(resolutionHeight);
  //    Font1::draw(text, scaleX - 1.00f, 1.00f - scaleY - offsetY, 0.0f, scaleX, scaleY);
  //    rowOffsetAccum += rowHeight;
  //  }

  {
    sprintf(reinterpret_cast<char*>(text), "Music %d.%03d %d", i32(Global::musicTimeElapsedNS / 1_s), i32((Global::musicTimeElapsedNS / 1_ms)) % 1000, i32(Global::musicPlaybackPosition));
    const f32 scaleX = f32(Font1::textureWidth(text)) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 offsetY = rowOffsetAccum * f32(Font1::charHeight) / f32(resolutionHeight);
    Font1::draw(text, scaleX - 1.00f, 1.00f - scaleY - offsetY, 0.0f, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }

  {
    sprintf(reinterpret_cast<char*>(text), "Tone %d+%d", Global::activeSfxToneIndex, Global::sfxToneAutoSwitchOffset);
    const f32 scaleX = f32(Font1::textureWidth(text)) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 offsetY = rowOffsetAccum * f32(Font1::charHeight) / f32(resolutionHeight);
    Font1::draw(text, scaleX - 1.00f, 1.00f - scaleY - offsetY, 0.0f, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }

#ifdef SHR3D_RECORDER
  if (Global::recorderBuffer.size() > 0)
  {
    sprintf(reinterpret_cast<char*>(text), "Recorder %d", i32(Global::recorderBuffer.size() / 1000));
    const f32 scaleX = f32(Font1::textureWidth(text)) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 offsetY = rowOffsetAccum * f32(Font1::charHeight) / f32(resolutionHeight);
    Font1::draw(text, scaleX - 1.00f, 1.00f - scaleY - offsetY, 0.0f, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }
#endif // SHR3D_RECORDER

  if (Global::selectedSongIndex >= 0)
  {
    sprintf(reinterpret_cast<char*>(text), "Song %s", reinterpret_cast<const char*>(Global::songFilePath[Global::selectedSongIndex].c_str()));
    const f32 scaleX = f32(Font1::textureWidth(text)) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 offsetY = rowOffsetAccum * f32(Font1::charHeight) / f32(resolutionHeight);
    Font1::draw(text, scaleX - 1.00f, 1.00f - scaleY - offsetY, 0.0f, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }

#ifdef SHR3D_SPOTIFY
  if (Global::songInfos[Global::selectedSongIndex].spotifyTrackId[0] != '\0')
  {
    sprintf(reinterpret_cast<char*>(text), "SpotifyTackId %s", Global::songInfos[Global::selectedSongIndex].spotifyTrackId);
    const f32 scaleX = f32(Font1::textureWidth(text)) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 offsetY = rowOffsetAccum * f32(Font1::charHeight) / f32(resolutionHeight);
    Font1::draw(text, scaleX - 1.00f, 1.00f - scaleY - offsetY, 0.0f, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }
#endif // SHR3D_SPOTIFY

#ifdef SHR3D_ENVIRONMENT_MILK
  if (Settings::environmentMilk && Global::milkCurrentPresetIndex >= 0)
  {
    sprintf(reinterpret_cast<char*>(text), "Milk %s", reinterpret_cast<const char*>(Global::milkPresetNames[Global::milkCurrentPresetIndex].c_str()));
    const f32 scaleX = f32(Font1::textureWidth(text)) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 offsetY = rowOffsetAccum * f32(Font1::charHeight) / f32(resolutionHeight);
    Font1::draw(text, scaleX - 1.00f, 1.00f - scaleY - offsetY, 0.0f, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }
#endif // SHR3D_ENVIRONMENT_MILK

#ifdef SHR3D_AUDIO_ASIO
  if (Settings::audioSystem == AudioSystem::ASIO)
  {
    sprintf(reinterpret_cast<char*>(text), "ASIO In: %d (%.2fms) Out: %d (%.2fms)", Global::audioAsioInputLatencyFrames, Global::audioAsioInputLatencyFrames * (1000.0f / sampleRate()), Global::audioAsioOutputLatencyFrames, Global::audioAsioOutputLatencyFrames * (1000.0f / sampleRate()));
    const f32 scaleX = f32(Font1::textureWidth(text)) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 offsetY = rowOffsetAccum * f32(Font1::charHeight) / f32(resolutionHeight);
    Font1::draw(text, scaleX - 1.00f, 1.00f - scaleY - offsetY, 0.0f, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }
#endif // SHR3D_AUDIO_ASIO

  //#ifdef SHR3D_AUDIO_WASAPI
  //  if (Settings::audioSystem == AudioSystem::WASAPI)
  //  {
  //    sprintf(reinterpret_cast<char*>(text), "WASAPI In: %d (%.2fms) Out: %d (%.2fms)", Global::audioWasapiInputLatencyFrames, Global::audioWasapiInputLatencyFrames * (1000.0f / sampleRate()), Global::audioWasapiOutputLatencyFrames, Global::audioWasapiOutputLatencyFrames * (1000.0f / sampleRate()));
  //    const f32 scaleX = f32(Font1::textureWidth(text)) * Font1::fontScaleFactor / f32(resolutionWidth);
  //    const f32 offsetY = rowOffsetAccum * f32(Font1::charHeight) / f32(resolutionHeight);
  //    Font1::draw(text, scaleX - 1.00f, 1.00f - scaleY - offsetY, 0.0f, scaleX, scaleY);
  //  }
  //#endif // SHR3D_AUDIO_WASAPI

#ifdef SHR3D_BENCHMARK_FPS
  {
    sprintf(reinterpret_cast<char*>(text), "Benchmark FPS: %.2f Frames: %llu %llu", Global::benchmarkAvgFPS, Global::benchmarkScoreLast, Global::benchmarkScore);
    const f32 scaleX = f32(Font1::textureWidth(text)) * Font1::fontScaleFactor / f32(resolutionWidth);
    const f32 offsetY = rowOffsetAccum * f32(Font1::charHeight) / f32(resolutionHeight);
    Font1::draw(text, scaleX - 1.00f, 1.00f - scaleY - offsetY, 0.0f, scaleX, scaleY);
    //rowOffsetAccum += rowHeight;
  }
#endif // SHR3D_BENCHMARK_FPS

  GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
}

static void drawWatermark(const i32 resolutionWidth, const i32 resolutionHeight)
{
#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontScreen));
  GL(glUniform4f(Shader::fontScreenUniformColor, Settings::hudWatermarkColor.r, Settings::hudWatermarkColor.g, Settings::hudWatermarkColor.b, Settings::hudWatermarkColor.a));
  const i32 width = Font1::charWidth * sizeof("Shr3D v" VERSION_STR) - 1;
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFScreen));
  GL(glUniform4f(Shader::fontMSDFScreenUniformColor, Settings::hudWatermarkColor.r, Settings::hudWatermarkColor.g, Settings::hudWatermarkColor.b, Settings::hudWatermarkColor.a));
  GL(glUniform1f(Shader::fontMSDFScreenUniformScreenPxRange, Const::fontMsdfScreenPxRange));
  const i32 width = Font1::textureWidth(u8"Shr3D v" VERSION_STR);
#endif // SHR3D_FONT_MSDF
  const f32 scaleX = 1.0f * width * Font1::fontScaleFactor / f32(resolutionWidth);
  const f32 scaleY = 1.0f * Font1::charHeight * Font1::fontScaleFactor / f32(resolutionHeight);
  Font1::draw(u8"Shr3D v" VERSION_STR, 0.99f - scaleX, -0.99f + scaleY, 0.0f, scaleX, scaleY);

  GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
}

void Hud::render(const Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const i32 resolutionWidth, const i32 resolutionHeight)
{
  if (Global::inputDebugInfo.toggled)
    drawDebugInfo(resolutionWidth, resolutionHeight);

  if (Settings::hudArrangementSwitch)
    drawArrangementSwitch(ctx.arrangementSwitchTime, selectedArrangementIndex, resolutionWidth, resolutionHeight);

  if (Settings::hudToneSwitch)
    drawSfxTone(resolutionWidth, resolutionHeight);

  if (Settings::hudWatermark)
    drawWatermark(resolutionWidth, resolutionHeight);

  if (Global::selectedSongIndex >= 0)
  {
    if (Settings::hudToneSwitchTimer)
      drawToneSwitchTimer(selectedArrangementIndex, resolutionWidth, resolutionHeight);

    drawSections(selectedArrangementIndex);
    drawQuickRepeater();

    if (Global::songInfos[Global::selectedSongIndex].loadState == Song::LoadState::complete && Settings::hudSongInfo)
      drawSongInfo(resolutionWidth, resolutionHeight);

    if (Settings::hudLyrics)
      drawLyrics(resolutionWidth, resolutionHeight);

    if (Settings::hudScore)
      drawScore(resolutionWidth, resolutionHeight);

    if (Settings::hudNewHighscore && Global::oldHighscore > 0.0f)
      drawNewHighscore(resolutionWidth, resolutionHeight);

    if (Global::inputMute.toggled)
      drawUnMuteHint(resolutionWidth, resolutionHeight);
  }
}






#ifdef SHR3D_OPENXR
static void drawDebugInfoXr(const mat4& viewProjectionMat)
{
  char8_t text[256];

  GL(glUseProgram(Shader::fontMSDFWorld));
  GL(glUniform4f(Shader::fontMSDFWorldUniformColor, 1.0f, 1.0f, 1.0f, 1.0f));
  GL(glUniform1f(Shader::fontMSDFWorldUniformScreenPxRange, Const::fontMsdfScreenPxRange));
  GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &viewProjectionMat.m00));

  const f32 scaleY = Settings::hudDebugXrScale * f32(Font1::charHeight) * Font1::fontScaleFactor;

  const f32 rowHeight = 2.0f * Font1::fontScaleFactor;
  f32 rowOffsetAccum;

  {
    sprintf(reinterpret_cast<char*>(text), "Framerate %.0f", 1.0f / TimeNS_To_Seconds(Global::frameDelta));
    const f32 scaleX = Settings::hudDebugXrScale * f32(Font1::textureWidth(text)) * Font1::fontScaleFactor;
    Font1::draw(text, Settings::hudDebugXrX + scaleX - 1.00f, Settings::hudDebugXrY + 1.00f - scaleY, Settings::hudDebugXrZ, scaleX, scaleY);
    rowOffsetAccum = rowHeight;
  }

  {
    sprintf(reinterpret_cast<char*>(text), "Cursor X %d Y %d", Global::inputCursorPosX, Global::inputCursorPosY);
    const f32 scaleX = Settings::hudDebugXrScale * f32(Font1::textureWidth(text)) * Font1::fontScaleFactor;
    const f32 offsetY = Settings::hudDebugXrScale * rowOffsetAccum * f32(Font1::charHeight);
    Font1::draw(text, Settings::hudDebugXrX + scaleX - 1.00f, Settings::hudDebugXrY + 1.00f - scaleY - offsetY, Settings::hudDebugXrZ, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }

#ifdef PLATFORM_OPENXR_ANDROID
  {
    sprintf(reinterpret_cast<char*>(text), "Thumbstick L X %.2f Y %.2f R X %.2f Y %.2f", Global::inputXRControllerEvent[0].thumbstick_x, Global::inputXRControllerEvent[0].thumbstick_y, Global::inputXRControllerEvent[1].thumbstick_x, Global::inputXRControllerEvent[1].thumbstick_y);
    const f32 scaleX = Settings::hudDebugXrScale * f32(Font1::textureWidth(text)) * Font1::fontScaleFactor;
    const f32 offsetY = Settings::hudDebugXrScale * rowOffsetAccum * f32(Font1::charHeight);
    Font1::draw(text, Settings::hudDebugXrX + scaleX - 1.00f, Settings::hudDebugXrY + 1.00f - scaleY - offsetY, Settings::hudDebugXrZ, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }
#endif // PLATFORM_OPENXR_ANDROID

#ifdef SHR3D_OPENXR_PCVR
  if (Global::xrInitialized)
  {
    sprintf(reinterpret_cast<char*>(text), "CursorXr X %d Y %d", Global::inputCursorPosXrX, Global::inputCursorPosXrY);
    const f32 scaleX = Settings::hudDebugXrScale * f32(Font1::textureWidth(text)) * Font1::fontScaleFactor;
    const f32 offsetY = Settings::hudDebugXrScale * rowOffsetAccum * f32(Font1::charHeight);
    Font1::draw(text, Settings::hudDebugXrX + scaleX - 1.00f, Settings::hudDebugXrY + 1.00f - scaleY - offsetY, Settings::hudDebugXrZ, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }
#endif // SHR3D_OPENXR_PCVR

  //  {
  //#ifndef SHR3D_OPENXR
  //    sprintf(reinterpret_cast<char*>(text), "Camera %.2f %.2f %.2f", Global::viewProjectionMat.m03, Global::viewProjectionMat.m13, Global::viewProjectionMat.m23);
  //#else // SHR3D_OPENXR
  //    sprintf(reinterpret_cast<char*>(text), "Camera %.2f %.2f %.2f", Global::viewMat.m03, Global::viewMat.m13, Global::viewMat.m23);
  //#endif // SHR3D_OPENXR
  //    const f32 scaleX = f32(Font1::textureWidth(text)) * Font1::fontScaleFactor / f32(resolutionWidth);
  //    const f32 offsetY = rowOffsetAccum * f32(Font1::charHeight) / f32(resolutionHeight);
  //    Font1::draw(text, Settings::hudDebugXrX + scaleX - 1.00f, Settings::hudDebugXrY + 1.00f - scaleY - offsetY, Settings::hudDebugXrZ, scaleX, scaleY);
  //    rowOffsetAccum += rowHeight;
  //  }

  {
    sprintf(reinterpret_cast<char*>(text), "Time %d.%03d", i32(Global::musicTimeElapsedNS / 1_s), i32((Global::musicTimeElapsedNS / 1_ms)) % 1000);
    const f32 scaleX = Settings::hudDebugXrScale * f32(Font1::textureWidth(text)) * Font1::fontScaleFactor;
    const f32 offsetY = Settings::hudDebugXrScale * rowOffsetAccum * f32(Font1::charHeight);
    Font1::draw(text, Settings::hudDebugXrX + scaleX - 1.00f, Settings::hudDebugXrY + 1.00f - scaleY - offsetY, Settings::hudDebugXrZ, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }

  {
    sprintf(reinterpret_cast<char*>(text), "Tone %d+%d", Global::activeSfxToneIndex, Global::sfxToneAutoSwitchOffset);
    const f32 scaleX = Settings::hudDebugXrScale * f32(Font1::textureWidth(text)) * Font1::fontScaleFactor;
    const f32 offsetY = Settings::hudDebugXrScale * rowOffsetAccum * f32(Font1::charHeight);
    Font1::draw(text, Settings::hudDebugXrX + scaleX - 1.00f, Settings::hudDebugXrY + 1.00f - scaleY - offsetY, Settings::hudDebugXrZ, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }

#ifdef SHR3D_RECORDER
  if (Global::recorderBuffer.size() > 0)
  {
    sprintf(reinterpret_cast<char*>(text), "Recorder %d", i32(Global::recorderBuffer.size() / 1000));
    const f32 scaleX = Settings::hudDebugXrScale * f32(Font1::textureWidth(text)) * Font1::fontScaleFactor;
    const f32 offsetY = Settings::hudDebugXrScale * rowOffsetAccum * f32(Font1::charHeight);
    Font1::draw(text, Settings::hudDebugXrX + scaleX - 1.00f, Settings::hudDebugXrY + 1.00f - scaleY - offsetY, Settings::hudDebugXrZ, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }
#endif // SHR3D_RECORDER

  if (Global::selectedSongIndex >= 0)
  {
    sprintf(reinterpret_cast<char*>(text), "Song %s", reinterpret_cast<const char*>(Global::songFilePath[Global::selectedSongIndex].c_str()));
    const f32 scaleX = Settings::hudDebugXrScale * f32(Font1::textureWidth(text)) * Font1::fontScaleFactor;
    const f32 offsetY = Settings::hudDebugXrScale * rowOffsetAccum * f32(Font1::charHeight);
    Font1::draw(text, Settings::hudDebugXrX + scaleX - 1.00f, Settings::hudDebugXrY + 1.00f - scaleY - offsetY, Settings::hudDebugXrZ, scaleX, scaleY);
    rowOffsetAccum += rowHeight;
  }

#ifdef SHR3D_SPOTIFY
  if (Global::songInfos[Global::selectedSongIndex].spotifyTrackId[0] != '\0')
  {
    sprintf(reinterpret_cast<char*>(text), "SpotifyTackId %s", Global::songInfos[Global::selectedSongIndex].spotifyTrackId);
    const f32 scaleX = Settings::hudDebugXrScale * f32(Font1::textureWidth(text)) * Font1::fontScaleFactor;
    const f32 offsetY = Settings::hudDebugXrScale * rowOffsetAccum * f32(Font1::charHeight);
    Font1::draw(text, Settings::hudDebugXrX + scaleX - 1.00f, Settings::hudDebugXrY + 1.00f - scaleY - offsetY, Settings::hudDebugXrZ, scaleX, scaleY);
  }
#endif // SHR3D_SPOTIFY

#ifdef SHR3D_ENVIRONMENT_MILK
  if (Settings::environmentMilk && Global::milkCurrentPresetIndex >= 0)
  {
    sprintf(reinterpret_cast<char*>(text), "Milk %s", reinterpret_cast<const char*>(Global::milkPresetNames[Global::milkCurrentPresetIndex].c_str()));
    const f32 scaleX = Settings::hudDebugXrScale * f32(Font1::textureWidth(text)) * Font1::fontScaleFactor;
    const f32 offsetY = Settings::hudDebugXrScale * rowOffsetAccum * f32(Font1::charHeight);
    Font1::draw(text, Settings::hudDebugXrX + scaleX - 1.00f, Settings::hudDebugXrY + 1.00f - scaleY - offsetY, Settings::hudDebugXrZ, scaleX, scaleY);
  }
#endif // SHR3D_ENVIRONMENT_MILK

  GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
}

static void drawArrangementSwitchXr(const TimeNS arrangementSwitchTime, const ArrangementIndex selectedArrangementIndex, const mat4& viewProjectionMat)
{
  if (Global::selectedSongIndex >= 0 && selectedArrangementIndex >= 0)
  {
    const f32 timeElapsed = TimeNS_To_Seconds(Global::time_ - arrangementSwitchTime);

    if (Const::hudDrawArrangementSwitchEndDuration < timeElapsed)
      return;

    f32 alpha = 1.0f;
    if (Const::hudDrawArrangementSwitchFadeInDuration > timeElapsed)
      alpha = timeElapsed / (Const::hudDrawArrangementSwitchFadeInDuration);
    else if (Const::hudDrawArrangementSwitchFadeOutDuration < timeElapsed)
      alpha = (timeElapsed - Const::hudDrawArrangementSwitchEndDuration) / (Const::hudDrawArrangementSwitchFadeOutDuration - Const::hudDrawArrangementSwitchEndDuration);

    GL(glUseProgram(Shader::fontMSDFWorld));
    GL(glUniform4f(Shader::fontMSDFWorldUniformColor, Settings::hudArrangementSwitchColor.r, Settings::hudArrangementSwitchColor.g, Settings::hudArrangementSwitchColor.b, Settings::hudArrangementSwitchColor.a * alpha));
    GL(glUniform1f(Shader::fontMSDFWorldUniformScreenPxRange, Const::fontMsdfScreenPxRange));
    GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &viewProjectionMat.m00));

    {
      const f32 scaleX = Settings::hudArrangementSwitchXrScaleX * f32(Font1::textureWidth(Global::songInfos[Global::selectedSongIndex].arrangementInfos[selectedArrangementIndex].arrangementName.c_str())) * Font1::fontScaleFactor;
      const f32 scaleY = Settings::hudArrangementSwitchXrScaleY * f32(Font1::charHeight) * Font1::fontScaleFactor;

      Font1::draw(Global::songInfos[Global::selectedSongIndex].arrangementInfos[selectedArrangementIndex].arrangementName.c_str(), Settings::hudArrangementSwitchXrX, Settings::hudArrangementSwitchXrY, Settings::hudArrangementSwitchXrZ, scaleX, scaleY);
    }

    GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
  }
}

static void drawSfxToneXr(const mat4& viewProjectionMat)
{
  const f32 timeElapsed = TimeNS_To_Seconds(Global::time_ - Global::sfxToneTime);

  if (Const::hudDrawSfxToneEndDuration < timeElapsed)
    return;

  f32 alpha = 1.0f;
  if (Const::hudDrawSfxToneFadeInDuration > timeElapsed)
    alpha = timeElapsed / (Const::hudDrawSfxToneFadeInDuration);
  else if (Const::hudDrawSfxToneFadeOutDuration < timeElapsed)
    alpha = (timeElapsed - Const::hudDrawSfxToneEndDuration) / (Const::hudDrawSfxToneFadeOutDuration - Const::hudDrawSfxToneEndDuration);

  GL(glUseProgram(Shader::fontMSDFWorld));
  GL(glUniform1f(Shader::fontMSDFWorldUniformScreenPxRange, Const::fontMsdfScreenPxRange));
  GL(glUniform4f(Shader::fontMSDFWorldUniformColor, Settings::hudToneSwitchColor.r, Settings::hudToneSwitchColor.g, Settings::hudToneSwitchColor.b, Settings::hudToneSwitchColor.a));
  GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &viewProjectionMat.m00));

  {
    char8_t text[64];
    sprintf(reinterpret_cast<char*>(text), "Tone Switch [B%2d|T%2d+%d]", (Global::activeSfxToneIndex + (Global::activeSfxToneIndex >= 0 ? Global::sfxToneAutoSwitchOffset : -Global::sfxToneAutoSwitchOffset)) / Const::sfxToneTonesPerBank, (abs(Global::activeSfxToneIndex)) % Const::sfxToneTonesPerBank, Global::sfxToneAutoSwitchOffset);
    const f32 scaleX = 0.01f * Settings::hudToneSwitchXrScale[0] * f32(Font1::textureWidth(text)) * Font1::fontScaleFactor;
    const f32 scaleY = 0.01f * Settings::hudToneSwitchXrScale[0] * f32(Font1::charHeight) * Font1::fontScaleFactor;
    Font1::draw(text, Settings::hudToneSwitchXrX - scaleX, Settings::hudToneSwitchXrY[0], Settings::hudToneSwitchXrZ, scaleX, scaleY);
  }
#ifdef SHR3D_SFX
  {
    const std::u8string& currentToneName = Global::sfxToneNames[Global::activeSfxToneIndex + (Global::activeSfxToneIndex >= 0 ? Global::sfxToneAutoSwitchOffset : -Global::sfxToneAutoSwitchOffset)];
    if (currentToneName.size() != 0)
    {
      const f32 scaleX = 0.01f * Settings::hudToneSwitchXrScale[1] * f32(Font1::textureWidth(currentToneName.c_str())) * Font1::fontScaleFactor;
      const f32 scaleY = 0.01f * Settings::hudToneSwitchXrScale[1] * f32(Font1::charHeight) * Font1::fontScaleFactor;
      Font1::draw(currentToneName.c_str(), Settings::hudToneSwitchXrX - scaleX, Settings::hudToneSwitchXrY[1], Settings::hudToneSwitchXrZ, scaleX, scaleY);
    }
  }
#endif // SHR3D_SFX
  GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
}

static void drawWatermarkXr(const mat4& viewProjectionMat)
{
  GL(glUseProgram(Shader::fontMSDFWorld));
  GL(glUniform1f(Shader::fontMSDFWorldUniformScreenPxRange, Const::fontMsdfScreenPxRange));
  GL(glUniform4f(Shader::fontMSDFWorldUniformColor, Settings::hudWatermarkColor.r, Settings::hudWatermarkColor.g, Settings::hudWatermarkColor.b, Settings::hudWatermarkColor.a));

  GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &viewProjectionMat.m00));

  const f32 scaleX = Settings::hudDebugXrScale * Font1::textureWidth(u8"Shr3D v" VERSION_STR) * Font1::fontScaleFactor;
  const f32 scaleY = Settings::hudDebugXrScale * f32(Font1::charHeight);
  Font1::draw(u8"Shr3D v" VERSION_STR, Settings::hudWatermarkXrX, Settings::hudWatermarkXrY, Settings::hudWatermarkXrZ, scaleX, scaleY);
}

static void drawSectionsXr(const ArrangementIndex selectedArrangementIndex, const mat4& viewProjectionMat)
{
  if (Settings::hudTimelineTone)
  {
    GL(glUseProgram(Shader::timelineToneWorld));

    const f32 left = Settings::hudTimelineToneXrX + 2.0f * -0.5f * Settings::hudTimelineToneXrScaleX;
    const f32 right = Settings::hudTimelineToneXrX + 2.0f * 0.5f * Settings::hudTimelineToneXrScaleX;
    const f32 top = Settings::hudTimelineToneXrY + Settings::hudTimelineToneXrScaleY;
    const f32 bottom = Settings::hudTimelineToneXrY;

    const mat4 model = {
      .m00 = 0.5f * (right - left),
      .m11 = 0.5f * (top - bottom),
      .m03 = left + 0.5f * (right - left),
      .m13 = bottom + 0.5f * (top - bottom),
      .m23 = Settings::hudTimelineToneXrZ
    };
    const mat4 mvp = vec::multiply(viewProjectionMat, model);
    GL(glUniformMatrix4fv(Shader::timelineToneWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

    const f32 progress = f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::musicTimeElapsedNS) / f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd);
    GL(glUniform1f(Shader::timelineToneWorldUniformProgress, progress));

    f32 tonePos[100];
    vec4 toneColor[102];
    ASSERT(Global::songTracks[selectedArrangementIndex].tones.size() < ARRAY_SIZE(tonePos));
    toneColor[0] = Settings::hudTimelineToneColor[0];
    toneColor[1] = Settings::hudTimelineToneColor[1];
    {
      i32 i = 0;
      for (; i < Global::songTracks[selectedArrangementIndex].tones.size(); ++i)
      {
        ASSERT(Global::songTracks[selectedArrangementIndex].tones[i].id >= 0);
        ASSERT(Global::songTracks[selectedArrangementIndex].tones[i].id <= 9);
        tonePos[i] = f32(Global::songTracks[selectedArrangementIndex].tones[i].timeNS) / f32(Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd);
        toneColor[i + 2] = Settings::hudTimelineToneColor[Global::songTracks[selectedArrangementIndex].tones[i].id + 1];
      }
      for (; i < ARRAY_SIZE(tonePos); ++i)
      {
        tonePos[i] = 1.0f;
      }
    }

    GL(glUniform1fv(Shader::timelineToneWorldUniformTonePos, ARRAY_SIZE(tonePos), tonePos));
    GL(glUniform4fv(Shader::timelineToneWorldUniformToneColor, ARRAY_SIZE(toneColor), &toneColor[0].r));

    GL(glDrawElementsBaseVertex_compat(planeZ));

  }
  if (Settings::hudTimelineLevel)
  {
    i32 overallMaxLevel = 0;
    for (const Song::LeveledSection& leveledSections : Global::songTracks[selectedArrangementIndex].leveledSections)
      if (leveledSections.maxLevel > overallMaxLevel)
        overallMaxLevel = leveledSections.maxLevel;

    for (i32 i = 0; i < i32(Global::songTracks[selectedArrangementIndex].leveledSections.size()); ++i)
    {
      const Song::LeveledSection& leveledSections = Global::songTracks[selectedArrangementIndex].leveledSections[i];

      f32 begin = TimeNS_To_Seconds(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + leveledSections.startTimeNS) / TimeNS_To_Seconds(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd);
      f32 end = TimeNS_To_Seconds(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + leveledSections.endTimeNS) / TimeNS_To_Seconds(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd);
      f32 level = f32(leveledSections.maxLevel) / f32(overallMaxLevel);

      const f32 left = Settings::hudTimelineLevelXrX + 2.0f * (begin - 0.5f) * Settings::hudTimelineLevelXrScaleX + Settings::hudTimelineLevelXrSpacing * 0.01f;
      const f32 right = Settings::hudTimelineLevelXrX + 2.0f * (end - 0.5f) * Settings::hudTimelineLevelXrScaleX - Settings::hudTimelineLevelXrSpacing * 0.01f;

      f32 top;
      f32 bottom;
      if (Settings::hudTimelineLevelXrFlipY)
      {
        top = Settings::hudTimelineLevelXrY - Settings::hudTimelineLevelXrScaleY + (level * Settings::hudTimelineLevelXrScaleY);
        bottom = Settings::hudTimelineLevelXrY - Settings::hudTimelineLevelXrScaleY;
      }
      else
      {
        top = Settings::hudTimelineLevelXrY;
        bottom = Settings::hudTimelineLevelXrY - (level * Settings::hudTimelineLevelXrScaleY);
      }

      GL(glUseProgram(Shader::timelineLevelWorld));

      const mat4 model = {
        .m00 = 0.5f * (right - left),
        .m11 = 0.5f * (top - bottom),
        .m03 = left + 0.5f * (right - left),
        .m13 = bottom + 0.5f * (top - bottom),
        .m23 = Settings::hudTimelineLevelXrZ
      };
      const mat4 mvp = vec::multiply(viewProjectionMat, model);
      GL(glUniformMatrix4fv(Shader::timelineLevelWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));


      const f32 progress = map_(Global::musicTimeElapsedNS, leveledSections.startTimeNS, leveledSections.endTimeNS, 0.0f, 1.0f);
      GL(glUniform1f(Shader::timelineLevelWorldUniformProgress, progress));

      const f32 selectedLevel = f32(Global::songLevels[selectedArrangementIndex].sectionLevels[i]) / f32(leveledSections.maxLevel);
      GL(glUniform1f(Shader::timelineLevelWorldUniformSelectedLevel, selectedLevel));

      GL(glUniform4f(Shader::timelineLevelWorldUniformColor, Settings::hudTimelineLevelColor[0].r, Settings::hudTimelineLevelColor[0].g, Settings::hudTimelineLevelColor[0].b, Settings::hudTimelineLevelColor[0].a));
      GL(glUniform4f(Shader::timelineLevelWorldUniformColor2, Settings::hudTimelineLevelColor[1].r, Settings::hudTimelineLevelColor[1].g, Settings::hudTimelineLevelColor[1].b, Settings::hudTimelineLevelColor[1].a));
      GL(glUniform4f(Shader::timelineLevelWorldUniformColor3, Settings::hudTimelineLevelColor[2].r, Settings::hudTimelineLevelColor[2].g, Settings::hudTimelineLevelColor[2].b, Settings::hudTimelineLevelColor[2].a));
      GL(glUniform4f(Shader::timelineLevelWorldUniformColor4, Settings::hudTimelineLevelColor[3].r, Settings::hudTimelineLevelColor[3].g, Settings::hudTimelineLevelColor[3].b, Settings::hudTimelineLevelColor[3].a));
      GL(glUniform4f(Shader::timelineLevelWorldUniformColor5, Settings::hudTimelineLevelColor[4].r, Settings::hudTimelineLevelColor[4].g, Settings::hudTimelineLevelColor[4].b, Settings::hudTimelineLevelColor[4].a));
      GL(glUniform4f(Shader::timelineLevelWorldUniformColor6, Settings::hudTimelineLevelColor[5].r, Settings::hudTimelineLevelColor[5].g, Settings::hudTimelineLevelColor[5].b, Settings::hudTimelineLevelColor[5].a));
      GL(glUniform4f(Shader::timelineLevelWorldUniformColor7, Settings::hudTimelineLevelColor[6].r, Settings::hudTimelineLevelColor[6].g, Settings::hudTimelineLevelColor[6].b, Settings::hudTimelineLevelColor[6].a));
      GL(glUniform4f(Shader::timelineLevelWorldUniformColor8, Settings::hudTimelineLevelColor[7].r, Settings::hudTimelineLevelColor[7].g, Settings::hudTimelineLevelColor[7].b, Settings::hudTimelineLevelColor[7].a));

      GL(glDrawElementsBaseVertex_compat(planeZ));
    }
  }
#ifdef SHR3D_MUSIC_STRETCHER

  if (Settings::hudTimelineMusicStretcher && Global::musicPlaybackBufferLR[0] != Global::musicDoubleBuffer[to_underlying_(Global::musicDoubleBufferStatus) % 2])
  {
    const f32 left = Settings::hudTimelineMusicStretcherXrX + 2.0f * -0.5f * Settings::hudTimelineMusicStretcherXrScaleX;
    const f32 right = Settings::hudTimelineMusicStretcherXrX + 2.0f * 0.5f * Settings::hudTimelineMusicStretcherXrScaleX;
    const f32 top = Settings::hudTimelineMusicStretcherXrY + Settings::hudTimelineMusicStretcherXrScaleY;
    const f32 bottom = Settings::hudTimelineMusicStretcherXrY;

    GL(glUseProgram(Shader::timelineMusicStretcherWorld));

    const mat4 model = {
      .m00 = 0.5f * (right - left),
      .m11 = 0.5f * (top - bottom),
      .m03 = left + 0.5f * (right - left),
      .m13 = bottom + 0.5f * (top - bottom),
      .m23 = Settings::hudTimelineMusicStretcherXrZ
    };
    const mat4 mvp = vec::multiply(viewProjectionMat, model);
    GL(glUniformMatrix4fv(Shader::timelineMusicStretcherWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

    const f32 progress0 = f32(Global::musicStretcherInputCurrent) / f32(Global::musicBufferLength);
    GL(glUniform1f(Shader::timelineMusicStretcherWorldUniformProgress0, progress0));

    const f32 progress1 = f32(Global::musicStretcherInputBegin) / f32(Global::musicBufferLength);
    GL(glUniform1f(Shader::timelineMusicStretcherWorldUniformProgress1, progress1));

    GL(glUniform4f(Shader::timelineMusicStretcherWorldUniformColor, Settings::hudTimelineMusicStretcherColor[0].r, Settings::hudTimelineMusicStretcherColor[0].g, Settings::hudTimelineMusicStretcherColor[0].b, Settings::hudTimelineMusicStretcherColor[0].a));
    GL(glUniform4f(Shader::timelineMusicStretcherWorldUniformColor2, Settings::hudTimelineMusicStretcherColor[1].r, Settings::hudTimelineMusicStretcherColor[1].g, Settings::hudTimelineMusicStretcherColor[1].b, Settings::hudTimelineMusicStretcherColor[1].a));

    GL(glDrawElementsBaseVertex_compat(planeZ));
  }
#endif // SHR3D_MUSIC_STRETCHER

}

static void drawQuickRepeaterXr(const mat4& viewProjectionMat)
{
  if (Global::quickRepeaterBeginTimeNS == I64::max)
    return;

  const f32 begin = f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::quickRepeaterBeginTimeNS) / f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd);
  const f32 end = Global::quickRepeaterEndTimeNS == I64::max
                  ? f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::musicTimeElapsedNS) / f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd)
                  : f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::quickRepeaterEndTimeNS) / f32(Global::songInfos[Global::selectedSongIndex].shredDelayBegin + Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd);

  GL(glUseProgram(Shader::timelineSimpleWorld));

  const f32 progress = Global::quickRepeaterEndTimeNS == I64::max
                       ? 1.0f
                       : map_(Global::musicTimeElapsedNS, Global::quickRepeaterBeginTimeNS, Global::quickRepeaterEndTimeNS, 0.0f, 1.0f);
  GL(glUniform1f(Shader::timelineSimpleWorldUniformProgress, progress));

  {
    const f32 left = Settings::hudTimelineQuickRepeaterXrX + 2.0f * (-0.5f + begin) * Settings::hudTimelineQuickRepeaterXrScaleX;
    const f32 right = Settings::hudTimelineQuickRepeaterXrX + 2.0f * (-0.5f + end) * Settings::hudTimelineQuickRepeaterXrScaleX;

    f32 top;
    f32 bottom;
    if (Settings::hudTimelineQuickRepeaterFlipY)
    {
      top = Settings::hudTimelineQuickRepeaterXrY + 0.5f * Settings::hudTimelineQuickRepeaterXrScaleY;
      bottom = Settings::hudTimelineQuickRepeaterXrY;
    }
    else
    {
      top = Settings::hudTimelineQuickRepeaterXrY;
      bottom = Settings::hudTimelineQuickRepeaterXrY - 0.5f * Settings::hudTimelineQuickRepeaterXrScaleY;
    }

    const mat4 model = {
      .m00 = 0.5f * (right - left),
      .m11 = 0.5f * (top - bottom),
      .m03 = left + 0.5f * (right - left),
      .m13 = bottom + 0.5f * (top - bottom),
      .m23 = Settings::hudTimelineMusicStretcherXrZ
    };
    const mat4 mvp = vec::multiply(viewProjectionMat, model);
    GL(glUniformMatrix4fv(Shader::timelineSimpleWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

    GL(glUniform4f(Shader::timelineSimpleWorldUniformColor, Settings::hudTimelineQuickRepeaterColor[0].r, Settings::hudTimelineQuickRepeaterColor[0].g, Settings::hudTimelineQuickRepeaterColor[0].b, Settings::hudTimelineQuickRepeaterColor[0].a));
    GL(glUniform4f(Shader::timelineSimpleWorldUniformColor2, Settings::hudTimelineQuickRepeaterColor[1].r, Settings::hudTimelineQuickRepeaterColor[1].g, Settings::hudTimelineQuickRepeaterColor[1].b, Settings::hudTimelineQuickRepeaterColor[1].a));

    GL(glDrawElementsBaseVertex_compat(planeZ));
  }
}

void Hud::render(const Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& viewProjectionMat)
{
  if (Global::inputDebugInfo.toggled)
    drawDebugInfoXr(viewProjectionMat);

  if (Settings::hudArrangementSwitch)
    drawArrangementSwitchXr(ctx.arrangementSwitchTime, selectedArrangementIndex, viewProjectionMat);

  if (Settings::hudWatermark)
    drawWatermarkXr(viewProjectionMat);

  if (Settings::hudToneSwitch)
    drawSfxToneXr(viewProjectionMat);

  if (Global::selectedSongIndex >= 0)
  {
    GL(glBindVertexArray(Global::staticDrawVao));
    drawSectionsXr(selectedArrangementIndex, viewProjectionMat);
    drawQuickRepeaterXr(viewProjectionMat);
    GL(glBindVertexArray(Global::dynamicDrawVao));
  }
}
#endif // SHR3D_OPENXR
