// SPDX-License-Identifier: Unlicense

#include "highway.h"

#include "data.h"
#include "font.h"
#include "geometry.h"
#include "global.h"
#include "helper.h"
#include "opengl.h"
#include "particle.h"
#include "psarc.h"
#include "settings.h"
#include "shader.h"
#include "song.h"
#include "sound.h"
#include "type.h"

#include <malloc.h>

void Highway::tick(Ctx& ctx, const ArrangementIndex selectedArrangementIndex)
{
  if (Global::selectedSongIndex == -1)
    return;

  ASSERT(Global::songInfos[Global::selectedSongIndex].loadState == Song::LoadState::complete);

  ctx.arrangement = &Global::songInfos[Global::selectedSongIndex].arrangementInfos[selectedArrangementIndex];
  //ctx.isBass = Global::songInfos[Global::selectedSongIndex].arrangementInfos[selectedArrangementIndex].isBass;
  //ctx.instrumentTuning = &Global::songInfos[Global::selectedSongIndex].arrangementInfos[selectedArrangementIndex].tuning;
  //ctx.instrumentCapoFret = &Global::songInfos[Global::selectedSongIndex].arrangementInfos[selectedArrangementIndex].capoFret;
  ctx.instrumentStringOffset = 0;
  ctx.instrumentHideFirstStrings = 0;

  {
    i8 i = 0;
    for (; i < Const::highwayInstrumentGuitarStringCount; ++i)
      if (ctx.arrangement->tuning.string[i] == -128)
        break;
    ctx.instrumentStringCount = i;
  }

  if (ctx.arrangement->isBass)
  {
    ctx.instrumentStringColors = Settings::highwayInstrumentBassStringColor;
    ctx.instrumentFirstWoundString = &Settings::highwayInstrumentBassFirstWoundString;
    ctx.instrumentStringSpacing = &Settings::highwayInstrumentBassStringSpacing;

    if (ctx.instrumentStringCount <= 4
      && ctx.arrangement->tuning.string[0] <= Settings::highwayInstrumentBass5StringTuning[0]
      && ctx.arrangement->tuning.string[1] <= Settings::highwayInstrumentBass5StringTuning[1]
      && ctx.arrangement->tuning.string[2] <= Settings::highwayInstrumentBass5StringTuning[2]
      && ctx.arrangement->tuning.string[3] <= Settings::highwayInstrumentBass5StringTuning[3])
    {
      if (Settings::highwayInstrumentBass5StringHideString0)
      {
        ctx.instrumentHideFirstStrings = 1;
      }
      else
      {
        ctx.instrumentStringCount = 5;
        ctx.instrumentStringOffset = 1;
      }
    }
    for (i32 i = 1; i < ARRAY_SIZE(ctx.instrumentFretPosition); ++i)
    {
      ctx.instrumentFretPosition[i] = f32(i) * Settings::highwayInstrumentBassFretSpacing * (1.0f + (f32(ARRAY_SIZE(ctx.instrumentFretPosition) - i - 1) * (Settings::highwayInstrumentBassFretSpacingFactor)) / f32(ARRAY_SIZE(ctx.instrumentFretPosition) - 1));
    }
  }
  else
  {
    ctx.instrumentStringColors = Settings::highwayInstrumentGuitarStringColor;
    ctx.instrumentFirstWoundString = &Settings::highwayInstrumentGuitarFirstWoundString;
    ctx.instrumentStringSpacing = &Settings::highwayInstrumentGuitarStringSpacing;

    if (ctx.instrumentStringCount <= 6
      && ctx.arrangement->tuning.string[0] <= Settings::highwayInstrumentGuitar7StringTuning[0]
      && ctx.arrangement->tuning.string[1] <= Settings::highwayInstrumentGuitar7StringTuning[1]
      && ctx.arrangement->tuning.string[2] <= Settings::highwayInstrumentGuitar7StringTuning[2]
      && ctx.arrangement->tuning.string[3] <= Settings::highwayInstrumentGuitar7StringTuning[3]
      && ctx.arrangement->tuning.string[4] <= Settings::highwayInstrumentGuitar7StringTuning[4]
      && ctx.arrangement->tuning.string[5] <= Settings::highwayInstrumentGuitar7StringTuning[5])
    {
      if (Settings::highwayInstrumentGuitar7StringHideString0)
      {
        ctx.instrumentHideFirstStrings = 1;
      }
      else
      {
        ctx.instrumentStringCount = 7;
        ctx.instrumentStringOffset = 1;
      }
    }
    for (i32 i = 1; i < ARRAY_SIZE(ctx.instrumentFretPosition); ++i)
    {
      ctx.instrumentFretPosition[i] = f32(i) * Settings::highwayInstrumentGuitarFretSpacing * (1.0f + (f32(ARRAY_SIZE(ctx.instrumentFretPosition) - i - 1) * (Settings::highwayInstrumentGuitarFretSpacingFactor)) / f32(ARRAY_SIZE(ctx.instrumentFretPosition) - 1));
    }
  }
}

static i32 getNoteBeginIndex(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = 0; i < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].notes.size()); ++i)
  {
    const Song::Note& note = Global::songTrackLevelAdjusted[selectedArrangementIndex].notes[i];
    const TimeNS noteTime = -note.timeNS - note.sustainNS + Global::musicTimeElapsedNS;
    if (noteTime < 0)
      return i;
  }

  return i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].notes.size()) - 1;
}

static i32 getNoteEndIndex(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].notes.size()) - 1; i >= 0; --i)
  {
    const Song::Note& note = Global::songTrackLevelAdjusted[selectedArrangementIndex].notes[i];
    const TimeNS noteTime = -note.timeNS + Global::musicTimeElapsedNS;
    if (noteTime > timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
      return i;
  }

  return -2;
}

static i32 getChordBeginIndex(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = 0; i < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].chords.size()); ++i)
  {
    const Song::Chord& chord = Global::songTrackLevelAdjusted[selectedArrangementIndex].chords[i];
    const TimeNS noteTime = -chord.timeNS + Global::musicTimeElapsedNS;
    if (noteTime < 0)
      return max_(0, i - 1); // -1, because chord might have sustained notes that would be cut off otherwise
  }

  return i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].chords.size()) - 1;
}

static i32 getChordEndIndex(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].chords.size()) - 1; i >= 0; --i)
  {
    const Song::Chord& chord = Global::songTrackLevelAdjusted[selectedArrangementIndex].chords[i];
    const TimeNS noteTime = -chord.timeNS + Global::musicTimeElapsedNS;
    if (noteTime > timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
      return i;
  }

  return -2;
}

static i32 getSustainBeginIndex(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = 0; i < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains.size()); ++i)
  {
    const Song::Sustain& sustain = Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains[i];
    const TimeNS noteTime = -sustain.endTimeNS + Global::musicTimeElapsedNS;
    if (noteTime < 0)
      return i;
  }

  return i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains.size()) - 1;
}

static i32 getSustainEndIndex(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains.size()) - 1; i >= 0; --i)
  {
    const Song::Sustain& sustain = Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains[i];
    const TimeNS noteTime = -sustain.startTimeNS + Global::musicTimeElapsedNS;
    if (noteTime > timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
      return i;
  }

  return -2;
}

static i32 getBeatBeginIndex(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = 0; i < i32(Global::songTracks[selectedArrangementIndex].beats.size()); ++i)
  {
    const Song::Beat& beat = Global::songTracks[selectedArrangementIndex].beats[i];
    const TimeNS noteTime = -beat.timeNS + Global::musicTimeElapsedNS;
    if (noteTime < 0)
      return i;
  }

  return i32(Global::songTracks[selectedArrangementIndex].beats.size()) - 1;
}

static i32 getBeatEndIndex(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = i32(Global::songTracks[selectedArrangementIndex].beats.size()) - 1; i >= 0; --i)
  {
    const Song::Beat& beat = Global::songTracks[selectedArrangementIndex].beats[i];
    const TimeNS noteTime = -beat.timeNS + Global::musicTimeElapsedNS;
    if (noteTime > timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
      return i;
  }

  return -2;
}

static i32 getArpeggioBeginIndex(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = 0; i < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios.size()); ++i)
  {
    const Song::Arpeggio& arpeggios = Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios[i];
    const TimeNS noteTime = -arpeggios.endTimeNS + Global::musicTimeElapsedNS;
    if (noteTime < 0)
      return i;
  }

  return i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios.size()) - 1;
}

static i32 getArpeggioEndIndex(const ArrangementIndex selectedArrangementIndex)
{
  for (i32 i = i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios.size()) - 1; i >= 0; --i)
  {
    const Song::Arpeggio& arpeggios = Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios[i];
    const TimeNS noteTime = -arpeggios.startTimeNS + Global::musicTimeElapsedNS;
    if (noteTime > timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
      return i;
  }

  return -2;
}

// struct contains only indecies that are usefull
struct TrackIndices
{
  i32 currentAnchor = -1;
  i32 noteBegin = -1;
  i32 noteEnd = -1;
  i32 chordBegin = -1;
  i32 chordEnd = -1;
  i32 sustainBegin = -1;
  i32 sustainEnd = -1;
  i32 beatBegin = -1;
  i32 beatEnd = -1;
  i32 arpeggioBegin = -1;
  i32 arpeggioEnd = -1;
};

static TrackIndices getTrackIndices(const ArrangementIndex selectedArrangementIndex)
{
  TrackIndices trackIndices = {
    .currentAnchor = Song::getCurrentAnchorIndex(selectedArrangementIndex),
    .noteBegin = getNoteBeginIndex(selectedArrangementIndex),
    .noteEnd = getNoteEndIndex(selectedArrangementIndex),
    .chordBegin = getChordBeginIndex(selectedArrangementIndex),
    .chordEnd = getChordEndIndex(selectedArrangementIndex),
    .sustainBegin = getSustainBeginIndex(selectedArrangementIndex),
    .sustainEnd = getSustainEndIndex(selectedArrangementIndex),
    .beatBegin = getBeatBeginIndex(selectedArrangementIndex),
    .beatEnd = getBeatEndIndex(selectedArrangementIndex),
    .arpeggioBegin = getArpeggioBeginIndex(selectedArrangementIndex),
    .arpeggioEnd = getArpeggioEndIndex(selectedArrangementIndex)
  };

  ASSERT(trackIndices.currentAnchor >= 0);
  ASSERT(trackIndices.currentAnchor < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors.size()));
  ASSERT(trackIndices.noteBegin >= -1);
  ASSERT(trackIndices.noteBegin < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].notes.size()));
  ASSERT(trackIndices.noteEnd >= -2);
  ASSERT(trackIndices.noteEnd < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].notes.size()));
  ASSERT(trackIndices.chordBegin >= -1);
  ASSERT(trackIndices.chordBegin < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].chords.size()));
  ASSERT(trackIndices.chordEnd >= -2);
  ASSERT(trackIndices.chordEnd < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].chords.size()));
  ASSERT(trackIndices.sustainBegin >= -1);
  ASSERT(trackIndices.sustainBegin < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains.size()));
  ASSERT(trackIndices.sustainEnd >= -2);
  ASSERT(trackIndices.sustainEnd < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains.size()));
  ASSERT(trackIndices.beatBegin >= -1);
  ASSERT(trackIndices.beatBegin < i32(Global::songTracks[selectedArrangementIndex].beats.size()));
  ASSERT(trackIndices.beatEnd >= -2);
  ASSERT(trackIndices.beatEnd < i32(Global::songTracks[selectedArrangementIndex].beats.size()));
  ASSERT(trackIndices.arpeggioBegin >= -1);
  ASSERT(trackIndices.arpeggioBegin < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios.size()));
  ASSERT(trackIndices.arpeggioEnd >= -2);
  ASSERT(trackIndices.arpeggioEnd < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios.size()));

  return trackIndices;
}

static TimeNS getAnchorTimeEndNS(const ArrangementIndex selectedArrangementIndex, i32 anchorIndex)
{
  if (anchorIndex == i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors.size()) - 1)
    return Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd;

  const Song::Anchor& anchor = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[anchorIndex + 1];
  return anchor.timeNS;
}

static const Song::Anchor& findAnchorByTime(const TimeNS timeNS, const ArrangementIndex selectedArrangementIndex, const i32 currentAnchorIndex)
{
  for (i32 j = currentAnchorIndex; j < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors.size()); ++j)
  {
    const Song::Anchor& anchor = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[j];
    if (timeNS >= anchor.timeNS)
    {
      const TimeNS anchorTimeEnd = getAnchorTimeEndNS(selectedArrangementIndex, j);
      if (timeNS < anchorTimeEnd)
        return anchor;
    }
  }

  return Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex];
}

static void drawGroundFrets(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, i32 currentAnchorIndex)
{
  if (currentAnchorIndex == -1)
    return;

  GL(glUseProgram(Shader::groundFret));

  GL(glUniform1f(Shader::groundFretUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::groundFretUniformFadeFarDistance, Settings::highwayFadeFarDistance));
  GL(glUniform1f(Shader::groundFretUniformFadeNearDistance, Settings::highwayFadeNearDistance));
  GL(glUniform1f(Shader::groundFretUniformFadeNearStrength, Settings::highwayFadeNearStrength));
  GL(glUniformMatrix4fv(Shader::groundFretUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));

  const i32 left = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].fret - 1;
  const i32 righ = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].fret + Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].width;

  for (i32 i = max_(0, left - 3); i < min_(24, righ + 3); ++i)
  {
    if (left <= i && i < righ)
    {
      GL(glUniform4f(Shader::groundFretUniformColor, Settings::highwayGroundFretColor[0].r, Settings::highwayGroundFretColor[0].g, Settings::highwayGroundFretColor[0].b, Settings::highwayGroundFretColor[0].a));
      GL(glUniform4f(Shader::groundFretUniformColor2, Settings::highwayGroundFretColor[1].r, Settings::highwayGroundFretColor[1].g, Settings::highwayGroundFretColor[1].b, Settings::highwayGroundFretColor[1].a));
    }
    else if (left - 2 <= i && i < righ + 2)
    {
      GL(glUniform4f(Shader::groundFretUniformColor, Settings::highwayGroundFretColor[0].r * 0.7f, Settings::highwayGroundFretColor[0].g * 0.7f, Settings::highwayGroundFretColor[0].b * 0.7f, Settings::highwayGroundFretColor[0].a * 0.7f));
      GL(glUniform4f(Shader::groundFretUniformColor2, Settings::highwayGroundFretColor[1].r * 0.7f, Settings::highwayGroundFretColor[1].g * 0.7f, Settings::highwayGroundFretColor[1].b * 0.7f, Settings::highwayGroundFretColor[1].a * 0.7f));
    }
    else
    {
      GL(glUniform4f(Shader::groundFretUniformColor, Settings::highwayGroundFretColor[0].r * 0.3f, Settings::highwayGroundFretColor[0].g * 0.3f, Settings::highwayGroundFretColor[0].b * 0.3f, Settings::highwayGroundFretColor[0].a * 0.3f));
      GL(glUniform4f(Shader::groundFretUniformColor2, Settings::highwayGroundFretColor[1].r * 0.3f, Settings::highwayGroundFretColor[1].g * 0.3f, Settings::highwayGroundFretColor[1].b * 0.3f, Settings::highwayGroundFretColor[1].a * 0.3f));
    }

    const mat4 model = {
      .m00 = 0.04f,
      .m22 = 0.5f * Settings::highwayViewDistance,
      .m03 = ctx.instrumentFretPosition[i],
      .m13 = -0.36f,
      .m23 = -0.5f * Settings::highwayViewDistance
    };

    GL(glUniformMatrix4fv(Shader::groundFretUniformModel, 1, GL_FALSE, &model.m00));

    GL(glDrawElements_(planeY));
  }
}

static void drawFretboardFrets(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat)
{
  GL(glUseProgram(Shader::fret));

  mat4 model;
  model.m11 = 0.0279f + 0.2948f * f32(ctx.instrumentStringCount) * *ctx.instrumentStringSpacing;
  model.m13 = -0.3f;
  const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
  GL(glUniformMatrix4fv(Shader::fretUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

  GL(glUniform1fv(Shader::fretUniformFretPositionOffsetX, ARRAY_SIZE(ctx.instrumentFretPosition), ctx.instrumentFretPosition));
  GL(glUniform4f(Shader::fretUniformColor, Settings::highwayFretboardFretColor.r, Settings::highwayFretboardFretColor.g, Settings::highwayFretboardFretColor.b, Settings::highwayFretboardFretColor.a));

  GL(glDrawElementsInstanced_(fret, ARRAY_SIZE(ctx.instrumentFretPosition)));
}

static void setStringColor(const Highway::Ctx& ctx, GLint colorUniform, const i8 string, const f32 alpha = -1.0f)
{
  ASSERT(string >= 0);
  ASSERT(string <= Const::highwayInstrumentGuitarStringCount - 1);

  const vec4& colorVec = ctx.instrumentStringColors[string];
  GL(glUniform4f(colorUniform, colorVec.r, colorVec.g, colorVec.b, alpha == -1.0f ? colorVec.a : alpha));
}

static i8 reverseStringFix(const Highway::Ctx& ctx, i8 i)
{
  if (!Settings::highwayReverseStrings)
    return i;

  return ctx.instrumentStringCount - 1 - i;
}

static f32 stringFadeUnplayedAlpha(const Highway::Ctx& ctx, f32 fretboardNearestNoteDistance[Const::highwayInstrumentGuitarStringCount][25], const i8 string)
{
  ASSERT(string >= 0);
  ASSERT(string <= Const::highwayInstrumentGuitarStringCount - 1);

  f32 alpha = Const::highwayStringFadeUnplayedAlpha;

  f32 distance = F32::inf;
  for (i32 i = 0; i < 25; ++i)
  {
    if (fretboardNearestNoteDistance[string][i] == 0.0f)
      continue;

    distance = min_(distance, fretboardNearestNoteDistance[string][i]);
  }

  if (distance < Const::highwayStringFadeUnplayedNearDistance)
  {
    ctx.highwayStringFadeUnplayedDecayTime[string] = Const::highwayStringFadeUnplayedDecayTime;
    alpha = 1.0f;
  }
  else if (distance < Const::highwayStringFadeUnplayedFarDistance)
    alpha = map_(distance, Const::highwayStringFadeUnplayedNearDistance, Const::highwayStringFadeUnplayedFarDistance, 1.0f, Const::highwayStringFadeUnplayedAlpha);

  if (ctx.highwayStringFadeUnplayedDecayTime[string] > 0.0f)
  {
    ctx.highwayStringFadeUnplayedDecayTime[string] -= TimeNS_To_Seconds(Global::frameDelta);
    if (ctx.highwayStringFadeUnplayedDecayTime[string] < 0.0f)
      ctx.highwayStringFadeUnplayedDecayTime[string] = 0.0f;
    else
      alpha = max_(alpha, (1.0f - Const::highwayStringFadeUnplayedAlpha) * (ctx.highwayStringFadeUnplayedDecayTime[string] / Const::highwayStringFadeUnplayedDecayTime) + Const::highwayStringFadeUnplayedAlpha);
  }

  ASSERT(alpha >= Const::highwayStringFadeUnplayedAlpha);
  ASSERT(alpha <= 1.0f);

  return alpha;
}

static void drawString(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, i8 i, GLint modelViewProjectionUniform, GLint colorUniform, f32 fretboardNearestNoteDistance[Const::highwayInstrumentGuitarStringCount][25])
{
  f32 alpha = 1.0f;
  if (Settings::highwayStringFadeUnplayed)
  {
    const i8 string = i - ctx.instrumentStringOffset;
    if (string > 0)
      alpha = stringFadeUnplayedAlpha(ctx, fretboardNearestNoteDistance, string);
    else
      alpha = Const::highwayStringFadeUnplayedAlpha;
  }

  setStringColor(ctx, colorUniform, i + ctx.instrumentHideFirstStrings, alpha);

  {
    const mat4 model = {
      .m11 = 1.0f + (i + ctx.instrumentHideFirstStrings) * Const::highwayStringGaugeMultiplier,
      .m22 = 1.0f + (i + ctx.instrumentHideFirstStrings) * Const::highwayStringGaugeMultiplier,
      .m13 = f32(reverseStringFix(ctx, i)) * *ctx.instrumentStringSpacing
    };
    const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
    GL(glUniformMatrix4fv(modelViewProjectionUniform, 1, GL_FALSE, &mvp.m00));
  }

  GL(glDrawElements_(string));
}

static void drawFretboardStrings(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, f32 fretboardNearestNoteDistance[Const::highwayInstrumentGuitarStringCount][25])
{
  // draw unwond wound
  const i8 noUnwoundStrings = max_(0, *ctx.instrumentFirstWoundString - ctx.instrumentHideFirstStrings);

  if (noUnwoundStrings >= 1)
  {
    GL(glUseProgram(Shader::string));

    for (i8 i = 0; i < noUnwoundStrings; ++i)
    {
      drawString(ctx, highwayViewProjectionMat, i, Shader::stringUniformModelViewProjection, Shader::stringUniformColor, fretboardNearestNoteDistance);
    }
  }

  // draw string wound
  if (noUnwoundStrings < ctx.instrumentStringCount)
  {
    GL(glUseProgram(Shader::stringWound));

    for (i8 i = noUnwoundStrings; i < ctx.instrumentStringCount; ++i)
    {
      drawString(ctx, highwayViewProjectionMat, i, Shader::stringWoundUniformModelViewProjection, Shader::stringWoundUniformColor, fretboardNearestNoteDistance);
    }
  }
}

// basic sustain without bends, tremolo, slides. It is just a simple quad.
static void drawNoteSustain(const Highway::Ctx& ctx, const f32 y, const mat4& highwayViewProjectionMat, const i8 noteFret, const TimeNS noteSustain, const TimeNS noteTime, const i32 chordBoxLeft, const i32 chordBoxWidth, const GLint viewProjectionUniform, const GLint modelUniform)
{
  mat4 model;
  f32 sustainHalfWidth;
  if (noteFret == 0)
  {
    sustainHalfWidth = 0.5_f32 * (ctx.instrumentFretPosition[chordBoxLeft - 1 + chordBoxWidth] - ctx.instrumentFretPosition[chordBoxLeft - 1]) * Settings::highwayNoteSustainWidthZero;
    model.m03 = ctx.instrumentFretPosition[chordBoxLeft - 1] + 0.5f * (ctx.instrumentFretPosition[chordBoxLeft - 1 + chordBoxWidth] - ctx.instrumentFretPosition[chordBoxLeft - 1]);
  }
  else
  {
    sustainHalfWidth = 0.5_f32 * Settings::highwayNoteSustainWidth;
    model.m03 = ctx.instrumentFretPosition[noteFret - 1] + 0.5f * (ctx.instrumentFretPosition[noteFret] - ctx.instrumentFretPosition[noteFret - 1]);
  }

  const f32 front = min_(TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed, 0.0f);
  const f32 back = TimeNS_To_Seconds(noteTime - noteSustain) * Settings::highwayScrollSpeed;

  model.m00 = sustainHalfWidth;
  model.m13 = y;
  model.m23 = back + 0.5f * (front - back);
  model.m22 = 0.5f * (front - back);

  GL(glUniformMatrix4fv(viewProjectionUniform, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniformMatrix4fv(modelUniform, 1, GL_FALSE, &model.m00));

  GL(glDrawElements_(planeY));
}

static void drawSlideToNoteSustain(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, TimeNS noteTime, const GLint viewProjectionUniform, const GLint modelUniform)
{
  ASSERT(note.slideTo >= 1);
  ASSERT(note.slideTo <= 24);

  const f32 slideX = ctx.instrumentFretPosition[note.slideTo] - ctx.instrumentFretPosition[note.fret];

  mat4 model;
  model.m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);
  model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
  model.m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

  GL(glUniformMatrix4fv(viewProjectionUniform, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniformMatrix4fv(modelUniform, 1, GL_FALSE, &model.m00));

  const f32 sustain = TimeNS_To_Seconds(note.sustainNS);

  const i32 triangleStripSegmentCount = 2 + i32(sustain / Settings::highwayNoteSustainCurveSampleDistance); // min 2. we always want a start and end point.
  const i32 triangleStripVertexCount = 2 * triangleStripSegmentCount; // triangle strip. vertex left and right.
  const i32 triangleStripVertexDataByteSize = 5 * triangleStripVertexCount * sizeof(f32);
  f32* triangleStripVertexData = reinterpret_cast<f32*>(alloca(triangleStripVertexDataByteSize)); // if this crashes, the stack size must be increased

  for (i32 i = 0; i < triangleStripSegmentCount; ++i)
  {
    const f32 u = f32(i) / f32(triangleStripSegmentCount - 1);
    const f32 xu = 3.0f * pow(u, 2.0f) * (1.0f - u) * slideX + pow(u, 3.0f) * slideX;
    const f32 zu = 3.0f * u * pow(1.0f - u, 2.0f) * 0.3f + 3.0f * pow(u, 2.0f) * (1.0f - u) * 0.7f + pow(u, 3.0f);

    const f32 sustainTime = (-zu * sustain + TimeNS_To_Seconds(noteTime)) * Settings::highwayScrollSpeed;

    // left vertex
    triangleStripVertexData[i * 10 + 0] = -0.5_f32 * Settings::highwayNoteSustainWidth + xu;
    triangleStripVertexData[i * 10 + 1] = 0.0f;
    triangleStripVertexData[i * 10 + 2] = -zu * sustain * Settings::highwayScrollSpeed;
    triangleStripVertexData[i * 10 + 3] = 0.0f;
    triangleStripVertexData[i * 10 + 4] = 0.5f;

    // right vertex
    triangleStripVertexData[i * 10 + 5] = 0.5_f32 * Settings::highwayNoteSustainWidth + xu;
    triangleStripVertexData[i * 10 + 6] = 0.0f;
    triangleStripVertexData[i * 10 + 7] = -zu * sustain * Settings::highwayScrollSpeed;
    triangleStripVertexData[i * 10 + 8] = 1.0f;
    triangleStripVertexData[i * 10 + 9] = 0.5f;
  }

  GL(glBindVertexArray(Global::dynamicDrawVao));
  GL(glBufferData(GL_ARRAY_BUFFER, triangleStripVertexDataByteSize, triangleStripVertexData, GL_STREAM_DRAW));
  GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, triangleStripVertexCount));
  GL(glBindVertexArray(Global::staticDrawVao));
}

static void drawUnpitchNoteSustain(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, TimeNS noteTime, const GLint viewProjectionUniform, const GLint modelUniform)
{
  ASSERT(note.slideUnpitchTo >= 0);
  ASSERT(note.slideUnpitchTo <= 24);

  const f32 slideX = ctx.instrumentFretPosition[note.slideUnpitchTo] - ctx.instrumentFretPosition[note.fret];

  mat4 model;
  model.m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);
  model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;

  GL(glUniformMatrix4fv(viewProjectionUniform, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniformMatrix4fv(modelUniform, 1, GL_FALSE, &model.m00));

  const f32 sustain = TimeNS_To_Seconds(note.sustainNS);

  const i32 triangleStripSegmentCount = 2 + i32(sustain / Settings::highwayNoteSustainCurveSampleDistance); // min 2. we always want a start and end point.
  const i32 triangleStripVertexCount = 2 * triangleStripSegmentCount; // triangle strip. vertex left and right.
  const i32 triangleStripVertexDataByteSize = 5 * triangleStripVertexCount * sizeof(f32);
  f32* triangleStripVertexData = reinterpret_cast<f32*>(alloca(triangleStripVertexDataByteSize)); // if this crashes, the stack size must be increased

  for (i32 i = 0; i < triangleStripSegmentCount; ++i)
  {
    const f32 u = f32(i) / f32(triangleStripSegmentCount - 1);
    const f32 xu = 3.0f * pow(u, 2.0f) * (1.0f - u) * slideX + pow(u, 3.0f) * slideX;
    const f32 zu = 3.0f * u * pow(1.0f - u, 2.0f) * 0.5f + 3.0f * pow(u, 2.0f) * (1.0f - u) + pow(u, 3.0f);

    const f32 sustainTime = (-zu * sustain + TimeNS_To_Seconds(noteTime)) * Settings::highwayScrollSpeed;

    // left vertex
    triangleStripVertexData[i * 10 + 0] = -0.5_f32 * Settings::highwayNoteSustainWidth + xu;
    triangleStripVertexData[i * 10 + 1] = 0.0f;
    triangleStripVertexData[i * 10 + 2] = sustainTime;
    triangleStripVertexData[i * 10 + 3] = 0.0f;
    triangleStripVertexData[i * 10 + 4] = 0.5f;

    // right vertex
    triangleStripVertexData[i * 10 + 5] = 0.5_f32 * Settings::highwayNoteSustainWidth + xu;
    triangleStripVertexData[i * 10 + 6] = 0.0f;
    triangleStripVertexData[i * 10 + 7] = sustainTime;
    triangleStripVertexData[i * 10 + 8] = 1.0f;
    triangleStripVertexData[i * 10 + 9] = 0.5f;
  }

  GL(glBindVertexArray(Global::dynamicDrawVao));
  GL(glBufferData(GL_ARRAY_BUFFER, triangleStripVertexDataByteSize, triangleStripVertexData, GL_STREAM_DRAW));
  GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, triangleStripVertexCount));
  GL(glBindVertexArray(Global::staticDrawVao));
}

static void drawVibratoNoteSustain(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, const TimeNS noteTime, const i32 chordBoxLeft, const i32 chordBoxWidth, const GLint viewProjectionUniform, const GLint modelUniform)
{
  mat4 model;
  f32 sustainHalfWidth;
  if (note.fret == 0)
  {
    model.m03 = ctx.instrumentFretPosition[chordBoxLeft - 1] + 0.5f * (ctx.instrumentFretPosition[chordBoxLeft - 1 + chordBoxWidth] - ctx.instrumentFretPosition[chordBoxLeft - 1]);
    sustainHalfWidth = 0.5_f32 * (ctx.instrumentFretPosition[chordBoxLeft - 1 + chordBoxWidth] - ctx.instrumentFretPosition[chordBoxLeft - 1]) * Settings::highwayNoteSustainWidthZero;
  }
  else
  {
    model.m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);
    sustainHalfWidth = 0.5_f32 * Settings::highwayNoteSustainWidth;
  }
  model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;

  GL(glUniformMatrix4fv(viewProjectionUniform, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniformMatrix4fv(modelUniform, 1, GL_FALSE, &model.m00));

  const f32 sustain = TimeNS_To_Seconds(note.sustainNS);

  const i32 triangleStripSegmentCount = 2 + i32(sustain / Settings::highwayNoteSustainCurveSampleDistance); // min 2. we always want a start and end point.
  const i32 triangleStripVertexCount = 2 * triangleStripSegmentCount; // triangle strip. vertex left and right.
  const i32 triangleStripVertexDataByteSize = 5 * triangleStripVertexCount * sizeof(f32);
  f32* triangleStripVertexData = reinterpret_cast<f32*>(alloca(triangleStripVertexDataByteSize)); // if this crashes, the stack size must be increased

  for (i32 i = 0; i < triangleStripSegmentCount; ++i)
  {
    const f32 u = f32(i) / f32(triangleStripSegmentCount - 1);

    const f32 f = u * sustain;
    f32 y = (f32(note.vibrato) / 200.0f) * *ctx.instrumentStringSpacing * cosf(f * Const::highwaySustainVibratoFrequency);

    // apply some linear dampening at the begin
    if (f < Const::highwaySustainVibratoDampingBegin)
      y = y * f * (1.0f / Const::highwaySustainVibratoDampingBegin);
    // apply some linear dampening at the end
    if (sustain - f < Const::highwaySustainVibratoDampingEnd)
      y = y * (sustain - f) * (1.0f / Const::highwaySustainVibratoDampingEnd);

    const f32 sustainTime = (TimeNS_To_Seconds(noteTime) - f) * Settings::highwayScrollSpeed;

    // left vertex
    triangleStripVertexData[i * 10 + 0] = -sustainHalfWidth;
    triangleStripVertexData[i * 10 + 1] = y;
    triangleStripVertexData[i * 10 + 2] = sustainTime;
    triangleStripVertexData[i * 10 + 3] = 0.0f;
    triangleStripVertexData[i * 10 + 4] = 0.5f;

    // right vertex
    triangleStripVertexData[i * 10 + 5] = sustainHalfWidth;
    triangleStripVertexData[i * 10 + 6] = y;
    triangleStripVertexData[i * 10 + 7] = sustainTime;
    triangleStripVertexData[i * 10 + 8] = 1.0f;
    triangleStripVertexData[i * 10 + 9] = 0.5f;
  }

  GL(glDisable(GL_CULL_FACE));
  GL(glBindVertexArray(Global::dynamicDrawVao));
  GL(glBufferData(GL_ARRAY_BUFFER, triangleStripVertexDataByteSize, triangleStripVertexData, GL_STREAM_DRAW));
  GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, triangleStripVertexCount));
  GL(glBindVertexArray(Global::staticDrawVao));
  GL(glEnable(GL_CULL_FACE));
}

static void drawVibratoBendNoteSustain(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, const TimeNS noteTime, const i32 chordBoxLeft, const i32 chordBoxWidth, const GLint viewProjectionUniform, const GLint modelUniform)
{
  ASSERT(note.bendValues.size() >= 1);

  mat4 model;
  f32 sustainHalfWidth;
  if (note.fret == 0)
  {
    model.m03 = ctx.instrumentFretPosition[chordBoxLeft - 1] + 0.5f * (ctx.instrumentFretPosition[chordBoxLeft - 1 + chordBoxWidth] - ctx.instrumentFretPosition[chordBoxLeft - 1]);
    sustainHalfWidth = 0.5_f32 * (ctx.instrumentFretPosition[chordBoxLeft - 1 + chordBoxWidth] - ctx.instrumentFretPosition[chordBoxLeft - 1]) * Settings::highwayNoteSustainWidthZero;
  }
  else
  {
    model.m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);
    sustainHalfWidth = 0.5_f32 * Settings::highwayNoteSustainWidth;
  }
  model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;

  GL(glUniformMatrix4fv(viewProjectionUniform, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniformMatrix4fv(modelUniform, 1, GL_FALSE, &model.m00));

  const f32 sustain = TimeNS_To_Seconds(note.sustainNS);

  const i32 triangleStripSegmentCount = 2 + i32(sustain / Settings::highwayNoteSustainCurveSampleDistance); // min 2. we always want a start and end point.
  const i32 triangleStripVertexCount = 2 * triangleStripSegmentCount; // triangle strip. vertex left and right.
  const i32 triangleStripVertexDataByteSize = 5 * triangleStripVertexCount * sizeof(f32);
  f32* triangleStripVertexData = reinterpret_cast<f32*>(alloca(triangleStripVertexDataByteSize)); // if this crashes, the stack size must be increased

  for (i32 i = 0; i < triangleStripSegmentCount; ++i)
  {
    // find current bend
    Song::BendValue firstDummyNoteBendValue
    {
      .timeNS = note.timeNS,
      .step = 0
    };
    Song::BendValue& bendValueBegin = firstDummyNoteBendValue;
    const Song::BendValue* bendValueEnd = &note.bendValues[0];
    f32 bendNoteTime = 0;

    const f32 u = f32(i) / f32(triangleStripSegmentCount - 1);

    const f32 f = u * sustain;

    for (i32 i = 1; i < i32(note.bendValues.size()); ++i)
    {
      bendNoteTime = TimeNS_To_Seconds(-bendValueEnd->timeNS + Global::musicTimeElapsedNS);

      if (bendNoteTime < (TimeNS_To_Seconds(noteTime) - f))
        break;

      bendValueBegin = *bendValueEnd;
      bendValueEnd = &note.bendValues[i];
    }

    const f32 bendStepsStart = note.string < 3 ? f32(bendValueBegin.step) * *ctx.instrumentStringSpacing : f32(-bendValueBegin.step) * *ctx.instrumentStringSpacing;
    const f32 bendStepsEnd = note.string < 3 ? f32(bendValueEnd->step) * *ctx.instrumentStringSpacing : f32(-bendValueEnd->step) * *ctx.instrumentStringSpacing;

    const f32 bendSustain = TimeNS_To_Seconds(bendValueEnd->timeNS - bendValueBegin.timeNS);
    ASSERT(bendSustain >= 0.0f); // should be > but too many songs have 0.0
    const f32 uu = (f - TimeNS_To_Seconds(bendValueBegin.timeNS - note.timeNS)) / bendSustain;

    f32 yu = bendStepsEnd; // the note can sustain longer than the bend values indicate. In this case the bend is held
    if (uu < 1.0f)
      yu = pow(1.0f - u, 3.0f) * bendStepsStart + 3.0f * uu * pow(1.0f - uu, 2.0f) * bendStepsStart + 3.0f * pow(uu, 2.0f) * (1.0f - uu) * bendStepsEnd + pow(uu, 3.0f) * bendStepsEnd;

    f32 y = (f32(note.vibrato) / 200.0f) * *ctx.instrumentStringSpacing * cosf(f * Const::highwaySustainVibratoFrequency);

    // apply some linear dampening at the begin
    if (f < Const::highwaySustainVibratoDampingBegin)
      y = y * f * (1.0f / Const::highwaySustainVibratoDampingBegin);
    // apply some linear dampening at the end
    if (sustain - f < Const::highwaySustainVibratoDampingEnd)
      y = y * (sustain - f) * (1.0f / Const::highwaySustainVibratoDampingEnd);

    const f32 sustainTime = (TimeNS_To_Seconds(noteTime) - f) * Settings::highwayScrollSpeed;

    // left vertex
    triangleStripVertexData[i * 10 + 0] = -sustainHalfWidth;
    triangleStripVertexData[i * 10 + 1] = y + yu;
    triangleStripVertexData[i * 10 + 2] = sustainTime;
    triangleStripVertexData[i * 10 + 3] = 0.0f;
    triangleStripVertexData[i * 10 + 4] = 0.5f;

    // right vertex
    triangleStripVertexData[i * 10 + 5] = sustainHalfWidth;
    triangleStripVertexData[i * 10 + 6] = y + yu;
    triangleStripVertexData[i * 10 + 7] = sustainTime;
    triangleStripVertexData[i * 10 + 8] = 1.0f;
    triangleStripVertexData[i * 10 + 9] = 0.5f;
  }

  GL(glDisable(GL_CULL_FACE));
  GL(glBindVertexArray(Global::dynamicDrawVao));
  GL(glBufferData(GL_ARRAY_BUFFER, triangleStripVertexDataByteSize, triangleStripVertexData, GL_STREAM_DRAW));
  GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, triangleStripVertexCount));
  GL(glBindVertexArray(Global::staticDrawVao));
  GL(glEnable(GL_CULL_FACE));
}

static void drawTremoloNoteSustain(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, TimeNS noteTime, i32 chordBoxLeft, i32 chordBoxWidth, const GLint viewProjectionUniform, const GLint modelUniform)
{
  mat4 model;

  f32 sustainHalfWidth;
  if (note.fret == 0)
  {
    model.m03 = ctx.instrumentFretPosition[chordBoxLeft - 1] + 0.5f * (ctx.instrumentFretPosition[chordBoxLeft - 1 + chordBoxWidth] - ctx.instrumentFretPosition[chordBoxLeft - 1]);
    sustainHalfWidth = 0.5_f32 * (ctx.instrumentFretPosition[chordBoxLeft - 1 + chordBoxWidth] - ctx.instrumentFretPosition[chordBoxLeft - 1]) * Settings::highwayNoteSustainWidthZero;
  }
  else
  {
    model.m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);
    sustainHalfWidth = 0.5_f32 * Settings::highwayNoteSustainWidth;
  }

  model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
  model.m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

  GL(glUniformMatrix4fv(viewProjectionUniform, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniformMatrix4fv(modelUniform, 1, GL_FALSE, &model.m00));

  const f32 sustain = TimeNS_To_Seconds(note.sustainNS);

  const i32 triangleStripSegmentCount = 2 + sustain / Settings::highwayNoteSustainTremoloSampleDistance; // min 2. we always want a start and end point.
  const i32 triangleStripVertexCount = 2 * triangleStripSegmentCount; // triangle strip. vertex left and right.
  const i32 triangleStripVertexDataByteSize = 5 * triangleStripVertexCount * sizeof(f32);
  f32* triangleStripVertexData = reinterpret_cast<f32*>(alloca(triangleStripVertexDataByteSize)); // if this crashes, the stack size must be increased

  for (i32 i = 0; i < triangleStripSegmentCount; ++i)
  {
    const f32 u = f32(i) / f32(triangleStripSegmentCount - 1);

    const f32 tremoloShake = (i % 2 == 0 ? Settings::highwayNoteSustainTremoloShakeStrength : -Settings::highwayNoteSustainTremoloShakeStrength);
    const f32 sustainTime = -u * sustain * Settings::highwayScrollSpeed;

    // left vertex
    triangleStripVertexData[i * 10 + 0] = -sustainHalfWidth + tremoloShake;
    triangleStripVertexData[i * 10 + 1] = 0.0f;
    triangleStripVertexData[i * 10 + 2] = sustainTime;
    triangleStripVertexData[i * 10 + 3] = 0.0f;
    triangleStripVertexData[i * 10 + 4] = 0.5f;

    // right vertex
    triangleStripVertexData[i * 10 + 5] = sustainHalfWidth + tremoloShake;
    triangleStripVertexData[i * 10 + 6] = 0.0f;
    triangleStripVertexData[i * 10 + 7] = sustainTime;
    triangleStripVertexData[i * 10 + 8] = 1.0f;
    triangleStripVertexData[i * 10 + 9] = 0.5f;
  }

  GL(glBindVertexArray(Global::dynamicDrawVao));
  GL(glBufferData(GL_ARRAY_BUFFER, triangleStripVertexDataByteSize, triangleStripVertexData, GL_STREAM_DRAW));
  GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, triangleStripVertexCount));
  GL(glBindVertexArray(Global::staticDrawVao));
}

static void drawTremoloSlideNoteSustain(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, const TimeNS noteTime, const GLint viewProjectionUniform, const GLint modelUniform)
{
  ASSERT(false); // TODO: replace std::vector with alloca.

  ASSERT(note.slideTo >= 1);
  ASSERT(note.slideTo <= 24);

  const f32 slideX = ctx.instrumentFretPosition[note.slideTo] - ctx.instrumentFretPosition[note.fret];

  mat4 model;
  model.m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);
  model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
  model.m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

  GL(glUniformMatrix4fv(viewProjectionUniform, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniformMatrix4fv(modelUniform, 1, GL_FALSE, &model.m00));

  std::vector<GLfloat> vv;

  {
    const f32 x[] = { 0.0f, 0.0f, slideX, slideX };
    const f32 z[] = { 0.0f, 0.3f, 0.7f, 1.0f };

    i32 j = 0;

    const f32 sustain = TimeNS_To_Seconds(note.sustainNS);

    for (f32 f = sustain - (0.5f * Settings::highwayNoteSustainTremoloSampleDistance); f > 0.0f; f -= Settings::highwayNoteSustainTremoloSampleDistance)
    {
      if (TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed - f * Settings::highwayScrollSpeed > 0.0f)
        continue;

      const f32 u = f / sustain;
      const f32 xu = pow(1.0f - u, 3.0f) * x[0] + 3.0f * u * pow(1.0f - u, 2.0f) * x[1] + 3.0f * pow(u, 2.0f) * (1.0f - u) * x[2] + pow(u, 3.0f) * x[3];
      const f32 zu = pow(1.0f - u, 3.0f) * z[0] + 3.0f * u * pow(1.0f - u, 2.0f) * z[1] + 3.0f * pow(u, 2.0f) * (1.0f - u) * z[2] + pow(u, 3.0f) * z[3];

      if (j % 2 == 0)
      {
        vv.insert(vv.end(), { 0.5_f32 * Settings::highwayNoteSustainWidth + xu + Settings::highwayNoteSustainTremoloShakeStrength, 0.0f, -zu * sustain * Settings::highwayScrollSpeed, 1.0f, 0.5f, });
        vv.insert(vv.end(), { -0.5_f32 * Settings::highwayNoteSustainWidth + xu + Settings::highwayNoteSustainTremoloShakeStrength, 0.0f, -zu * sustain * Settings::highwayScrollSpeed, 0.0f, 0.5f });
      }
      else
      {
        vv.insert(vv.end(), { 0.5_f32 * Settings::highwayNoteSustainWidth + xu - Settings::highwayNoteSustainTremoloShakeStrength, 0.0f, -zu * sustain * Settings::highwayScrollSpeed, 1.0f, 0.5f, });
        vv.insert(vv.end(), { -0.5_f32 * Settings::highwayNoteSustainWidth + xu - Settings::highwayNoteSustainTremoloShakeStrength , 0.0f, -zu * sustain * Settings::highwayScrollSpeed, 0.0f, 0.5f });
      }
      ++j;
    }
  }

  GL(glBindVertexArray(Global::dynamicDrawVao));
  GL(glBufferData(GL_ARRAY_BUFFER, vv.size() * sizeof(GLfloat), vv.data(), GL_STREAM_DRAW));
  GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, i32(vv.size() / 5)));
  GL(glBindVertexArray(Global::staticDrawVao));
}

static void drawTremoloSlideUnpitchVibratoNoteSustain(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, const TimeNS noteTime, const GLint viewProjectionUniform, const GLint modelUniform)
{
  ASSERT(note.slideUnpitchTo >= 0);
  ASSERT(note.slideUnpitchTo <= 24);

  const f32 slideX = ctx.instrumentFretPosition[note.slideUnpitchTo] - ctx.instrumentFretPosition[note.fret];

  mat4 model;
  model.m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);
  model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;

  GL(glUniformMatrix4fv(viewProjectionUniform, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniformMatrix4fv(modelUniform, 1, GL_FALSE, &model.m00));

  const f32 sustain = TimeNS_To_Seconds(note.sustainNS);

  const i32 triangleStripSegmentCount = 2 + i32(sustain / Settings::highwayNoteSustainCurveSampleDistance); // min 2. we always want a start and end point.
  const i32 triangleStripVertexCount = 2 * triangleStripSegmentCount; // triangle strip. vertex left and right.
  const i32 triangleStripVertexDataByteSize = 5 * triangleStripVertexCount * sizeof(f32);
  f32* triangleStripVertexData = reinterpret_cast<f32*>(alloca(triangleStripVertexDataByteSize)); // if this crashes, the stack size must be increased

  for (i32 i = 0; i < triangleStripSegmentCount; ++i)
  {
    const f32 u = f32(i) / f32(triangleStripSegmentCount - 1);

    const f32 xu = 3.0f * pow(u, 2.0f) * (1.0f - u) * slideX + pow(u, 3.0f) * slideX;
    const f32 zu = 3.0f * u * pow(1.0f - u, 2.0f) * 0.5f + 3.0f * pow(u, 2.0f) * (1.0f - u) + pow(u, 3.0f);

    const f32 f = u * sustain;
    f32 y = (f32(note.vibrato) / 200.0f) * *ctx.instrumentStringSpacing * cosf(f * Const::highwaySustainVibratoFrequency);

    // apply some linear dampening at the begin
    if (f < Const::highwaySustainVibratoDampingBegin)
      y = y * f * (1.0f / Const::highwaySustainVibratoDampingBegin);
    // apply some linear dampening at the end
    if (sustain - f < Const::highwaySustainVibratoDampingEnd)
      y = y * (sustain - f) * (1.0f / Const::highwaySustainVibratoDampingEnd);

    const f32 tremoloShake = (i % 2 == 0 ? Settings::highwayNoteSustainTremoloShakeStrength : -Settings::highwayNoteSustainTremoloShakeStrength);
    const f32 sustainTime = (-zu * sustain + TimeNS_To_Seconds(noteTime)) * Settings::highwayScrollSpeed;

    // left vertex
    triangleStripVertexData[i * 10 + 0] = -0.5f * Settings::highwayNoteSustainWidth + xu + tremoloShake;
    triangleStripVertexData[i * 10 + 1] = y;
    triangleStripVertexData[i * 10 + 2] = sustainTime;
    triangleStripVertexData[i * 10 + 3] = 0.0f;
    triangleStripVertexData[i * 10 + 4] = 0.5f;

    // right vertex
    triangleStripVertexData[i * 10 + 5] = 0.5f * Settings::highwayNoteSustainWidth + xu + tremoloShake;
    triangleStripVertexData[i * 10 + 6] = y;
    triangleStripVertexData[i * 10 + 7] = sustainTime;
    triangleStripVertexData[i * 10 + 8] = 1.0f;
    triangleStripVertexData[i * 10 + 9] = 0.5f;
  }

  GL(glDisable(GL_CULL_FACE));
  GL(glBindVertexArray(Global::dynamicDrawVao));
  GL(glBufferData(GL_ARRAY_BUFFER, triangleStripVertexDataByteSize, triangleStripVertexData, GL_STREAM_DRAW));
  GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, triangleStripVertexCount));
  GL(glBindVertexArray(Global::staticDrawVao));
  GL(glEnable(GL_CULL_FACE));
}

static void drawTremoloSlideUnpitchNoteSustain(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, const TimeNS noteTime, const GLint viewProjectionUniform, const GLint modelUniform)
{
  ASSERT(note.slideUnpitchTo >= 0);
  ASSERT(note.slideUnpitchTo <= 24);

  const f32 slideX = ctx.instrumentFretPosition[note.slideUnpitchTo] - ctx.instrumentFretPosition[note.fret];

  mat4 model;
  model.m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);
  model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;

  GL(glUniformMatrix4fv(viewProjectionUniform, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniformMatrix4fv(modelUniform, 1, GL_FALSE, &model.m00));

  const f32 sustain = TimeNS_To_Seconds(note.sustainNS);

  const i32 triangleStripSegmentCount = 2 + i32(sustain / Settings::highwayNoteSustainTremoloSampleDistance); // min 2. we always want a start and end point.
  const i32 triangleStripVertexCount = 2 * triangleStripSegmentCount; // triangle strip. vertex left and right.
  const i32 triangleStripVertexDataByteSize = 5 * triangleStripVertexCount * sizeof(f32);
  f32* triangleStripVertexData = reinterpret_cast<f32*>(alloca(triangleStripVertexDataByteSize)); // if this crashes, the stack size must be increased

  for (i32 i = 0; i < triangleStripSegmentCount; ++i)
  {
    const f32 u = f32(i) / f32(triangleStripSegmentCount - 1);

    const f32 xu = 3.0f * pow(u, 2.0f) * (1.0f - u) * slideX + pow(u, 3.0f) * slideX;
    const f32 zu = 3.0f * u * pow(1.0f - u, 2.0f) * 0.5f + 3.0f * pow(u, 2.0f) * (1.0f - u) + pow(u, 3.0f);

    const f32 tremoloShake = (i % 2 == 0 ? Settings::highwayNoteSustainTremoloShakeStrength : -Settings::highwayNoteSustainTremoloShakeStrength);
    const f32 sustainTime = (-zu * sustain + TimeNS_To_Seconds(noteTime)) * Settings::highwayScrollSpeed;

    // left vertex
    triangleStripVertexData[i * 10 + 0] = -0.5f * Settings::highwayNoteSustainWidth + xu + tremoloShake;
    triangleStripVertexData[i * 10 + 1] = 0.0f;
    triangleStripVertexData[i * 10 + 2] = sustainTime;
    triangleStripVertexData[i * 10 + 3] = 0.0f;
    triangleStripVertexData[i * 10 + 4] = 0.5f;

    // right vertex
    triangleStripVertexData[i * 10 + 5] = 0.5f * Settings::highwayNoteSustainWidth + xu + tremoloShake;
    triangleStripVertexData[i * 10 + 6] = 0.0f;
    triangleStripVertexData[i * 10 + 7] = sustainTime;
    triangleStripVertexData[i * 10 + 8] = 1.0f;
    triangleStripVertexData[i * 10 + 9] = 0.5f;
  }

  GL(glBindVertexArray(Global::dynamicDrawVao));
  GL(glBufferData(GL_ARRAY_BUFFER, triangleStripVertexDataByteSize, triangleStripVertexData, GL_STREAM_DRAW));
  GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, triangleStripVertexCount));
  GL(glBindVertexArray(Global::staticDrawVao));
}

static void drawVibratoSlideUnpitchNoteSustain(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, const TimeNS noteTime, const GLint viewProjectionUniform, const GLint modelUniform)
{
  ASSERT(false); // TODO: replace std::vector with alloca.

  ASSERT(note.slideUnpitchTo >= 0);
  ASSERT(note.slideUnpitchTo <= 24);

  const f32 slideX = ctx.instrumentFretPosition[note.slideUnpitchTo] - ctx.instrumentFretPosition[note.fret];

  mat4 model;
  model.m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);
  model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;

  GL(glUniformMatrix4fv(viewProjectionUniform, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniformMatrix4fv(modelUniform, 1, GL_FALSE, &model.m00));

  std::vector<GLfloat> vv;

  {
    const f32 sustain = TimeNS_To_Seconds(note.sustainNS);

    for (f32 f = sustain - (0.5f * Settings::highwayNoteSustainTremoloSampleDistance); f > 0.0f; f -= Settings::highwayNoteSustainTremoloSampleDistance)
    {
      const f32 u = f / sustain;
      const f32 xu = 3.0f * pow(u, 2.0f) * (1.0f - u) * slideX + pow(u, 3.0f) * slideX;
      const f32 zu = 3.0f * u * pow(1.0f - u, 2.0f) * 0.5f + 3.0f * pow(u, 2.0f) * (1.0f - u) + pow(u, 3.0f);

      const f32 sustainTime = (-zu * sustain + TimeNS_To_Seconds(noteTime)) * Settings::highwayScrollSpeed;

      f32 y = (f32(note.vibrato) / 200.0f) * *ctx.instrumentStringSpacing * cosf(f * Const::highwaySustainVibratoFrequency);

      // apply some linear dampening at the begin
      if (f < Const::highwaySustainVibratoDampingBegin)
        y = y * f * (1.0f / Const::highwaySustainVibratoDampingBegin);
      // apply some linear dampening at the end
      if (sustain - f < Const::highwaySustainVibratoDampingEnd)
        y = y * (sustain - f) * (1.0f / Const::highwaySustainVibratoDampingEnd);

      vv.insert(vv.end(), { 0.5_f32 * Settings::highwayNoteSustainWidth + xu, y, sustainTime, 1.0f, 0.5f, });
      vv.insert(vv.end(), { -0.5_f32 * Settings::highwayNoteSustainWidth + xu, y, sustainTime, 0.0f, 0.5f });
    }

    const f32 sustainTime = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;
    vv.insert(vv.end(), { 0.5_f32 * Settings::highwayNoteSustainWidth, 0.0f, sustainTime, 1.0f, 0.5f, });
    vv.insert(vv.end(), { -0.5_f32 * Settings::highwayNoteSustainWidth, 0.0f, sustainTime, 0.0f, 0.5f });
  }

  GL(glDisable(GL_CULL_FACE));
  GL(glBindVertexArray(Global::dynamicDrawVao));
  GL(glBufferData(GL_ARRAY_BUFFER, vv.size() * sizeof(GLfloat), vv.data(), GL_STREAM_DRAW));
  GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, i32(vv.size() / 5)));
  GL(glBindVertexArray(Global::staticDrawVao));
  GL(glEnable(GL_CULL_FACE));
}

static void drawTremoloVibratoNoteSustain(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, TimeNS noteTime, i32 chordBoxLeft, i32 chordBoxWidth, const GLint viewProjectionUniform, const GLint modelUniform)
{
  mat4 model;
  f32 sustainHalfWidth;
  if (note.fret == 0)
  {
    model.m03 = ctx.instrumentFretPosition[chordBoxLeft - 1] + 0.5f * (ctx.instrumentFretPosition[chordBoxLeft - 1 + chordBoxWidth] - ctx.instrumentFretPosition[chordBoxLeft - 1]);
    sustainHalfWidth = 0.5_f32 * (ctx.instrumentFretPosition[chordBoxLeft - 1 + chordBoxWidth] - ctx.instrumentFretPosition[chordBoxLeft - 1]) * Settings::highwayNoteSustainWidthZero;
  }
  else
  {
    model.m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);
    sustainHalfWidth = 0.5_f32 * Settings::highwayNoteSustainWidth;
  }

  model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
  model.m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

  GL(glUniformMatrix4fv(viewProjectionUniform, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniformMatrix4fv(modelUniform, 1, GL_FALSE, &model.m00));

  const f32 sustain = TimeNS_To_Seconds(note.sustainNS);

  const i32 triangleStripSegmentCount = 2 + i32(sustain / Settings::highwayNoteSustainCurveSampleDistance); // min 2. we always want a start and end point.
  const i32 triangleStripVertexCount = 2 * triangleStripSegmentCount; // triangle strip. vertex left and right.
  const i32 triangleStripVertexDataByteSize = 5 * triangleStripVertexCount * sizeof(f32);
  f32* triangleStripVertexData = reinterpret_cast<f32*>(alloca(triangleStripVertexDataByteSize)); // if this crashes, the stack size must be increased

  for (i32 i = 0; i < triangleStripSegmentCount; ++i)
  {
    const f32 u = f32(i) / f32(triangleStripSegmentCount - 1);
    const f32 f = u * sustain;
    f32 y = (f32(note.vibrato) / 200.0f) * *ctx.instrumentStringSpacing * cosf(f * Const::highwaySustainVibratoFrequency);

    // apply some linear dampening at the begin
    if (f < Const::highwaySustainVibratoDampingBegin)
      y = y * f * (1.0f / Const::highwaySustainVibratoDampingBegin);
    // apply some linear dampening at the end
    if (sustain - f < Const::highwaySustainVibratoDampingEnd)
      y = y * (sustain - f) * (1.0f / Const::highwaySustainVibratoDampingEnd);

    const f32 tremoloShake = (i % 2 == 0 ? Settings::highwayNoteSustainTremoloShakeStrength : -Settings::highwayNoteSustainTremoloShakeStrength);
    const f32 sustainTime = -u * sustain * Settings::highwayScrollSpeed;

    // left vertex
    triangleStripVertexData[i * 10 + 0] = -sustainHalfWidth + tremoloShake;
    triangleStripVertexData[i * 10 + 1] = y;
    triangleStripVertexData[i * 10 + 2] = sustainTime;
    triangleStripVertexData[i * 10 + 3] = 0.0f;
    triangleStripVertexData[i * 10 + 4] = 0.5f;

    // right vertex
    triangleStripVertexData[i * 10 + 5] = sustainHalfWidth + tremoloShake;
    triangleStripVertexData[i * 10 + 6] = y;
    triangleStripVertexData[i * 10 + 7] = sustainTime;
    triangleStripVertexData[i * 10 + 8] = 1.0f;
    triangleStripVertexData[i * 10 + 9] = 0.5f;
  }

  GL(glDisable(GL_CULL_FACE));
  GL(glBindVertexArray(Global::dynamicDrawVao));
  GL(glBufferData(GL_ARRAY_BUFFER, triangleStripVertexDataByteSize, triangleStripVertexData, GL_STREAM_DRAW));
  GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, triangleStripVertexCount));
  GL(glBindVertexArray(Global::staticDrawVao));
  GL(glEnable(GL_CULL_FACE));
}

static void drawBendNoteSustain(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, i32 chordBoxLeft, i32 chordBoxWidth, const GLint viewProjectionUniform, const GLint modelUniform)
{
  Song::BendValue firstDummyNoteBendValue
  {
    .timeNS = note.timeNS,
    .step = 0
  };

  Song::BendValue& bendValueBegin = firstDummyNoteBendValue;

  for (i32 i = 0; i < i32(note.bendValues.size()); ++i)
  {
    const Song::BendValue& bendValueEnd = note.bendValues[i];
    const f32 bendNoteTime = TimeNS_To_Seconds(-bendValueBegin.timeNS + Global::musicTimeElapsedNS);

    const f32 bendSteps = bendValueEnd.step - bendValueBegin.step;
    const f32 bendSustain = TimeNS_To_Seconds(bendValueEnd.timeNS - bendValueBegin.timeNS);
    ASSERT(bendSustain >= 0.0f); // should be > but too many songs have 0.0

    const i8 string = note.string < 3 ? note.string + bendValueBegin.step : note.string - bendValueBegin.step;

    if (bendSteps == 0.0f)
    {
      const f32 y = f32(reverseStringFix(ctx, string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
      drawNoteSustain(ctx, y, highwayViewProjectionMat, note.fret, timeNS_From_Seconds(bendSustain), timeNS_From_Seconds(bendNoteTime), chordBoxLeft, chordBoxWidth, viewProjectionUniform, modelUniform);
    }
    else
    {
      mat4 model;
      model.m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);
      model.m13 = f32(reverseStringFix(ctx, string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;

      GL(glUniformMatrix4fv(viewProjectionUniform, 1, GL_FALSE, &highwayViewProjectionMat.m00));
      GL(glUniformMatrix4fv(modelUniform, 1, GL_FALSE, &model.m00));

      const f32 bendStepsHeight = note.string < 3 ? bendSteps * *ctx.instrumentStringSpacing : -bendSteps * *ctx.instrumentStringSpacing;

      const i32 triangleStripSegmentCount = 2 + bendSustain / Settings::highwayNoteSustainCurveSampleDistance; // min 2. we always want a start and end point.
      const i32 triangleStripVertexCount = 2 * triangleStripSegmentCount; // triangle strip. vertex left and right.
      const i32 triangleStripVertexDataByteSize = 5 * triangleStripVertexCount * sizeof(f32);
      f32* triangleStripVertexData = reinterpret_cast<f32*>(alloca(triangleStripVertexDataByteSize)); // if this crashes, the stack size must be increased

      for (i32 j = 0; j < triangleStripSegmentCount; ++j)
      {
        const f32 u = f32(j) / f32(triangleStripSegmentCount - 1);
        const f32 yu = 3.0f * pow(u, 2.0f) * (1.0f - u) * bendStepsHeight + pow(u, 3.0f) * bendStepsHeight;
        const f32 zu = 3.0f * u * pow(1.0f - u, 2.0f) * Settings::highwayNoteBendCurve + 3.0f * pow(u, 2.0f) * (1.0f - u) * (1.0f - Settings::highwayNoteBendCurve) + pow(u, 3.0f);

        const f32 sustainTime = (bendNoteTime - zu * bendSustain) * Settings::highwayScrollSpeed;

        // left vertex
        triangleStripVertexData[j * 10 + 0] = -0.5_f32 * Settings::highwayNoteSustainWidth;
        triangleStripVertexData[j * 10 + 1] = yu;
        triangleStripVertexData[j * 10 + 2] = sustainTime;
        triangleStripVertexData[j * 10 + 3] = 0.0f;
        triangleStripVertexData[j * 10 + 4] = 0.5f;

        // right vertex
        triangleStripVertexData[j * 10 + 5] = 0.5_f32 * Settings::highwayNoteSustainWidth;
        triangleStripVertexData[j * 10 + 6] = yu;
        triangleStripVertexData[j * 10 + 7] = sustainTime;
        triangleStripVertexData[j * 10 + 8] = 1.0f;
        triangleStripVertexData[j * 10 + 9] = 0.5f;
      }

      GL(glDisable(GL_CULL_FACE));
      GL(glBindVertexArray(Global::dynamicDrawVao));
      GL(glBufferData(GL_ARRAY_BUFFER, triangleStripVertexDataByteSize, triangleStripVertexData, GL_STREAM_DRAW));
      GL(glDrawArrays(GL_TRIANGLE_STRIP, 0, triangleStripVertexCount));
      GL(glBindVertexArray(Global::staticDrawVao));
      GL(glEnable(GL_CULL_FACE));
    }

    bendValueBegin = bendValueEnd;
  }

  { // the bend can end before the sustain of the note ends. Keep the current bend step and draw it.
    const Song::BendValue& lastBendValue = note.bendValues[note.bendValues.size() - 1];
    const TimeNS sustainTimeLeft = note.timeNS + note.sustainNS - lastBendValue.timeNS;
    if (sustainTimeLeft > 0)
    {
      const TimeNS bendNoteTime = -lastBendValue.timeNS + Global::musicTimeElapsedNS;
      const f32 y = (f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) + (note.string < 3 ? lastBendValue.step : -lastBendValue.step)) * *ctx.instrumentStringSpacing;
      drawNoteSustain(ctx, y, highwayViewProjectionMat, note.fret, sustainTimeLeft, bendNoteTime, chordBoxLeft, chordBoxWidth, viewProjectionUniform, modelUniform);
    }
  }
}

static void drawNoteSymbol(const NoteSymbols noteSymbol)
{
  switch (noteSymbol)
  {
  case NoteSymbols::none:
    break; // do nothing
  case NoteSymbols::fretMute:
    GL(glDrawElements_(noteSymbolFretMute));
    break;
  case NoteSymbols::hammerOn:
    GL(glDrawElements_(noteSymbolHammerOn));
    break;
  case NoteSymbols::harmonic:
    GL(glDrawElements_(noteSymbolHarmonic));
    break;
  case NoteSymbols::palmMute:
    GL(glDrawElements_(noteSymbolPalmMute));
    break;
  case NoteSymbols::pinchHarmonic:
    GL(glDrawElements_(noteSymbolPinchHarmonic));
    break;
  case NoteSymbols::pop:
    GL(glDrawElements_(noteSymbolPop));
    break;
  case NoteSymbols::pullOff:
    GL(glDrawElements_(noteSymbolPullOff));
    break;
  case NoteSymbols::slap:
    GL(glDrawElements_(noteSymbolSlap));
    break;
  case NoteSymbols::tap:
    GL(glDrawElements_(noteSymbolTap));
    break;
  case NoteSymbols::slimX:
    GL(glDrawElements_(noteSymbolSlimX));
    break;
  case NoteSymbols::slimEye:
    GL(glDrawElements_(noteSymbolSlimEye));
    break;
  case NoteSymbols::triangleDown:
    GL(glDrawElements_(noteSymbolTriangleDown));
    break;
  case NoteSymbols::triangleUp:
    GL(glDrawElements_(noteSymbolTriangleUp));
    break;
  case NoteSymbols::wideX:
    GL(glDrawElements_(noteSymbolWideX));
    break;
  case NoteSymbols::wideV:
    GL(glDrawElements_(noteSymbolWideV));
    break;
  case NoteSymbols::wideUpsideV:
    GL(glDrawElements_(noteSymbolWideUpsideV));
    break;
  case NoteSymbols::wideOffsetV:
    GL(glDrawElements_(noteSymbolWideOffsetV));
    break;
  case NoteSymbols::wideEye:
    GL(glDrawElements_(noteSymbolWideEye));
    break;
  case NoteSymbols::sineRight:
    GL(glDrawElements_(noteSymbolSineRight));
    break;
  case NoteSymbols::sineLeft:
    GL(glDrawElements_(noteSymbolSineLeft));
    break;
  default:
    unreachable();
  }
}

static void setFretboardNearestNoteDistance(const Song::Note& note, const TimeNS noteTime, f32 fretboardNearestNoteDistance[Const::highwayInstrumentGuitarStringCount][25])
{
  if (noteTime >= 0)
    fretboardNearestNoteDistance[note.string][note.fret] = 0.01f; // note sustain
  else if (fretboardNearestNoteDistance[note.string][note.fret] == 0.0f)
    fretboardNearestNoteDistance[note.string][note.fret] = max_(TimeNS_To_Seconds(-noteTime), 0.01f);
  else if (TimeNS_To_Seconds(-noteTime) < fretboardNearestNoteDistance[note.string][note.fret])
    fretboardNearestNoteDistance[note.string][note.fret] = max_(TimeNS_To_Seconds(-noteTime), 0.01f);

  ASSERT(fretboardNearestNoteDistance[note.string][note.fret] >= 0.01f);
}

static void drawNote(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, TimeNS noteTime, f32 fretboardNearestNoteDistance[Const::highwayInstrumentGuitarStringCount][25], i32 chordBoxLeft, i32 chordBoxWidth)
{
  setFretboardNearestNoteDistance(note, noteTime, fretboardNearestNoteDistance);

  if (note.accent) // this note symbol is handled earlier since it is implemented by using an extra shader
  {
    if (noteTime < 0)
    {
      GL(glUseProgram(Shader::noteAccent));

      GL(glUniform1f(Shader::noteAccentUniformViewDistance, Settings::highwayViewDistance));
      GL(glUniform1f(Shader::noteAccentUniformFadeFarDistance, Settings::highwayFadeFarDistance));

      setStringColor(ctx, Shader::noteAccentUniformColor, note.string + ctx.instrumentStringOffset + ctx.instrumentHideFirstStrings, 1.0f);

      mat4 model = {
        .m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing,
        .m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed
      };
      if (note.fret == 0)
      {
        model.m00 = 0.5f * ((ctx.instrumentFretPosition[chordBoxLeft - 1 + chordBoxWidth] - ctx.instrumentFretPosition[chordBoxLeft - 1]));
        model.m11 = 0.12f;
        model.m03 = ctx.instrumentFretPosition[chordBoxLeft] + 0.25f * (ctx.instrumentFretPosition[chordBoxLeft + chordBoxWidth] - ctx.instrumentFretPosition[chordBoxLeft]);
      }
      else
      {
        model.m00 = 0.4;
        model.m11 = 0.3f;
        model.m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);
      }

      GL(glUniformMatrix4fv(Shader::noteAccentUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
      GL(glUniformMatrix4fv(Shader::noteAccentUniformModel, 1, GL_FALSE, &model.m00));

      GL(glDrawElements_(planeZ));
    }
  }

  GL(glUseProgram(Shader::highwayFadeFar));

  GL(glUniform1f(Shader::highwayFadeFarUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::highwayFadeFarUniformFadeFarDistance, Settings::highwayFadeFarDistance));

  if (noteTime < 0)
  {
    if (note.fret == 0)
    {
      setStringColor(ctx, Shader::highwayFadeFarUniformColor, note.string + ctx.instrumentStringOffset + ctx.instrumentHideFirstStrings, 1.0f);

      const f32 zeroIndentionWidth = 0.05f;
      const f32 noteZeroLeftWidth = 0.216608f; // this is from the 3D model. Not a good idea

      {
        mat4 model;

        model.m03 = ctx.instrumentFretPosition[chordBoxLeft - 1] + zeroIndentionWidth;
        model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
        model.m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

        GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
        GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformModel, 1, GL_FALSE, &model.m00));

        GL(glDrawElements_(noteZeroLeft));
      }

      {
        mat4 model;
        model.m00 = ((ctx.instrumentFretPosition[chordBoxLeft - 1 + chordBoxWidth] - ctx.instrumentFretPosition[chordBoxLeft - 1]) - (zeroIndentionWidth + noteZeroLeftWidth) * 2.0f) * (1.0f / noteZeroLeftWidth);
        model.m03 = ctx.instrumentFretPosition[chordBoxLeft - 1] + zeroIndentionWidth + noteZeroLeftWidth;
        model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
        model.m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

        GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
        GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformModel, 1, GL_FALSE, &model.m00));

        GL(glDrawElements_(noteZeroMiddle));
      }

      {
        mat4 model;
        model.m03 = ctx.instrumentFretPosition[chordBoxLeft - 1 + chordBoxWidth] - zeroIndentionWidth - noteZeroLeftWidth;
        model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
        model.m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

        GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
        GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformModel, 1, GL_FALSE, &model.m00));

        GL(glDrawElements_(noteZeroRight));
      }

      {
        mat4 model;
        model.m03 = ctx.instrumentFretPosition[chordBoxLeft - 1] + 0.5f * ctx.instrumentFretPosition[chordBoxWidth];
        model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
        model.m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

        GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
        GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformModel, 1, GL_FALSE, &model.m00));
      }
    }
    else
    {
      //if (!note.ignore)
      if (!note.isLinked)
      {
        const f32 x = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);

        mat4 model;
        if (Settings::highwayNoteRotate != 0 && note.rotation)
        {
          const f32 rotZ = clamp((-TimeNS_To_Seconds(noteTime) - Settings::highwayNoteRotateEndTime) * Settings::highwayNoteRotateSpeed, 0.0f, 0.5f * PI_) * f32(Settings::highwayNoteRotate);
          model.m00 = Settings::highwayNoteWidth * cos(rotZ);
          model.m01 = -sin(rotZ);
          model.m10 = sin(rotZ);
          model.m11 = Settings::highwayNoteWidth * cos(rotZ);
        }
        else
        {
          model.m00 = Settings::highwayNoteWidth;
          model.m11 = Settings::highwayNoteHeight;
        }
        model.m03 = x;

        f32 bendSteps = 0.0f;
        if (note.bendValues.size() >= 1)
        {
          if (note.timeNS == note.bendValues[0].timeNS)
          {
            bendSteps = note.bendValues[0].step * (1.0f - clamp((-TimeNS_To_Seconds(noteTime) - Settings::highwayNoteBendEndTime) * Settings::highwayNoteBendSpeed, 0.0f, 1.0f));
            bendSteps = note.string < 3 ? bendSteps : -bendSteps;
          }
        }

        model.m13 = (f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) + bendSteps) * *ctx.instrumentStringSpacing;
        model.m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

        GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
        GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformModel, 1, GL_FALSE, &model.m00));

        setStringColor(ctx, Shader::highwayFadeFarUniformColor, note.string + ctx.instrumentStringOffset + ctx.instrumentHideFirstStrings, 1.0f);

        switch (Settings::highwayNoteShape)
        {
        case NoteShape::hex:
          GL(glDrawElements_(noteHex));
          break;
        case NoteShape::rect:
          GL(glDrawElements_(noteRect));
          break;
        default:
          unreachable();
        }
      }
    }
    if (note.hammerOn)
      drawNoteSymbol(Settings::highwayNoteSymbolHammerOn);
    if (note.harmonic)
      drawNoteSymbol(Settings::highwayNoteSymbolHarmonic);
    if (note.frethandMute)
      drawNoteSymbol(Settings::highwayNoteSymbolFretMute);
    if (note.palmMute)
      drawNoteSymbol(Settings::highwayNoteSymbolPalmMute);
    if (note.pullOff)
      drawNoteSymbol(Settings::highwayNoteSymbolPullOff);
    if (note.pluck) // not sure if this is pop
      drawNoteSymbol(Settings::highwayNoteSymbolPop);
    if (note.slap)
      drawNoteSymbol(Settings::highwayNoteSymbolSlap);
    if (note.pinchHarmonic)
      drawNoteSymbol(Settings::highwayNoteSymbolPinchHarmonic);
    if (note.tap)
      drawNoteSymbol(Settings::highwayNoteSymbolTap);

    if (note.maxBend != 0.0f)
    {
      const f32 flipY = note.string < 3 ? 1.0f : -1.0f;

      f32 bendSteps = 0.0f;
      if (note.bendValues.size() >= 1 && note.timeNS == note.bendValues[0].timeNS)
        bendSteps = note.bendValues[0].step * flipY;

      mat4 model;
      model.m00 = flipY;
      model.m11 = flipY;
      model.m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);
      model.m13 = (f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) + bendSteps) * *ctx.instrumentStringSpacing + flipY * Settings::highwayNoteBendHintOffset;
      model.m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

      // draw full and half bents.
      // idea:
      // bent between 0 and 0.25 should not be bents.
      // bent between 0.25 and 0.75 should be half bents.
      // bent between 0.75 and 1.25 should be full bents.
      // ...
      f32 intPart;
      const f32 fractPart = modf(note.maxBend + 0.25f, &intPart); // 0.2f to circumvent rounding error

      for (i32 i = 0; i < i32(intPart); ++i)
      {
        model.m13 += flipY * Settings::highwayNoteBendHintDistance;

        GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
        GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformModel, 1, GL_FALSE, &model.m00));

        GL(glDrawElements_(noteBendStepFull));
      }
      if (fractPart >= 0.5f && fractPart < 1.0f)
      {
        model.m13 += flipY * Settings::highwayNoteBendHintDistance;

        GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
        GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformModel, 1, GL_FALSE, &model.m00));

        GL(glDrawElements_(noteBendStepHalf));
      }
    }
  }

  if (note.sustainNS != 0)
  {
    GL(glUseProgram(Shader::sustain));

    GL(glUniform1f(Shader::sustainUniformViewDistance, Settings::highwayViewDistance));
    GL(glUniform1f(Shader::sustainUniformFadeFarDistance, Settings::highwayFadeFarDistance));

    {
      const vec4& colorVec = ctx.instrumentStringColors[note.string + ctx.instrumentStringOffset + ctx.instrumentHideFirstStrings];
      if (noteTime < 0)
      {
        GL(glUniform4f(Shader::sustainUniformColor, colorVec.r, colorVec.g, colorVec.b, 0.65f));
      }
      else
      {
        GL(glUniform4f(Shader::sustainUniformColor, colorVec.r * 1.5f, colorVec.g * 1.5f, colorVec.b * 1.5f, 0.95f));
      }
    }

    if (note.tremolo && note.slideTo >= 1 && note.slideTo != note.fret)
    {
      ASSERT(note.maxBend == 0.0f);
      ASSERT(note.vibrato == 0);
      ASSERT(note.slideUnpitchTo == -1);
      drawTremoloSlideNoteSustain(ctx, highwayViewProjectionMat, note, noteTime, Shader::sustainUniformViewProjection, Shader::sustainUniformModel);
    }
    else if (note.tremolo && note.slideUnpitchTo != -1 && note.vibrato != 0)
    {
      ASSERT(note.maxBend == 0.0f);
      ASSERT(note.slideTo == -1);
      drawTremoloSlideUnpitchVibratoNoteSustain(ctx, highwayViewProjectionMat, note, noteTime, Shader::sustainUniformViewProjection, Shader::sustainUniformModel);
    }
    else if (note.tremolo && note.slideUnpitchTo != -1)
    {
      ASSERT(note.maxBend == 0.0f);
      ASSERT(note.vibrato == 0);
      ASSERT(note.slideTo == -1);
      drawTremoloSlideUnpitchNoteSustain(ctx, highwayViewProjectionMat, note, noteTime, Shader::sustainUniformViewProjection, Shader::sustainUniformModel);
    }
    else if (note.tremolo && note.vibrato != 0)
    {
      ASSERT(note.maxBend == 0.0f);
      ASSERT(note.slideTo == -1);
      ASSERT(note.slideUnpitchTo == -1);
      drawTremoloVibratoNoteSustain(ctx, highwayViewProjectionMat, note, noteTime, chordBoxLeft, chordBoxWidth, Shader::sustainUniformViewProjection, Shader::sustainUniformModel);
    }
    else if (note.tremolo)
    {
      ASSERT(note.maxBend == 0.0f);
      ASSERT(note.vibrato == 0);
      ASSERT(note.slideTo == -1);
      ASSERT(note.slideUnpitchTo == -1);
      drawTremoloNoteSustain(ctx, highwayViewProjectionMat, note, noteTime, chordBoxLeft, chordBoxWidth, Shader::sustainUniformViewProjection, Shader::sustainUniformModel);
    }
    else if (note.vibrato != 0 && note.slideUnpitchTo != -1)
    {
      ASSERT(note.maxBend == 0.0f);
      ASSERT(note.tremolo == 0);
      ASSERT(note.slideTo == -1);
      drawVibratoSlideUnpitchNoteSustain(ctx, highwayViewProjectionMat, note, noteTime, Shader::sustainUniformViewProjection, Shader::sustainUniformModel);
    }
    else if (note.vibrato != 0 && note.maxBend != 0.0f)
    {
      ASSERT(note.tremolo == 0);
      ASSERT(note.slideTo == -1);
      ASSERT(note.slideUnpitchTo == -1);
      drawVibratoBendNoteSustain(ctx, highwayViewProjectionMat, note, noteTime, chordBoxLeft, chordBoxWidth, Shader::sustainUniformViewProjection, Shader::sustainUniformModel);
    }
    else if (note.vibrato != 0)
    {
      ASSERT(note.maxBend == 0.0f);
      ASSERT(note.tremolo == 0);
      ASSERT(note.slideTo == -1);
      ASSERT(note.slideUnpitchTo == -1);
      drawVibratoNoteSustain(ctx, highwayViewProjectionMat, note, noteTime, chordBoxLeft, chordBoxWidth, Shader::sustainUniformViewProjection, Shader::sustainUniformModel);
    }
    else if (note.maxBend != 0.0f)
    {
      ASSERT(note.tremolo == 0);
      ASSERT(note.vibrato == 0);
      ASSERT(note.slideTo == -1);
      ASSERT(note.slideUnpitchTo == -1);
      drawBendNoteSustain(ctx, highwayViewProjectionMat, note, chordBoxLeft, chordBoxWidth, Shader::sustainUniformViewProjection, Shader::sustainUniformModel);
    }
    else if (note.slideTo >= 1 && note.slideTo != note.fret)
    {
      ASSERT(note.maxBend == 0.0f);
      ASSERT(note.tremolo == 0);
      ASSERT(note.vibrato == 0);
      ASSERT(note.slideUnpitchTo == -1);
      drawSlideToNoteSustain(ctx, highwayViewProjectionMat, note, noteTime, Shader::sustainUniformViewProjection, Shader::sustainUniformModel);
    }
    else if (note.slideUnpitchTo != -1)
    {
      ASSERT(note.maxBend == 0.0f);
      ASSERT(note.tremolo == 0);
      ASSERT(note.vibrato == 0);
      ASSERT(note.slideTo == -1);
      drawUnpitchNoteSustain(ctx, highwayViewProjectionMat, note, noteTime, Shader::sustainUniformViewProjection, Shader::sustainUniformModel);
    }
    else
    {
      ASSERT(note.maxBend == 0.0f);
      ASSERT(note.tremolo == 0);
      ASSERT(note.vibrato == 0);
      ASSERT(note.slideTo == -1);
      ASSERT(note.slideUnpitchTo == -1);
      const f32 y = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
      drawNoteSustain(ctx, y, highwayViewProjectionMat, note.fret, note.sustainNS, noteTime, chordBoxLeft, chordBoxWidth, Shader::sustainUniformViewProjection, Shader::sustainUniformModel);
    }
  }
}

static void drawNoteStand(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, TimeNS noteTime)
{
  GL(glUseProgram(Shader::noteStand));

  GL(glUniform1f(Shader::noteStandUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::noteStandUniformFadeFarDistance, Settings::highwayFadeFarDistance));

  const f32 y = -0.735f * *ctx.instrumentStringSpacing;

  const mat4 model = {
    .m11 = 2.0f * f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing - 2.0f * y,
    .m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]),
    .m13 = y,
    .m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed,
  };

  GL(glUniformMatrix4fv(Shader::noteStandUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniformMatrix4fv(Shader::noteStandUniformModel, 1, GL_FALSE, &model.m00));

  setStringColor(ctx, Shader::noteStandUniformColor, note.string + ctx.instrumentStringOffset + ctx.instrumentHideFirstStrings, 1.0f);

  GL(glDrawElements_(noteStand));
}

static void drawNoteStandZero(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, TimeNS noteTime, i32 chordBoxLeft, i32 chordBoxWidth)
{
  GL(glUseProgram(Shader::noteStandZero));

  GL(glUniform1f(Shader::noteStandZeroUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::noteStandZeroUniformFadeFarDistance, Settings::highwayFadeFarDistance));

  const f32 left = ctx.instrumentFretPosition[chordBoxLeft - 1];
  const f32 top = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
  const f32 right = ctx.instrumentFretPosition[chordBoxLeft + chordBoxWidth - 1];
  const f32 bottom = -0.60f * *ctx.instrumentStringSpacing;

  {
    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 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed
    };

    GL(glUniformMatrix4fv(Shader::noteStandZeroUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
    GL(glUniformMatrix4fv(Shader::noteStandZeroUniformModel, 1, GL_FALSE, &model.m00));
  }

  setStringColor(ctx, Shader::noteStandZeroUniformColor, note.string + ctx.instrumentStringOffset + ctx.instrumentHideFirstStrings, 1.0f);

  GL(glDrawElements_(planeZ));
}

static void drawNotes(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, f32 fretboardNearestNoteDistance[Const::highwayInstrumentGuitarStringCount][25], i32 currentAnchorIndex, i32 noteBeginIndex, i32 noteEndIndex, i32 sustainBeginIndex, i32 sustainEndIndex)
{
  for (i32 i = noteEndIndex; i >= noteBeginIndex; --i)
  {
    const Song::Note& note = Global::songTrackLevelAdjusted[selectedArrangementIndex].notes[i];

    const TimeNS noteTime = -note.timeNS + Global::musicTimeElapsedNS;

    if (noteTime - note.sustainNS > 0)
      continue;
    if (noteTime < timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
      continue;

    const Song::Anchor& anchor = findAnchorByTime(note.timeNS, selectedArrangementIndex, currentAnchorIndex);

    drawNote(ctx, highwayViewProjectionMat, note, noteTime, fretboardNearestNoteDistance, anchor.fret, anchor.width);

    if (noteTime < 0)
    {
      bool drawStand = true; // skip note stand draw on Arpeggios. It causes ZFighting.

      for (i32 j = sustainEndIndex; j >= sustainBeginIndex; --j)
      {
        const Song::Sustain& sustain = Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains[j];
        if (sustain.startTimeNS == note.timeNS)
        {
          drawStand = false;
          break;
        }
      }

      if (note.fret >= 1)  // Draw Fret Numbers for Chord
      {
        if (drawStand && Settings::highwayNoteStand && !note.isLinked)
        {
          drawNoteStand(ctx, highwayViewProjectionMat, note, noteTime);
        }

        bool highDensityNote = false;
        if (i >= 1)
        {
          const Song::Note& prevNote = Global::songTrackLevelAdjusted[selectedArrangementIndex].notes[i - 1];
          if (note.fret == prevNote.fret)
          {
            highDensityNote = !(TimeNS_To_Seconds(note.timeNS - prevNote.timeNS) > Const::highwayDrawHighDensityNoteFretNumberSkipDuration);
          }
        }

        if (!highDensityNote)
        {
#ifdef SHR3D_FONT_BITMAP
          GL(glUseProgram(Shader::fontWorldFadeFarAndNear));
          GL(glUniformMatrix4fv(Shader::fontWorldFadeFarAndNearUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
          GL(glUniform1f(Shader::fontWorldFadeFarAndNearUniformViewDistance, Settings::highwayViewDistance));
          GL(glUniform1f(Shader::fontWorldFadeFarAndNearUniformFadeFarDistance, Settings::highwayFadeFarDistance));
          GL(glUniform1f(Shader::fontWorldFadeFarAndNearUniformFadeNearDistance, Settings::highwayFadeNearDistance));
          GL(glUniform1f(Shader::fontWorldFadeFarAndNearUniformFadeNearStrength, Settings::highwayFadeNearStrength));
          GL(glUniform4f(Shader::fontWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[0].r, Settings::highwayFretboardFretNumberColor[0].g, Settings::highwayFretboardFretNumberColor[0].b, Settings::highwayFretboardFretNumberColor[0].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
          GL(glUseProgram(Shader::fontMSDFWorldFadeFarAndNear));
          GL(glUniformMatrix4fv(Shader::fontMSDFWorldFadeFarAndNearUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
          GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformViewDistance, Settings::highwayViewDistance));
          GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformFadeFarDistance, Settings::highwayFadeFarDistance));
          GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformFadeNearDistance, Settings::highwayFadeNearDistance));
          GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformFadeNearStrength, Settings::highwayFadeNearStrength));
          GL(glUniform4f(Shader::fontMSDFWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[0].r, Settings::highwayFretboardFretNumberColor[0].g, Settings::highwayFretboardFretNumberColor[0].b, Settings::highwayFretboardFretNumberColor[0].a));
          GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

          {
            const mat4 model = {
              .m00 = 0.25f,
              .m11 = 0.25f,
              .m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]),
              .m13 = -0.2f,
              .m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed + 0.2f
            };

#ifdef SHR3D_FONT_BITMAP
            GL(glUniformMatrix4fv(Shader::fontWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
            GL(glUniformMatrix4fv(Shader::fontMSDFWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_MSDF
          }

          GL(glBindTexture(GL_TEXTURE_2D, Font1::fretNumberTextureCache[note.fret]));
          GL(glDrawElements_(planeZFlippedV));
          GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
        }
      }
      else
      {
        if (drawStand && Settings::highwayNoteStandZero)
        {
          drawNoteStandZero(ctx, highwayViewProjectionMat, note, noteTime, anchor.fret, anchor.width);
        }
      }
    }
  }
}

static void drawGroundAnchor(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Anchor& anchor, const TimeNS noteTimeBegin, const TimeNS noteTimeEnd)
{
  const f32 front = min_(TimeNS_To_Seconds(noteTimeBegin) * Settings::highwayScrollSpeed, 0.0f);
  const f32 back = TimeNS_To_Seconds(noteTimeEnd) * Settings::highwayScrollSpeed;

  for (i32 i = 0; i < anchor.width; ++i)
  {
    const f32 left = ctx.instrumentFretPosition[anchor.fret - 1 + i];
    const f32 right = ctx.instrumentFretPosition[anchor.fret + i];

    if (Const::hasFretDotInlay[anchor.fret + i])
    {
      GL(glUniform4f(Shader::groundAnchorUniformColor, Settings::highwayAnchorColor[0].r, Settings::highwayAnchorColor[0].g, Settings::highwayAnchorColor[0].b, Settings::highwayAnchorColor[0].a));
      GL(glUniform4f(Shader::groundAnchorUniformColor2, Settings::highwayAnchorColor[2].r, Settings::highwayAnchorColor[2].g, Settings::highwayAnchorColor[2].b, Settings::highwayAnchorColor[2].a));
    }
    else
    {
      GL(glUniform4f(Shader::groundAnchorUniformColor, Settings::highwayAnchorColor[1].r, Settings::highwayAnchorColor[1].g, Settings::highwayAnchorColor[1].b, Settings::highwayAnchorColor[1].a));
      GL(glUniform4f(Shader::groundAnchorUniformColor2, Settings::highwayAnchorColor[3].r, Settings::highwayAnchorColor[3].g, Settings::highwayAnchorColor[3].b, Settings::highwayAnchorColor[3].a));
    }

    GL(glUniform1f(Shader::groundAnchorUniformColorExponent, Settings::highwayAnchorColorExponent));

    const mat4 model = {
      .m00 = 0.5f * (right - left),
      .m22 = 0.5f * (front - back),
      .m03 = left + 0.5f * (right - left),
      .m13 = -0.37f,
      .m23 = back + 0.5f * (front - back)
    };

    GL(glUniformMatrix4fv(Shader::groundAnchorUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
    GL(glUniformMatrix4fv(Shader::groundAnchorUniformModel, 1, GL_FALSE, &model.m00));

    GL(glDrawElements_(planeY));
  }
}

static void drawGroundAnchors(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, i32 currentAnchorIndex)
{
  GL(glUseProgram(Shader::groundAnchor));

  GL(glUniform1f(Shader::groundAnchorUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::groundAnchorUniformFadeFarDistance, Settings::highwayFadeFarDistance));
  GL(glUniform1f(Shader::groundAnchorUniformFadeNearDistance, Settings::highwayFadeNearDistance));
  GL(glUniform1f(Shader::groundAnchorUniformFadeNearStrength, Settings::highwayFadeNearStrength));

  for (i32 i = currentAnchorIndex; i < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors.size()); ++i)
  {
    const Song::Anchor& anchor0 = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[i];
    const TimeNS noteTimeBegin = -anchor0.timeNS + Global::musicTimeElapsedNS;

    if (noteTimeBegin < timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
      break;

    const TimeNS noteTimeEnd = -getAnchorTimeEndNS(selectedArrangementIndex, i) + Global::musicTimeElapsedNS;

    drawGroundAnchor(ctx, highwayViewProjectionMat, anchor0, noteTimeBegin, noteTimeEnd);
  }
}

static void drawChordName(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, f32 noteTime, i32 chordBoxLeft, bool highDensity, const char* chordName)
{
  ASSERT(Settings::highwayChordName);

  if (chordName == nullptr)
    return;

  f32 alpha = highDensity ? 0.0f : 1.0f;

  if (noteTime > 0.0f)
  {
    if (noteTime < Const::highwayDrawChordNameWaitDuration)
    {
      alpha = 1.0f;
      noteTime = 0.0f;
    }
    else
    {
      alpha = (Const::highwayDrawChordNameEndDuration - noteTime) / (Const::highwayDrawChordNameEndDuration - Const::highwayDrawChordNameFadeOutDuration);
      noteTime -= Const::highwayDrawChordNameWaitDuration;
    }
  }

  ASSERT(alpha >= 0.0f);
  ASSERT(alpha <= 1.0f);

  if (alpha <= 0.0f)
    return;

  alpha *= Settings::highwayChordNameColor.a;

#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontWorldFadeFar));
  GL(glUniform1f(Shader::fontWorldFadeFarUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::fontWorldFadeFarUniformFadeFarDistance, Settings::highwayFadeFarDistance));
  GL(glUniformMatrix4fv(Shader::fontWorldFadeFarUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniform4f(Shader::fontWorldFadeFarUniformColor, Settings::highwayChordNameColor.r, Settings::highwayChordNameColor.g, Settings::highwayChordNameColor.b, alpha));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFWorldFadeFar));
  GL(glUniform1f(Shader::fontMSDFWorldFadeFarUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::fontMSDFWorldFadeFarUniformFadeFarDistance, Settings::highwayFadeFarDistance));
  GL(glUniformMatrix4fv(Shader::fontMSDFWorldFadeFarUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniform4f(Shader::fontMSDFWorldFadeFarUniformColor, Settings::highwayChordNameColor.r, Settings::highwayChordNameColor.g, Settings::highwayChordNameColor.b, alpha));
  GL(glUniform1f(Shader::fontMSDFWorldFadeFarUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

  {
    const mat4 model = {
      .m00 = 0.5f,
      .m11 = 0.5f,
      .m03 = ctx.instrumentFretPosition[chordBoxLeft] - 1.5f,
      .m13 = f32(ctx.instrumentStringCount) * *ctx.instrumentStringSpacing,
      .m23 = noteTime * Settings::highwayScrollSpeed
    };

#ifdef SHR3D_FONT_BITMAP
    GL(glUniformMatrix4fv(Shader::fontWorldFadeFarUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
    GL(glUniformMatrix4fv(Shader::fontMSDFWorldFadeFarUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_MSDF

  }

  GL(glBindTexture(GL_TEXTURE_2D, Font1::textTexture(chordName)));

  GL(glDrawElements_(planeZFlippedV));

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

static void drawChord(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, const Song::Chord& chord, TimeNS noteTime, f32 fretboardNearestNoteDistance[Const::highwayInstrumentGuitarStringCount][25], i32 currentAnchorIndex, bool highDensityFixed)
{
  u32 fretsInChord = 0;
  for (const Song::Note& note : chord.chordNotes)
  {
    fretsInChord |= 1 << note.fret;
  }

  for (; currentAnchorIndex < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors.size()); ++currentAnchorIndex)
  {
    const TimeNS anchorNoteTimeEnd = -getAnchorTimeEndNS(selectedArrangementIndex, currentAnchorIndex) + Global::musicTimeElapsedNS;

    if (anchorNoteTimeEnd < noteTime)
      break;
  }

  const i32 chordBoxLeft = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].fret;
  const i32 chordBoxRight = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].fret + Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].width;

  if (!highDensityFixed)
  {
    for (i32 i = i32(chord.chordNotes.size()) - 1; i >= 0; --i)
    {
      const Song::Note& note = chord.chordNotes[i];

      drawNote(ctx, highwayViewProjectionMat, note, noteTime, fretboardNearestNoteDistance, chordBoxLeft, chordBoxRight - chordBoxLeft);
    }

    if (noteTime < 0)
    {
      if (Settings::highwayChordBox)
      { // draw ChordBox
        const f32 left = ctx.instrumentFretPosition[chordBoxLeft - 1];
        const f32 top = f32(ctx.instrumentStringCount) * *ctx.instrumentStringSpacing - 0.40f * *ctx.instrumentStringSpacing;
        const f32 right = ctx.instrumentFretPosition[chordBoxRight - 1];
        const f32 bottom = -0.60f * *ctx.instrumentStringSpacing;

        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 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed
        };

        if (chord.chordNotes.size() <= 2)
        {
          GL(glUseProgram(Shader::chordBoxDoubleStop));
          GL(glUniform1f(Shader::chordBoxDoubleStopUniformViewDistance, Settings::highwayViewDistance));
          GL(glUniform1f(Shader::chordBoxDoubleStopUniformFadeFarDistance, Settings::highwayFadeFarDistance));
          GL(glUniform4f(Shader::chordBoxDoubleStopUniformColor, Settings::highwayChordBoxColor.r, Settings::highwayChordBoxColor.g, Settings::highwayChordBoxColor.b, Settings::highwayChordBoxColor.a));
          GL(glUniformMatrix4fv(Shader::chordBoxDoubleStopUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
          GL(glUniformMatrix4fv(Shader::chordBoxDoubleStopUniformModel, 1, GL_FALSE, &model.m00));
        }
        else
        {
          GL(glUseProgram(Shader::chordBox));
          GL(glUniform1f(Shader::chordBoxUniformViewDistance, Settings::highwayViewDistance));
          GL(glUniform1f(Shader::chordBoxUniformFadeFarDistance, Settings::highwayFadeFarDistance));
          GL(glUniform4f(Shader::chordBoxUniformColor, Settings::highwayChordBoxColor.r, Settings::highwayChordBoxColor.g, Settings::highwayChordBoxColor.b, Settings::highwayChordBoxColor.a));
          GL(glUniformMatrix4fv(Shader::chordBoxUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
          GL(glUniformMatrix4fv(Shader::chordBoxUniformModel, 1, GL_FALSE, &model.m00));
        }

        GL(glDrawElements_(planeZ));
      }

      if (Settings::highwayChordFretNumbers)
      { // Draw Fret Numbers for Chord
#ifdef SHR3D_FONT_BITMAP
        GL(glUseProgram(Shader::fontWorldFadeFarAndNear));
        GL(glUniformMatrix4fv(Shader::fontWorldFadeFarAndNearUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
        GL(glUniform1f(Shader::fontWorldFadeFarAndNearUniformViewDistance, Settings::highwayViewDistance));
        GL(glUniform1f(Shader::fontWorldFadeFarAndNearUniformFadeFarDistance, Settings::highwayFadeFarDistance));
        GL(glUniform1f(Shader::fontWorldFadeFarAndNearUniformFadeNearDistance, Settings::highwayFadeNearDistance));
        GL(glUniform1f(Shader::fontWorldFadeFarAndNearUniformFadeNearStrength, Settings::highwayFadeNearStrength));
        GL(glUniform4f(Shader::fontWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[0].r, Settings::highwayFretboardFretNumberColor[0].g, Settings::highwayFretboardFretNumberColor[0].b, Settings::highwayFretboardFretNumberColor[0].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
        GL(glUseProgram(Shader::fontMSDFWorldFadeFarAndNear));
        GL(glUniformMatrix4fv(Shader::fontMSDFWorldFadeFarAndNearUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
        GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformViewDistance, Settings::highwayViewDistance));
        GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformFadeFarDistance, Settings::highwayFadeFarDistance));
        GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformFadeNearDistance, Settings::highwayFadeNearDistance));
        GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformFadeNearStrength, Settings::highwayFadeNearStrength));
        GL(glUniform4f(Shader::fontMSDFWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[0].r, Settings::highwayFretboardFretNumberColor[0].g, Settings::highwayFretboardFretNumberColor[0].b, Settings::highwayFretboardFretNumberColor[0].a));
        GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

        for (i32 i = 1; i < 24; ++i)
        {
          if (fretsInChord & (1 << i))
          {
            {
              const mat4 model = {
                .m00 = 0.25f,
                .m11 = 0.25f,
                .m03 = ctx.instrumentFretPosition[i - 1] + 0.5f * (ctx.instrumentFretPosition[i] - ctx.instrumentFretPosition[i - 1]),
                .m13 = -0.2f,
                .m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed + 0.2f
              };

#ifdef SHR3D_FONT_BITMAP
              GL(glUniformMatrix4fv(Shader::fontWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
              GL(glUniformMatrix4fv(Shader::fontMSDFWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_MSDF
            }

            GL(glBindTexture(GL_TEXTURE_2D, Font1::fretNumberTextureCache[i]));
            GL(glDrawElements_(planeZFlippedV));
          }
        }
        GL(glBindTexture(GL_TEXTURE_2D, Global::texture));
      }
    }
  }
  else
  {
    if (noteTime < 0)
    { // draw highDensity ChordBox
      for (i32 i = i32(chord.chordNotes.size()) - 1; i >= 0; --i)
      {
        const Song::Note& note = chord.chordNotes[i];
        setFretboardNearestNoteDistance(note, noteTime, fretboardNearestNoteDistance);
      }

      const f32 left = ctx.instrumentFretPosition[chordBoxLeft - 1];
      const f32 top = 0.5f * (f32(ctx.instrumentStringCount) * *ctx.instrumentStringSpacing - 0.40f * *ctx.instrumentStringSpacing);
      const f32 right = ctx.instrumentFretPosition[chordBoxRight - 1];
      const f32 bottom = -0.60f * *ctx.instrumentStringSpacing;

      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 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed
      };

      if (chord.fretHandMute)
      {
        GL(glUseProgram(Shader::chordBoxFretMute));
        GL(glUniform1f(Shader::chordBoxFretMuteUniformViewDistance, Settings::highwayViewDistance));
        GL(glUniform1f(Shader::chordBoxFretMuteUniformFadeFarDistance, Settings::highwayFadeFarDistance));
        GL(glUniform4f(Shader::chordBoxFretMuteUniformColor, Settings::highwayChordBoxColor.r, Settings::highwayChordBoxColor.g, Settings::highwayChordBoxColor.b, Settings::highwayChordBoxColor.a));
        GL(glUniformMatrix4fv(Shader::chordBoxFretMuteUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
        GL(glUniformMatrix4fv(Shader::chordBoxFretMuteUniformModel, 1, GL_FALSE, &model.m00));
      }
      else if (chord.palmMute)
      {
        GL(glUseProgram(Shader::chordBoxPalmMute));
        GL(glUniform1f(Shader::chordBoxPalmMuteUniformViewDistance, Settings::highwayViewDistance));
        GL(glUniform1f(Shader::chordBoxPalmMuteUniformFadeFarDistance, Settings::highwayFadeFarDistance));
        GL(glUniform4f(Shader::chordBoxPalmMuteUniformColor, Settings::highwayChordBoxColor.r, Settings::highwayChordBoxColor.g, Settings::highwayChordBoxColor.b, Settings::highwayChordBoxColor.a));
        GL(glUniformMatrix4fv(Shader::chordBoxPalmMuteUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
        GL(glUniformMatrix4fv(Shader::chordBoxPalmMuteUniformModel, 1, GL_FALSE, &model.m00));
      }
      else if (chord.chordNotes.size() <= 2)
      {
        GL(glUseProgram(Shader::chordBoxHighDensityDoubleStop));
        GL(glUniform1f(Shader::chordBoxHighDensityDoubleStopUniformViewDistance, Settings::highwayViewDistance));
        GL(glUniform1f(Shader::chordBoxHighDensityDoubleStopUniformFadeFarDistance, Settings::highwayFadeFarDistance));
        GL(glUniform4f(Shader::chordBoxHighDensityDoubleStopUniformColor, Settings::highwayChordBoxColor.r, Settings::highwayChordBoxColor.g, Settings::highwayChordBoxColor.b, Settings::highwayChordBoxColor.a));
        GL(glUniformMatrix4fv(Shader::chordBoxHighDensityDoubleStopUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
        GL(glUniformMatrix4fv(Shader::chordBoxHighDensityDoubleStopUniformModel, 1, GL_FALSE, &model.m00));
      }
      else
      {
        GL(glUseProgram(Shader::chordBoxHighDensity));
        GL(glUniform1f(Shader::chordBoxHighDensityUniformViewDistance, Settings::highwayViewDistance));
        GL(glUniform1f(Shader::chordBoxHighDensityUniformFadeFarDistance, Settings::highwayFadeFarDistance));
        GL(glUniform4f(Shader::chordBoxHighDensityUniformColor, Settings::highwayChordBoxColor.r, Settings::highwayChordBoxColor.g, Settings::highwayChordBoxColor.b, Settings::highwayChordBoxColor.a));
        GL(glUniformMatrix4fv(Shader::chordBoxHighDensityUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
        GL(glUniformMatrix4fv(Shader::chordBoxHighDensityUniformModel, 1, GL_FALSE, &model.m00));
      }

      GL(glDrawElements_(planeZ));
    }
  }

  if (Settings::highwayChordName)
  {
    drawChordName(ctx, highwayViewProjectionMat, TimeNS_To_Seconds(noteTime), chordBoxLeft, highDensityFixed, chord.name.size() == 0 ? nullptr : chord.name.c_str());
  }
}

static void drawFretboardChordFingerPositions(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, const Song::Chord& chord)
{
#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontWorld));
  GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniform4f(Shader::fontWorldUniformColor, Settings::highwayFingerNumberColor.r, Settings::highwayFingerNumberColor.g, Settings::highwayFingerNumberColor.b, Settings::highwayFingerNumberColor.a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFWorld));
  GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniform4f(Shader::fontMSDFWorldUniformColor, Settings::highwayFingerNumberColor.r, Settings::highwayFingerNumberColor.g, Settings::highwayFingerNumberColor.b, Settings::highwayFingerNumberColor.a));
  GL(glUniform1f(Shader::fontMSDFWorldUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

  mat4 model = {
    .m00 = 0.2f,
    .m11 = 0.2f,
    .m23 = 0.1f
  };

  if (chord.chordId_deprecated >= 0)
  {
    ASSERT(chord.chordId_deprecated < i32(Global::songTracks[selectedArrangementIndex].chordTemplates_deprecated.size()));

    const Song::ChordTemplate_deprecated& chordTemplate = Global::songTracks[selectedArrangementIndex].chordTemplates_deprecated[chord.chordId_deprecated];

    for (i32 i = 0; i < 6; ++i)
    {
      if (chordTemplate.fret[i] <= 0)
        continue;
      if (chordTemplate.finger[i] <= 0)
        continue;

      model.m03 = ctx.instrumentFretPosition[chordTemplate.fret[i] - 1] + 0.5f * (ctx.instrumentFretPosition[chordTemplate.fret[i]] - ctx.instrumentFretPosition[chordTemplate.fret[i] - 1]);
      model.m13 = f32(reverseStringFix(ctx, i + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;

      {
        const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
        GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
        GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF
      }

      GL(glBindTexture(GL_TEXTURE_2D, Font1::fretNumberTextureCache[chordTemplate.finger[i]]));

      GL(glDrawElements_(planeZFlippedV));
    }
  }
  else
  {
    for (const Song::Note& note : chord.chordNotes)
    {
      if (note.fret == 0)
        continue;
      if (note.finger <= 0)
        continue;

      model.m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);
      model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;

      {
        const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
        GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
        GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF
      }

      GL(glBindTexture(GL_TEXTURE_2D, Font1::fretNumberTextureCache[note.finger]));

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

static void drawArpeggioNoteElements(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const Song::Note& note, TimeNS noteTime, i32 chordBoxLeft, i32 chordBoxWidth)
{
  GL(glUseProgram(Shader::highwayFadeFar));

  GL(glUniform1f(Shader::highwayFadeFarUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::highwayFadeFarUniformFadeFarDistance, Settings::highwayFadeFarDistance));

  setStringColor(ctx, Shader::highwayFadeFarUniformColor, note.string + ctx.instrumentStringOffset + ctx.instrumentHideFirstStrings, 1.0f);

  if (note.fret == 0)
  {
    const f32 zeroIndentionWidth = 0.05f;
    const f32 noteZeroLeftWidth = 0.350633f; // this is from the 3D model. Not a good idea

    {
      mat4 model;
      model.m03 = ctx.instrumentFretPosition[chordBoxLeft - 1] + zeroIndentionWidth;
      model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
      model.m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

      GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
      GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformModel, 1, GL_FALSE, &model.m00));

      GL(glDrawElements_(noteZeroArpeggioLeft));
    }

    {
      mat4 model;
      model.m03 = ctx.instrumentFretPosition[chordBoxLeft - 1 + chordBoxWidth] - zeroIndentionWidth - noteZeroLeftWidth;
      model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
      model.m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

      GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
      GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformModel, 1, GL_FALSE, &model.m00));

      GL(glDrawElements_(noteZeroArpeggioRight));
    }
  }
  else
  {
    const f32 x = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]);

    mat4 model;
    model.m03 = x;
    model.m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
    model.m23 = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

    GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
    GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformModel, 1, GL_FALSE, &model.m00));

    GL(glDrawElements_(noteArpeggio));
  }
}

static void drawArpeggio(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, const Song::Arpeggio& arpeggio, i32 noteBeginIndex, i32 noteEndIndex, TimeNS noteTimeBegin, TimeNS noteTimeEnd, i32 currentAnchorIndex)
{
  ASSERT(Settings::highwayChordBoxArpeggio);

  for (; currentAnchorIndex < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors.size()); ++currentAnchorIndex)
  {
    const TimeNS anchorNoteTimeEnd = -getAnchorTimeEndNS(selectedArrangementIndex, currentAnchorIndex) + Global::musicTimeElapsedNS;

    if (anchorNoteTimeEnd < noteTimeBegin)
      break;
  }

  // Workaround for when anchor is shorter than noteTimeEnd. Happens on Deep Purple - Soldier of Fortune on the anchor[8]
  if (noteTimeEnd < -getAnchorTimeEndNS(selectedArrangementIndex, currentAnchorIndex) + Global::musicTimeElapsedNS)
  {
    if (currentAnchorIndex >= 1 && -getAnchorTimeEndNS(selectedArrangementIndex, currentAnchorIndex - 1) + Global::musicTimeElapsedNS < noteTimeBegin)
      return;
    noteTimeEnd = -getAnchorTimeEndNS(selectedArrangementIndex, currentAnchorIndex) + Global::musicTimeElapsedNS;
  }

  const i32 chordBoxLeft = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].fret;
  const i32 chordBoxRight = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].fret + Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].width;

  i32 notesInsideArpeggio = 0;
  for (i32 i = noteBeginIndex; i < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].notes.size()); ++i) // noteEndIndex is not sufficent here since arpeggio notes might not be not drawn yet.
  {
    const Song::Note& note = Global::songTrackLevelAdjusted[selectedArrangementIndex].notes[i];
    if (note.timeNS >= arpeggio.startTimeNS)
    {
      if (note.timeNS <= arpeggio.endTimeNS)
        ++notesInsideArpeggio;
      else
        break;
    }
  }

  if (notesInsideArpeggio >= 2)
  {
    for (i32 i = noteBeginIndex; i <= noteEndIndex; ++i)
    {
      const Song::Note& note = Global::songTrackLevelAdjusted[selectedArrangementIndex].notes[i];
      if (note.timeNS >= arpeggio.startTimeNS && note.timeNS <= arpeggio.endTimeNS)
      {
        if (noteTimeBegin <= 0)
          drawArpeggioNoteElements(ctx, highwayViewProjectionMat, note, noteTimeBegin, chordBoxLeft, chordBoxRight - chordBoxLeft);
      }
    }


    { // draw arpeggio chord box
      GL(glUseProgram(Shader::chordBoxArpeggio));
      GL(glUniform1f(Shader::chordBoxArpeggioUniformViewDistance, Settings::highwayViewDistance));
      GL(glUniform1f(Shader::chordBoxArpeggioUniformFadeFarDistance, Settings::highwayFadeFarDistance));
      GL(glUniform4f(Shader::chordBoxArpeggioUniformColor, Settings::highwayChordBoxArpeggioColor.r, Settings::highwayChordBoxArpeggioColor.g, Settings::highwayChordBoxArpeggioColor.b, Settings::highwayChordBoxArpeggioColor.a));

      const f32 left = ctx.instrumentFretPosition[chordBoxLeft - 1];
      const f32 top = f32(ctx.instrumentStringCount) * *ctx.instrumentStringSpacing - 0.40f * *ctx.instrumentStringSpacing;
      const f32 right = ctx.instrumentFretPosition[chordBoxRight - 1];
      const f32 bottom = -0.60f * *ctx.instrumentStringSpacing;

      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 = TimeNS_To_Seconds(noteTimeBegin) * Settings::highwayScrollSpeed
      };

      GL(glUniformMatrix4fv(Shader::chordBoxArpeggioUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
      GL(glUniformMatrix4fv(Shader::chordBoxArpeggioUniformModel, 1, GL_FALSE, &model.m00));

      GL(glDrawElements_(planeZ));

      if (Settings::highwayChordName)
      {
        drawChordName(ctx, highwayViewProjectionMat, TimeNS_To_Seconds(noteTimeBegin), chordBoxLeft, false, arpeggio.name.c_str());
      }
    }
  }
}

static void drawGroundSustain(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, i32 currentAnchorIndex, TimeNS noteTimeBegin, TimeNS noteTimeEnd)
{
  //i32 oldAnchor = currentAnchorIndex;

  for (; currentAnchorIndex < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors.size()); ++currentAnchorIndex)
  {
    const TimeNS anchorNoteTimeEnd = -getAnchorTimeEndNS(selectedArrangementIndex, currentAnchorIndex) + Global::musicTimeElapsedNS;

    if (anchorNoteTimeEnd < noteTimeBegin)
      break;
  }

  // Workaround for when anchor is shorter than noteTimeEnd. Happens on Deep Purple - Soldier of Fortune on the anchor[8]
  if (noteTimeEnd < -getAnchorTimeEndNS(selectedArrangementIndex, currentAnchorIndex) + Global::musicTimeElapsedNS)
  {
    if (currentAnchorIndex >= 1 && -getAnchorTimeEndNS(selectedArrangementIndex, currentAnchorIndex - 1) + Global::musicTimeElapsedNS < noteTimeBegin)
    {
      ASSERT(false); // TODO: This early return should not get hit anymore. remove the inner if.
      return;
    }
    noteTimeEnd = -getAnchorTimeEndNS(selectedArrangementIndex, currentAnchorIndex) + Global::musicTimeElapsedNS;
  }

  const i32 chordBoxLeft = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].fret;
  const i32 chordBoxRight = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].fret + Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].width;

  { // drawGroundSustain
    const f32 left = ctx.instrumentFretPosition[chordBoxLeft - 1] - 0.1f;
    const f32 right = ctx.instrumentFretPosition[chordBoxRight - 1] + 0.1f;
    const f32 front = min_(TimeNS_To_Seconds(noteTimeBegin) * Settings::highwayScrollSpeed, 0.0f);
    const f32 back = TimeNS_To_Seconds(noteTimeEnd) * Settings::highwayScrollSpeed;

    GL(glUseProgram(Shader::groundSustain));

    GL(glUniform1f(Shader::groundSustainUniformViewDistance, Settings::highwayViewDistance));
    GL(glUniform1f(Shader::groundSustainUniformFadeFarDistance, Settings::highwayFadeFarDistance));
    GL(glUniform1f(Shader::groundSustainUniformFadeNearDistance, Settings::highwayFadeNearDistance));
    GL(glUniform1f(Shader::groundSustainUniformFadeNearStrength, Settings::highwayFadeNearStrength));
    GL(glUniform4f(Shader::groundSustainUniformColor, Settings::highwaySustainColor.r, Settings::highwaySustainColor.g, Settings::highwaySustainColor.b, Settings::highwaySustainColor.a));
    GL(glUniformMatrix4fv(Shader::groundSustainUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));

    {
      const mat4 model = {
        .m00 = 0.5f * (right - left),
        .m22 = 0.5f * (front - back),
        .m03 = left + 0.5f * (right - left),
        .m13 = -0.35f,
        .m23 = back + 0.5f * (front - back)
      };
      GL(glUniformMatrix4fv(Shader::groundSustainUniformModel, 1, GL_FALSE, &model.m00));
    }

    GL(glDrawElements_(planeY));
  }
}

static void drawArpeggioGroundSustain(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, i32 currentAnchorIndex, i32 arpeggioBeginIndex, i32 arpeggioEndIndex)
{
  for (i32 i = arpeggioEndIndex; i >= arpeggioBeginIndex; --i)
  {
    const Song::Arpeggio& arpeggio = Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios[i];

    const TimeNS noteTimeBegin = -arpeggio.startTimeNS + Global::musicTimeElapsedNS;
    const TimeNS noteTimeEnd = -arpeggio.endTimeNS + Global::musicTimeElapsedNS;

    if (noteTimeEnd > 0)
      continue;

    // in case the handshape is longer than the anchor, don't snap to next anchor. skip it.
    if (noteTimeBegin > 0 && Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].timeNS > arpeggio.startTimeNS)
      continue;

    drawGroundSustain(ctx, selectedArrangementIndex, highwayViewProjectionMat, currentAnchorIndex, noteTimeBegin, noteTimeEnd);
  }
}

static void drawChordGroundSustain(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, i32 currentAnchorIndex, i32 arpeggioBeginIndex, i32 arpeggioEndIndex)
{
  for (i32 i = arpeggioEndIndex; i >= arpeggioBeginIndex; --i)
  {
    const Song::Sustain& sustain = Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains[i];

    const TimeNS noteTimeBegin = -sustain.startTimeNS + Global::musicTimeElapsedNS;
    const TimeNS noteTimeEnd = -sustain.endTimeNS + Global::musicTimeElapsedNS;

    if (noteTimeEnd > 0)
      continue;

    // in case the handshape is longer than the anchor, don't snap to next anchor. skip it.
    if (noteTimeBegin > 0 && Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].timeNS > sustain.startTimeNS)
      continue;

    drawGroundSustain(ctx, selectedArrangementIndex, highwayViewProjectionMat, currentAnchorIndex, noteTimeBegin, noteTimeEnd);
  }
}

static void drawChordsAndArpeggios(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, f32 fretboardNearestNoteDistance[Const::highwayInstrumentGuitarStringCount][25], i32 currentAnchorIndex, i32 sustainBeginIndex, i32 sustainEndIndex, i32 noteBeginIndex, i32 noteEndIndex, i32 chordBeginIndex, i32 chordEndIndex, i32 arpeggioBeginIndex, i32 arpeggioEndIndex)
{
  // Handshapes can draw Arpeggio ChordBoxes. They need to be drawn in from back to front together with normal ChordBoxes to not cause problems because of transparency
  if (sustainBeginIndex == -1 && chordBeginIndex == -1)
    return;

  i32 handShapIndex = sustainEndIndex;
  i32 chordIndex = chordEndIndex;
  i32 arpeggioIndex = arpeggioEndIndex;

  while (chordIndex >= chordBeginIndex || arpeggioIndex >= arpeggioBeginIndex)
  {
    const Song::Chord* chord = nullptr;
    const Song::Arpeggio* arpeggio = nullptr;

    if (chordIndex >= 0)
      chord = &Global::songTrackLevelAdjusted[selectedArrangementIndex].chords[chordIndex];
    if (arpeggioIndex >= 0)
      arpeggio = &Global::songTrackLevelAdjusted[selectedArrangementIndex].arpeggios[arpeggioIndex];

    const bool nextDrawIsSustain = (chord == nullptr) || (arpeggio != nullptr && arpeggio->startTimeNS > chord->timeNS);

    if (nextDrawIsSustain)
    {
      if (arpeggio != nullptr)
      {
        if (Settings::highwayChordBoxArpeggio)
        {
          const TimeNS noteTimeBegin = -arpeggio->startTimeNS + Global::musicTimeElapsedNS;
          const TimeNS noteTimeEnd = -arpeggio->endTimeNS + Global::musicTimeElapsedNS;

          if (noteTimeBegin < 0)
          {
            drawArpeggio(ctx, selectedArrangementIndex, highwayViewProjectionMat, *arpeggio, noteBeginIndex, noteEndIndex, noteTimeBegin, noteTimeEnd, currentAnchorIndex);
          }
        }
        --arpeggioIndex;
      }
    }
    else
    {
      ASSERT(chord != nullptr);
      const TimeNS noteTime = -chord->timeNS + Global::musicTimeElapsedNS;

      TimeNS chordSustain = 0;

      for (const Song::Note& note : chord->chordNotes)
      {
        chordSustain = max_(chordSustain, note.sustainNS);
      }

      if (TimeNS_To_Seconds(noteTime - chordSustain) > Const::highwayDrawChordNameEndDuration)
      {
        --chordIndex;
        continue;
      }
      if (TimeNS_To_Seconds(noteTime) < -Settings::highwayViewDistance / Settings::highwayScrollSpeed)
      {
        --chordIndex;
        continue;
      }


      bool highDensityFixed = chord->highDensity; // some songs don't set the highDensity on chords. When it is not set, check if the previous chord is the same.
      if (!highDensityFixed && chordIndex >= 1)
      {
        const Song::Chord& prevChord = Global::songTrackLevelAdjusted[selectedArrangementIndex].chords[chordIndex - 1];
        if (chord->chordNotes.size() == prevChord.chordNotes.size())
        {
          highDensityFixed = true;
          for (i32 j = 0; j < i32(chord->chordNotes.size()); ++j)
          {
            if (chord->chordNotes[j].string != prevChord.chordNotes[j].string
              || chord->chordNotes[j].fret != prevChord.chordNotes[j].fret
              || chord->chordNotes[j].sustainNS != 0)
            {
              highDensityFixed = false;
              break;
            }
          }
          if (highDensityFixed)
          {
            highDensityFixed = !(TimeNS_To_Seconds(chord->timeNS - prevChord.timeNS) > Const::highwayDrawHighDensityChordSkipDuration);
          }
        }
      }

      drawChord(ctx, selectedArrangementIndex, highwayViewProjectionMat, *chord, noteTime, fretboardNearestNoteDistance, currentAnchorIndex, highDensityFixed);

      --chordIndex;
    }
  }

  if (Settings::highwayFingerNumbers)
  {
    i32 fingerPositionNextChord = -1;
    f32 fingerPositionNoteTimeBegin = -Const::highwayFingerPreTime;

    for (i32 i = chordEndIndex; i >= chordBeginIndex; --i)
    {
      const Song::Chord& chord = Global::songTrackLevelAdjusted[selectedArrangementIndex].chords[i];

      const TimeNS noteTime = -chord.timeNS + Global::musicTimeElapsedNS;

      TimeNS chordSustain = 0;
      for (const Song::Note& note : chord.chordNotes)
      {
        chordSustain = max_(chordSustain, note.sustainNS);
      }

      if (TimeNS_To_Seconds(noteTime - chordSustain) > Const::highwayDrawChordNameEndDuration)
        continue;
      if (TimeNS_To_Seconds(noteTime) < -Settings::highwayViewDistance / Settings::highwayScrollSpeed)
        continue;

      if (noteTime < 0 && TimeNS_To_Seconds(noteTime) > fingerPositionNoteTimeBegin)
      {
        fingerPositionNextChord = i;
        fingerPositionNoteTimeBegin = TimeNS_To_Seconds(noteTime);
      }
    }

    if (fingerPositionNextChord >= 0)
    {
      const Song::Chord& chord = Global::songTrackLevelAdjusted[selectedArrangementIndex].chords[fingerPositionNextChord];

      drawFretboardChordFingerPositions(ctx, selectedArrangementIndex, highwayViewProjectionMat, chord);
    }
  }
}

static void drawFretboardDotInlays(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, i32 currentAnchorIndex)
{
  GL(glUseProgram(Shader::dotInlay));

  for (i32 i = 3/*skip dot on first thread*/; i <= 24; ++i)
  {
    if (!Const::hasFretDotInlay[i])
      continue;

    {
      const i32 left = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].fret;
      const i32 right = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].fret + Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].width;
      if (left <= i && i < right)
      {
        GL(glUniform4f(Shader::dotInlayUniformColor, Settings::highwayDotInlayColor[0].r, Settings::highwayDotInlayColor[0].g, Settings::highwayDotInlayColor[0].b, Settings::highwayDotInlayColor[0].a));
        GL(glUniform4f(Shader::dotInlayUniformColor2, Settings::highwayDotInlayColor[1].r, Settings::highwayDotInlayColor[1].g, Settings::highwayDotInlayColor[1].b, Settings::highwayDotInlayColor[1].a));
      }
      else
      {
        GL(glUniform4f(Shader::dotInlayUniformColor, Settings::highwayDotInlayColor[2].r, Settings::highwayDotInlayColor[2].g, Settings::highwayDotInlayColor[2].b, Settings::highwayDotInlayColor[2].a));
        GL(glUniform4f(Shader::dotInlayUniformColor2, Settings::highwayDotInlayColor[3].r, Settings::highwayDotInlayColor[3].g, Settings::highwayDotInlayColor[3].b, Settings::highwayDotInlayColor[3].a));
      }
    }

    mat4 model{
      .m00 = 0.2f,
      .m11 = 0.2f,
      .m22 = 0.2f,
      .m03 = ctx.instrumentFretPosition[i - 1] / 2.0f + ctx.instrumentFretPosition[i] / 2.0f,
      .m13 = (f32(ctx.instrumentStringCount - 1) * *ctx.instrumentStringSpacing) / 2.0f,
      .m23 = 0.05f // make sure inlays don't intersect with strings
    };

    if (i % 12 != 0)
    {
      const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
      GL(glUniformMatrix4fv(Shader::dotInlayUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

      GL(glDrawElements_(planeZ));
    }
    else // 12th and 24th fret draw double dots
    {
      const f32 m13 = model.m13;
      const f32 distY = f32(ctx.instrumentStringCount - 1) * *ctx.instrumentStringSpacing * 0.33f;
      {
        model.m13 = m13 + distY;

        const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
        GL(glUniformMatrix4fv(Shader::dotInlayUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

        GL(glDrawElements_(planeZ));
      }
      {
        model.m13 = m13 - distY;

        const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
        GL(glUniformMatrix4fv(Shader::dotInlayUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));

        GL(glDrawElements_(planeZ));
      }
    }
  }
}

static void drawFretboardNearestNote(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, f32 fretboardNearestNoteDistance[Const::highwayInstrumentGuitarStringCount][25])
{
  GL(glUseProgram(Shader::highwayFadeFar));

  GL(glUniform1f(Shader::highwayFadeFarUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::highwayFadeFarUniformFadeFarDistance, Settings::highwayFadeFarDistance));

  for (i32 i = 0; i < ctx.instrumentStringCount; ++i)
  {
    for (i32 j = 1; j < 25; ++j)
    {
      ASSERT(fretboardNearestNoteDistance[i][j] >= 0.0f);

      if (fretboardNearestNoteDistance[i][j] == 0.0f)
        continue;

      const f32 normalizedDistance = min_(Const::highwayDrawFretboardNearestNoteDistance * fretboardNearestNoteDistance[i][j], 1.0f);
      if (normalizedDistance >= 1.0f)
        continue;

      const f32 scale = 1.0f - normalizedDistance;

      ASSERT(0.0f < scale && scale <= 1.0f);

      const mat4 model{
        .m00 = scale * Settings::highwayFretboardNoteWidth,
        .m11 = scale * Settings::highwayFretboardNoteHeight,
        .m22 = scale,
        .m03 = ctx.instrumentFretPosition[j - 1] + 0.5f * (ctx.instrumentFretPosition[j] - ctx.instrumentFretPosition[j - 1]),
        .m13 = f32(reverseStringFix(ctx, i + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing,
        .m23 = 0.1f,
      };

      GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
      GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformModel, 1, GL_FALSE, &model.m00));

      setStringColor(ctx, Shader::highwayFadeFarUniformColor, i + ctx.instrumentStringOffset + ctx.instrumentHideFirstStrings);

      GL(glDrawElements_(noteFretboard));
    }
  }
}

static void drawFretboardFretNumbers(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, i32 currentAnchorIndex)
{
#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontWorld));
  GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFWorld));
  GL(glUniform1f(Shader::fontMSDFWorldUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

  mat4 model = {
    .m00 = 0.2f,
    .m11 = 0.2f,
    .m13 = -0.7f,
    .m23 = 0.0f
  };

  for (i32 i = 1; i <= 24; ++i)
  {
    const i32 left = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].fret;
    const i32 right = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].fret + Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex].width;

    if (left <= i && i < right)
    {
#ifdef SHR3D_FONT_BITMAP
      GL(glUniform4f(Shader::fontWorldUniformColor, Settings::highwayFretboardFretNumberColor[0].r, Settings::highwayFretboardFretNumberColor[0].g, Settings::highwayFretboardFretNumberColor[0].b, Settings::highwayFretboardFretNumberColor[0].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniform4f(Shader::fontMSDFWorldUniformColor, Settings::highwayFretboardFretNumberColor[0].r, Settings::highwayFretboardFretNumberColor[0].g, Settings::highwayFretboardFretNumberColor[0].b, Settings::highwayFretboardFretNumberColor[0].a));
#endif // SHR3D_FONT_MSDF

      model.m03 = ctx.instrumentFretPosition[i - 1] + 0.5f * (ctx.instrumentFretPosition[i] - ctx.instrumentFretPosition[i - 1]);

      {
        const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
        GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
        GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF
      }

      GL(glBindTexture(GL_TEXTURE_2D, Font1::fretNumberTextureCache[i]));

      GL(glDrawElements_(planeZFlippedV));
    }
    else if (left - 1 <= i && i < right + 1)
    {
#ifdef SHR3D_FONT_BITMAP
      GL(glUniform4f(Shader::fontWorldUniformColor, Settings::highwayFretboardFretNumberColor[1].r, Settings::highwayFretboardFretNumberColor[1].g, Settings::highwayFretboardFretNumberColor[1].b, Settings::highwayFretboardFretNumberColor[1].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniform4f(Shader::fontMSDFWorldUniformColor, Settings::highwayFretboardFretNumberColor[1].r, Settings::highwayFretboardFretNumberColor[1].g, Settings::highwayFretboardFretNumberColor[1].b, Settings::highwayFretboardFretNumberColor[1].a));
#endif // SHR3D_FONT_MSDF

      model.m03 = ctx.instrumentFretPosition[i - 1] + 0.5f * (ctx.instrumentFretPosition[i] - ctx.instrumentFretPosition[i - 1]);

      {
        const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
        GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
        GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF
      }

      GL(glBindTexture(GL_TEXTURE_2D, Font1::fretNumberTextureCache[i]));

      GL(glDrawElements_(planeZFlippedV));
    }
    else if (Const::hasFretDotInlay[i])
    {
#ifdef SHR3D_FONT_BITMAP
      GL(glUniform4f(Shader::fontWorldUniformColor, Settings::highwayFretboardFretNumberColor[2].r, Settings::highwayFretboardFretNumberColor[2].g, Settings::highwayFretboardFretNumberColor[2].b, Settings::highwayFretboardFretNumberColor[2].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniform4f(Shader::fontMSDFWorldUniformColor, Settings::highwayFretboardFretNumberColor[2].r, Settings::highwayFretboardFretNumberColor[2].g, Settings::highwayFretboardFretNumberColor[2].b, Settings::highwayFretboardFretNumberColor[2].a));
#endif // SHR3D_FONT_MSDF

      model.m03 = ctx.instrumentFretPosition[i - 1] + 0.5f * (ctx.instrumentFretPosition[i] - ctx.instrumentFretPosition[i - 1]);

      {
        const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
        GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
        GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF
      }

      GL(glBindTexture(GL_TEXTURE_2D, Font1::fretNumberTextureCache[i]));

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

static void drawFretboardCapo(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat)
{
  if (ctx.arrangement->capoFret <= 0)
    return;

  GL(glUseProgram(Shader::highwayFadeFar));

  GL(glUniform1f(Shader::highwayFadeFarUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::highwayFadeFarUniformFadeFarDistance, Settings::highwayFadeFarDistance));

  const mat4 model = {
    .m11 = ctx.instrumentStringCount * *ctx.instrumentStringSpacing,
    .m03 = ctx.instrumentFretPosition[ctx.arrangement->capoFret - 1] + 0.5f * (ctx.instrumentFretPosition[ctx.arrangement->capoFret] - ctx.instrumentFretPosition[ctx.arrangement->capoFret - 1]),
    .m13 = ((ctx.instrumentStringCount - 1) / 2.0f) * *ctx.instrumentStringSpacing,
  };

  GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniformMatrix4fv(Shader::highwayFadeFarUniformModel, 1, GL_FALSE, &model.m00));

  GL(glUniform4f(Shader::highwayFadeFarUniformColor, Settings::highwayCapoColor.r, Settings::highwayCapoColor.g, Settings::highwayCapoColor.b, Settings::highwayCapoColor.a));

  GL(glDrawElements_(capo));
}

static void drawFretboardStringNoteNames(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat)
{
#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontWorld));
  GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniform4f(Shader::fontWorldUniformColor, Settings::highwayFretboardNoteNameColor[0].r, Settings::highwayFretboardNoteNameColor[0].g, Settings::highwayFretboardNoteNameColor[0].b, Settings::highwayFretboardNoteNameColor[0].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFWorld));
  GL(glUniform4f(Shader::fontMSDFWorldUniformColor, Settings::highwayFretboardNoteNameColor[0].r, Settings::highwayFretboardNoteNameColor[0].g, Settings::highwayFretboardNoteNameColor[0].b, Settings::highwayFretboardNoteNameColor[0].a));
  GL(glUniform1f(Shader::fontMSDFWorldUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

  mat4 model = {
    .m00 = 0.15f,
    .m11 = 0.15f,
    .m03 = -0.2f,
    .m23 = 0.1f
  };

  for (i8 y = 0; y < ctx.instrumentStringCount; ++y)
  {
    const i8 string = reverseStringFix(ctx, y);
    if (string - ctx.instrumentStringOffset >= 0)
    {
      model.m13 = f32(y) * *ctx.instrumentStringSpacing;

      {
        const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
        GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
        GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF
      }

      const MidiNote stringTuning = getStringNoteZeroTuningHighway(ctx, string);
      const u8 noteNameIndex = to_underlying_(stringTuning) % 12;

      GL(glBindTexture(GL_TEXTURE_2D, Font1::noteNamesTextureCache[noteNameIndex]));

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

static void drawFretboardNearestNoteNames(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat)
{
#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontWorld));
  GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniform4f(Shader::fontWorldUniformColor, Settings::highwayFretboardNoteNameColor[1].r, Settings::highwayFretboardNoteNameColor[1].g, Settings::highwayFretboardNoteNameColor[1].b, Settings::highwayFretboardNoteNameColor[1].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFWorld));
  GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniform4f(Shader::fontMSDFWorldUniformColor, Settings::highwayFretboardNoteNameColor[1].r, Settings::highwayFretboardNoteNameColor[1].g, Settings::highwayFretboardNoteNameColor[1].b, Settings::highwayFretboardNoteNameColor[1].a));
  GL(glUniform1f(Shader::fontMSDFWorldUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

  mat4 model = {
    .m00 = 0.15f,
    .m11 = 0.15f,
    .m23 = 0.1f
  };

  for (i8 y = 0; y < ctx.instrumentStringCount; ++y)
  {
    model.m13 = f32(y) * *ctx.instrumentStringSpacing;
    const MidiNote stringTuning = getStringNoteZeroTuningHighway(ctx, reverseStringFix(ctx, y));

    for (i32 i = 1; i <= 24; ++i)
    {
      model.m03 = ctx.instrumentFretPosition[i - 1] + 0.2f;

      {
        const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
        GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
        GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF
      }

      const MidiNote note = MidiNote(to_underlying_(stringTuning) + i);
      const u8 noteNameIndex = to_underlying_(note) % 12;

      GL(glBindTexture(GL_TEXTURE_2D, Font1::noteNamesTextureCache[noteNameIndex]));

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

#ifdef SHR3D_SFX_CORE_HEXFIN
static void drawTuner(const mat4& highwayViewProjectionMat, const i8 string, const f32 x, const f32 y, const f32 frequency)
{
  const f32 cf = -12.0f * log2f(Global::a4ReferenceFrequency / frequency);
  const f32 cents = tunerCents(frequency, tunerReferenceFrequency(cf, Global::a4ReferenceFrequency));
  const f32 centsPos = 0.5f + (cents / 100.0f);

  GL(glUseProgram(Shader::worldTuner));

  GL(glUniform1f(Shader::worldTunerUniformCentsPos, centsPos));
  GL(glUniform4f(Shader::worldTunerUniformColor, Settings::highwayTunerColor[0].r, Settings::highwayTunerColor[0].g, Settings::highwayTunerColor[0].b, Settings::highwayTunerColor[0].a));
  GL(glUniform4f(Shader::worldTunerUniformColor2, Settings::highwayTunerColor[1].r, Settings::highwayTunerColor[1].g, Settings::highwayTunerColor[1].b, Settings::highwayTunerColor[1].a));
  GL(glUniform4f(Shader::worldTunerUniformColor3, Settings::highwayTunerColor[2].r, Settings::highwayTunerColor[2].g, Settings::highwayTunerColor[2].b, Settings::highwayTunerColor[2].a));

  {
    const mat4 model = {
      .m00 = 0.5f,
      .m11 = 0.03f,
      .m03 = x,
      .m13 = y,
      .m23 = 0.1f
    };
    const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
    GL(glUniformMatrix4fv(Shader::worldTunerUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
  }

  GL(glDrawElements_(planeZ));

#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontWorld));
  GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniform4f(Shader::fontWorldUniformColor, Settings::highwayTunerColor[3].r, Settings::highwayTunerColor[3].g, Settings::highwayTunerColor[3].b, Settings::highwayTunerColor[3].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFWorld));
  GL(glUniform1f(Shader::fontMSDFWorldUniformScreenPxRange, Const::fontMsdfScreenPxRange));
  GL(glUniform4f(Shader::fontMSDFWorldUniformColor, Settings::highwayTunerColor[3].r, Settings::highwayTunerColor[3].g, Settings::highwayTunerColor[3].b, Settings::highwayTunerColor[3].a));
#endif // SHR3D_FONT_MSDF

  if (string >= 0)
  {
    const MidiNote targetNote = getStringNoteZeroTuningHighway(Global::highwayCtx, string);
    const i32 targetNoteNameIndex = to_underlying_(targetNote) % 12;
    const i32 targetNoteOctave = (to_underlying_(targetNote) / 12) - 1;

    char noteStr[8];
    sprintf(reinterpret_cast<char*>(noteStr), "%s%d", Const::noteNames[targetNoteNameIndex], targetNoteOctave);

    {
      const mat4 model = {
        .m00 = 0.1f,
        .m11 = 0.1f,
        .m03 = x + 0.7f,
        .m13 = y,
        .m23 = 0.1f
      };
      const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
      GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF

    }

    GL(glBindTexture(GL_TEXTURE_2D, Font1::textTexture(noteStr)));

    GL(glDrawElements_(planeZFlippedV));
  }
  {
    const MidiNote note = MidiNote(tunerNote(cf));
    const i32 noteNameIndex = to_underlying_(note) % 12;
    const i32 noteOctave = (to_underlying_(note) / 12) - 1;

    char noteStr[8];
    sprintf(reinterpret_cast<char*>(noteStr), "%s%d", Const::noteNames[noteNameIndex], noteOctave);

    {
      const mat4 model = {
        .m00 = 0.1f,
        .m11 = 0.1f,
        .m03 = x - 0.7f,
        .m13 = y,
        .m23 = 0.1f
      };
      const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
      GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF
    }

    GL(glBindTexture(GL_TEXTURE_2D, Font1::textTexture(noteStr)));

    GL(glDrawElements_(planeZFlippedV));
  }

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

static void drawTuners(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat)
{
  const f32 x = -1.3f;
  {
    const f32 frequencyMono = Global::frequencyMono;
    if (frequencyMono != 0.0f)
    {
      const f32 y = (ctx.instrumentStringCount - 1) * *ctx.instrumentStringSpacing;
      drawTuner(highwayViewProjectionMat, -1, x, y + 0.3f, frequencyMono);
    }
  }

#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
  if (IS_DIVIDED_PICKUP_ENABLED)
  {
    for (i8 i = 0; i < ctx.instrumentStringCount - ctx.instrumentStringOffset; ++i)
    {
      const f32 frequency = Global::frequency[i];
      if (frequency != 0.0f)
      {
        const f32 y = f32(reverseStringFix(ctx, i + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing;
        drawTuner(highwayViewProjectionMat, i, x, y, frequency);
      }
    }
  }
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED
}
#endif // SHR3D_SFX_CORE_HEXFIN

static void drawVUMeterMono(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const char* name, const f32 top, const f32 bottom, const f32 left, const f32 right, const f32 volume, f32& peakVolumeDB, TimeNS& peakTime)
{
  const f32 volumeDB = tunerDBFromVolume(volume);

  if (volumeDB > peakVolumeDB || peakTime + Const::highwayVUMeterPeakDuration < Global::time_)
  {
    peakVolumeDB = volumeDB;
    peakTime = Global::time_;
  }

  GL(glUseProgram(Shader::worldVUMeterMono));

  GL(glUniform4f(Shader::worldVUMeterMonoUniformColor2, Settings::highwayVUMeterColor[1].r, Settings::highwayVUMeterColor[1].g, Settings::highwayVUMeterColor[1].b, Settings::highwayVUMeterColor[1].a));
  GL(glUniform4f(Shader::worldVUMeterMonoUniformColor3, Settings::highwayVUMeterColor[3].r, Settings::highwayVUMeterColor[3].g, Settings::highwayVUMeterColor[3].b, Settings::highwayVUMeterColor[3].a));

  if (peakVolumeDB < 0.0f)
    GL(glUniform4f(Shader::worldVUMeterMonoUniformColor, Settings::highwayVUMeterColor[0].r, Settings::highwayVUMeterColor[0].g, Settings::highwayVUMeterColor[0].b, Settings::highwayVUMeterColor[0].a));
  else
    GL(glUniform4f(Shader::worldVUMeterMonoUniformColor2, Settings::highwayVUMeterColor[2].r, Settings::highwayVUMeterColor[2].g, Settings::highwayVUMeterColor[2].b, Settings::highwayVUMeterColor[2].a));

  {
    const f32 volumeDBClamped = clamp(volumeDB, -60.0f, 0.0f);
    const f32 volumeDBClampedMapped = map_(volumeDBClamped, -60.0f, 0.0f, 0.0f, 1.0f);
    GL(glUniform1f(Shader::worldVUMeterMonoUniformVolume, volumeDBClampedMapped));
  }

  {
    const f32 volumeDBClamped = clamp(peakVolumeDB, -60.0f, 0.0f);
    const f32 volumeDBClampedMapped = map_(volumeDBClamped, -60.0f, 0.0f, 0.0f, 1.0f);
    GL(glUniform1f(Shader::worldVUMeterMonoUniformPeakVolume, volumeDBClampedMapped));
  }

  {
    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 = 0.1f
    };
    const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
    GL(glUniformMatrix4fv(Shader::worldVUMeterMonoUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
  }

  GL(glDrawElements_(planeZ));

#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontWorld));
  GL(glUniform4f(Shader::fontWorldUniformColor, Settings::highwayVUMeterColor[3].r, Settings::highwayVUMeterColor[3].g, Settings::highwayVUMeterColor[3].b, Settings::highwayVUMeterColor[3].a));
  GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFWorld));
  GL(glUniform4f(Shader::fontMSDFWorldUniformColor, Settings::highwayVUMeterColor[3].r, Settings::highwayVUMeterColor[3].g, Settings::highwayVUMeterColor[3].b, Settings::highwayVUMeterColor[3].a));
  GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniform1f(Shader::fontMSDFWorldUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

  char peakVolumeDBStr[8];
  sprintf(reinterpret_cast<char*>(peakVolumeDBStr), "%.1f", peakVolumeDB);

  mat4 model = {
    .m00 = 0.06f,
    .m11 = 0.04f,
    .m03 = (left + right) / 2.0f,
    .m13 = (ctx.instrumentStringCount - 1) * *ctx.instrumentStringSpacing + 0.05f,
    .m23 = 0.1f
  };

  {
    const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
    GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
    GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF
  }

  GL(glBindTexture(GL_TEXTURE_2D, Font1::textTexture(peakVolumeDBStr)));

  GL(glDrawElements_(planeZFlippedV));

#ifdef SHR3D_FONT_BITMAP
  GL(glUniform4f(Shader::fontWorldUniformColor, Settings::highwayVUMeterColor[4].r, Settings::highwayVUMeterColor[4].g, Settings::highwayVUMeterColor[4].b, Settings::highwayVUMeterColor[4].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUniform4f(Shader::fontMSDFWorldUniformColor, Settings::highwayVUMeterColor[4].r, Settings::highwayVUMeterColor[4].g, Settings::highwayVUMeterColor[4].b, Settings::highwayVUMeterColor[4].a));
#endif // SHR3D_FONT_MSDF

  model.m23 = 0.12f;

  for (i32 i = 0; i < 5; ++i)
  {
    char db[4];
    sprintf(reinterpret_cast<char*>(db), "%d", -54 + i * 12);

    model.m13 = ((0.5f + i) * f32(ctx.instrumentStringCount - 1) * *ctx.instrumentStringSpacing) / 5.0f;

    {
      const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
      GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF
    }

    GL(glBindTexture(GL_TEXTURE_2D, Font1::textTexture(db)));

    GL(glDrawElements_(planeZFlippedV));
  }

  model.m13 = -0.1f;

  {
    const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
    GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
    GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF
  }

  GL(glBindTexture(GL_TEXTURE_2D, Font1::textTexture(name)));

  GL(glDrawElements_(planeZFlippedV));
}

static void drawVUMeterStereo(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const char* name, const f32 top, const f32 bottom, const f32 left, const f32 right, f32 volumeLeft, f32 volumeRight, f32& peakVolumeDBLeft, TimeNS& peakTimeLeft, f32& peakVolumeDBRight, TimeNS& peakTimeRight)
{
  const f32 volumeDBLeft = tunerDBFromVolume(volumeLeft);
  const f32 volumeDBRight = tunerDBFromVolume(volumeRight);

  if (volumeDBLeft > peakVolumeDBLeft || peakTimeLeft + Const::highwayVUMeterPeakDuration < Global::time_)
  {
    peakVolumeDBLeft = volumeDBLeft;
    peakTimeLeft = Global::time_;
  }

  if (volumeDBRight > peakVolumeDBRight || peakTimeRight + Const::highwayVUMeterPeakDuration < Global::time_)
  {
    peakVolumeDBRight = volumeDBRight;
    peakTimeRight = Global::time_;
  }

  GL(glUseProgram(Shader::worldVUMeterStereo));

  GL(glUniform4f(Shader::worldVUMeterStereoUniformColor2, Settings::highwayVUMeterColor[1].r, Settings::highwayVUMeterColor[1].g, Settings::highwayVUMeterColor[1].b, Settings::highwayVUMeterColor[1].a));
  GL(glUniform4f(Shader::worldVUMeterStereoUniformColor3, Settings::highwayVUMeterColor[3].r, Settings::highwayVUMeterColor[3].g, Settings::highwayVUMeterColor[3].b, Settings::highwayVUMeterColor[3].a));

  const f32 peakVolumeDB = max_(peakVolumeDBLeft, peakVolumeDBRight);
  if (peakVolumeDB < 0.0f)
    GL(glUniform4f(Shader::worldVUMeterStereoUniformColor, Settings::highwayVUMeterColor[0].r, Settings::highwayVUMeterColor[0].g, Settings::highwayVUMeterColor[0].b, Settings::highwayVUMeterColor[0].a));
  else
    GL(glUniform4f(Shader::worldVUMeterStereoUniformColor2, Settings::highwayVUMeterColor[2].r, Settings::highwayVUMeterColor[2].g, Settings::highwayVUMeterColor[2].b, Settings::highwayVUMeterColor[2].a));

  {
    const f32 volumeDBLeftClamped = clamp(volumeDBLeft, -60.0f, 0.0f);
    const f32 volumeDBLeftClampedMapped = map_(volumeDBLeftClamped, -60.0f, 0.0f, 0.0f, 1.0f);
    GL(glUniform1f(Shader::worldVUMeterStereoUniformVolumeLeft, volumeDBLeftClampedMapped));
  }

  {
    const f32 volumeDBRightClamped = clamp(volumeDBRight, -60.0f, 0.0f);
    const f32 volumeDBRightClampedMapped = map_(volumeDBRightClamped, -60.0f, 0.0f, 0.0f, 1.0f);
    GL(glUniform1f(Shader::worldVUMeterStereoUniformVolumeRight, volumeDBRightClampedMapped));
  }

  {
    const f32 volumeDBLeftClamped = clamp(peakVolumeDBLeft, -60.0f, 0.0f);
    const f32 volumeDBLeftClampedMapped = map_(volumeDBLeftClamped, -60.0f, 0.0f, 0.0f, 1.0f);
    GL(glUniform1f(Shader::worldVUMeterStereoUniformPeakVolumeLeft, volumeDBLeftClampedMapped));
  }

  {
    const f32 volumeDBRightClamped = clamp(peakVolumeDBRight, -60.0f, 0.0f);
    const f32 volumeDBRightClampedMapped = map_(volumeDBRightClamped, -60.0f, 0.0f, 0.0f, 1.0f);
    GL(glUniform1f(Shader::worldVUMeterStereoUniformPeakVolumeRight, volumeDBRightClampedMapped));
  }

  {
    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 = 0.1f
    };
    const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
    GL(glUniformMatrix4fv(Shader::worldVUMeterStereoUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
  }

  GL(glDrawElements_(planeZ));

#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontWorld));
  GL(glUniform4f(Shader::fontWorldUniformColor, Settings::highwayVUMeterColor[3].r, Settings::highwayVUMeterColor[3].g, Settings::highwayVUMeterColor[3].b, Settings::highwayVUMeterColor[3].a));
  GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFWorld));
  GL(glUniform4f(Shader::fontMSDFWorldUniformColor, Settings::highwayVUMeterColor[3].r, Settings::highwayVUMeterColor[3].g, Settings::highwayVUMeterColor[3].b, Settings::highwayVUMeterColor[3].a));
  GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniform1f(Shader::fontMSDFWorldUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

  char peakVolumeDBStr[8];
  sprintf(reinterpret_cast<char*>(peakVolumeDBStr), "%.1f", peakVolumeDB);

  mat4 model = {
    .m00 = 0.06f,
    .m11 = 0.04f,
    .m03 = (left + right) / 2.0f,
    .m13 = (ctx.instrumentStringCount - 1) * *ctx.instrumentStringSpacing + 0.05f,
    .m23 = 0.1f
  };

  {
    const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
    GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
    GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF
  }

  GL(glBindTexture(GL_TEXTURE_2D, Font1::textTexture(peakVolumeDBStr)));

  GL(glDrawElements_(planeZFlippedV));

#ifdef SHR3D_FONT_BITMAP
  GL(glUniform4f(Shader::fontWorldUniformColor, Settings::highwayVUMeterColor[4].r, Settings::highwayVUMeterColor[4].g, Settings::highwayVUMeterColor[4].b, Settings::highwayVUMeterColor[4].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUniform4f(Shader::fontMSDFWorldUniformColor, Settings::highwayVUMeterColor[4].r, Settings::highwayVUMeterColor[4].g, Settings::highwayVUMeterColor[4].b, Settings::highwayVUMeterColor[4].a));
#endif // SHR3D_FONT_MSDF

  model.m23 = 0.12f;

  for (i32 i = 0; i < 5; ++i)
  {
    char db[4];
    sprintf(reinterpret_cast<char*>(db), "%d", -54 + i * 12);

    model.m13 = ((0.5f + i) * f32(ctx.instrumentStringCount - 1) * *ctx.instrumentStringSpacing) / 5.0f;

    {
      const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
      GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF
    }

    GL(glBindTexture(GL_TEXTURE_2D, Font1::textTexture(db)));

    GL(glDrawElements_(planeZFlippedV));

  }

  model.m13 = -0.1f;

  {
    const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
#ifdef SHR3D_FONT_BITMAP
    GL(glUniformMatrix4fv(Shader::fontWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
    GL(glUniformMatrix4fv(Shader::fontMSDFWorldUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
#endif // SHR3D_FONT_MSDF
  }

  GL(glBindTexture(GL_TEXTURE_2D, Font1::textTexture(name)));

  GL(glDrawElements_(planeZFlippedV));
}

static void drawVUMeters(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat)
{
  const f32 x =
#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
    Settings::highwayTuner && IS_DIVIDED_PICKUP_ENABLED ? -1.6f :
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED
    0.0f;

  drawVUMeterMono(ctx, highwayViewProjectionMat, "IN", (ctx.instrumentStringCount - 1) * *ctx.instrumentStringSpacing, 0.0f, x - 1.4f, x - 1.2f, Global::inputVolumeMono, ctx.vuMeterInputPeakVolumeDB, ctx.vuMeterInputPeakTime);
  drawVUMeterStereo(ctx, highwayViewProjectionMat, "FX", (ctx.instrumentStringCount - 1) * *ctx.instrumentStringSpacing, 0.0f, x - 1.1f, x - 0.9f, Global::effectVolumeLeft, Global::effectVolumeRight, Global::highwayVUMeterEffectPeakVolumeDBLeft, Global::highwayVUMeterEffectPeakTimeLeft, Global::highwayVUMeterEffectPeakVolumeDBRight, Global::highwayVUMeterEffectPeakTimeRight);
  drawVUMeterStereo(ctx, highwayViewProjectionMat, "OUT", (ctx.instrumentStringCount - 1) * *ctx.instrumentStringSpacing, 0.0f, x - 0.8f, x - 0.6f, Global::outputVolumeLeft, Global::outputVolumeRight, Global::highwayVUMeterOutputPeakVolumeDBLeft, Global::highwayVUMeterOutputPeakTimeLeft, Global::highwayVUMeterOutputPeakVolumeDBRight, Global::highwayVUMeterOutputPeakTimeRight);

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

static void drawGroundBeatFretNumber(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, f32 noteTimeEnd, i32 chordBoxLeft, i32 chordBoxWidth)
{
#ifdef SHR3D_FONT_BITMAP
  GL(glUseProgram(Shader::fontWorldFadeFarAndNear));
  GL(glUniformMatrix4fv(Shader::fontWorldFadeFarAndNearUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniform1f(Shader::fontWorldFadeFarAndNearUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::fontWorldFadeFarAndNearUniformFadeFarDistance, Settings::highwayFadeFarDistance));
  GL(glUniform1f(Shader::fontWorldFadeFarAndNearUniformFadeNearDistance, Settings::highwayFadeNearDistance));
  GL(glUniform1f(Shader::fontWorldFadeFarAndNearUniformFadeNearStrength, Settings::highwayFadeNearStrength));
  GL(glUniform4f(Shader::fontWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[0].r, Settings::highwayFretboardFretNumberColor[0].g, Settings::highwayFretboardFretNumberColor[0].b, Settings::highwayFretboardFretNumberColor[0].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
  GL(glUseProgram(Shader::fontMSDFWorldFadeFarAndNear));
  GL(glUniformMatrix4fv(Shader::fontMSDFWorldFadeFarAndNearUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));
  GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformFadeFarDistance, Settings::highwayFadeFarDistance));
  GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformFadeNearDistance, Settings::highwayFadeNearDistance));
  GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformFadeNearStrength, Settings::highwayFadeNearStrength));
  GL(glUniform4f(Shader::fontMSDFWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[0].r, Settings::highwayFretboardFretNumberColor[0].g, Settings::highwayFretboardFretNumberColor[0].b, Settings::highwayFretboardFretNumberColor[0].a));
  GL(glUniform1f(Shader::fontMSDFWorldFadeFarAndNearUniformScreenPxRange, Const::fontMsdfScreenPxRange));
#endif // SHR3D_FONT_MSDF

  mat4 model = {
    .m00 = 0.2f,
    .m11 = 0.2f,
    .m13 = -0.7f,
    .m23 = noteTimeEnd * Settings::highwayScrollSpeed
  };

  const i32 leftBegin = max_(1, chordBoxLeft - 3);
  const i32 leftEnd = chordBoxLeft;
  for (i32 i = leftBegin; i < leftEnd; ++i)
  {
    const i32 left = chordBoxLeft;

    if (left <= i)
    {
#ifdef SHR3D_FONT_BITMAP
      GL(glUniform4f(Shader::fontWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[0].r, Settings::highwayFretboardFretNumberColor[0].g, Settings::highwayFretboardFretNumberColor[0].b, Settings::highwayFretboardFretNumberColor[0].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniform4f(Shader::fontMSDFWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[0].r, Settings::highwayFretboardFretNumberColor[0].g, Settings::highwayFretboardFretNumberColor[0].b, Settings::highwayFretboardFretNumberColor[0].a));
#endif // SHR3D_FONT_MSDF

      model.m03 = ctx.instrumentFretPosition[i - 1] + 0.5f * (ctx.instrumentFretPosition[i] - ctx.instrumentFretPosition[i - 1]);

#ifdef SHR3D_FONT_BITMAP
      GL(glUniformMatrix4fv(Shader::fontWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniformMatrix4fv(Shader::fontMSDFWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_MSDF

      GL(glBindTexture(GL_TEXTURE_2D, Font1::fretNumberTextureCache[i]));

      GL(glDrawElements_(planeZFlippedV));
    }
    else if (left - 1 <= i)
    {
#ifdef SHR3D_FONT_BITMAP
      GL(glUniform4f(Shader::fontWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[1].r, Settings::highwayFretboardFretNumberColor[1].g, Settings::highwayFretboardFretNumberColor[1].b, Settings::highwayFretboardFretNumberColor[1].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniform4f(Shader::fontMSDFWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[1].r, Settings::highwayFretboardFretNumberColor[1].g, Settings::highwayFretboardFretNumberColor[1].b, Settings::highwayFretboardFretNumberColor[1].a));
#endif // SHR3D_FONT_MSDF

      model.m03 = ctx.instrumentFretPosition[i - 1] + 0.5f * (ctx.instrumentFretPosition[i] - ctx.instrumentFretPosition[i - 1]);

#ifdef SHR3D_FONT_BITMAP
      GL(glUniformMatrix4fv(Shader::fontWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniformMatrix4fv(Shader::fontMSDFWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_MSDF

      GL(glBindTexture(GL_TEXTURE_2D, Font1::fretNumberTextureCache[i]));

      GL(glDrawElements_(planeZFlippedV));
    }
    else if (Const::hasFretDotInlay[i])
    {
#ifdef SHR3D_FONT_BITMAP
      GL(glUniform4f(Shader::fontWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[2].r, Settings::highwayFretboardFretNumberColor[2].g, Settings::highwayFretboardFretNumberColor[2].b, Settings::highwayFretboardFretNumberColor[2].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniform4f(Shader::fontMSDFWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[2].r, Settings::highwayFretboardFretNumberColor[2].g, Settings::highwayFretboardFretNumberColor[2].b, Settings::highwayFretboardFretNumberColor[2].a));
#endif // SHR3D_FONT_MSDF

      model.m03 = ctx.instrumentFretPosition[i - 1] + 0.5f * (ctx.instrumentFretPosition[i] - ctx.instrumentFretPosition[i - 1]);

#ifdef SHR3D_FONT_BITMAP
      GL(glUniformMatrix4fv(Shader::fontWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniformMatrix4fv(Shader::fontMSDFWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_MSDF

      GL(glBindTexture(GL_TEXTURE_2D, Font1::fretNumberTextureCache[i]));

      GL(glDrawElements_(planeZFlippedV));
    }
  }
  const i32 rightBegin = chordBoxLeft + chordBoxWidth;
  const i32 rightEnd = min_(24, rightBegin + 3);
  for (i32 i = rightBegin; i <= rightEnd; ++i)
  {
    //const f32 x = ctx.instrumentFretPosition[i - 1] + 0.5f * (ctx.instrumentFretPosition[i] - ctx.instrumentFretPosition[i - 1]);

    const i32 right = chordBoxLeft + chordBoxWidth;

    if (i < right)
    {
#ifdef SHR3D_FONT_BITMAP
      GL(glUniform4f(Shader::fontWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[0].r, Settings::highwayFretboardFretNumberColor[0].g, Settings::highwayFretboardFretNumberColor[0].b, Settings::highwayFretboardFretNumberColor[0].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniform4f(Shader::fontMSDFWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[0].r, Settings::highwayFretboardFretNumberColor[0].g, Settings::highwayFretboardFretNumberColor[0].b, Settings::highwayFretboardFretNumberColor[0].a));
#endif // SHR3D_FONT_MSDF

      model.m03 = ctx.instrumentFretPosition[i - 1] + 0.5f * (ctx.instrumentFretPosition[i] - ctx.instrumentFretPosition[i - 1]);

#ifdef SHR3D_FONT_BITMAP
      GL(glUniformMatrix4fv(Shader::fontWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniformMatrix4fv(Shader::fontMSDFWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_MSDF

      GL(glBindTexture(GL_TEXTURE_2D, Font1::fretNumberTextureCache[i]));

      GL(glDrawElements_(planeZFlippedV));
    }
    else if (i < right + 1)
    {
#ifdef SHR3D_FONT_BITMAP
      GL(glUniform4f(Shader::fontWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[1].r, Settings::highwayFretboardFretNumberColor[1].g, Settings::highwayFretboardFretNumberColor[1].b, Settings::highwayFretboardFretNumberColor[1].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniform4f(Shader::fontMSDFWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[1].r, Settings::highwayFretboardFretNumberColor[1].g, Settings::highwayFretboardFretNumberColor[1].b, Settings::highwayFretboardFretNumberColor[1].a));
#endif // SHR3D_FONT_MSDF

      model.m03 = ctx.instrumentFretPosition[i - 1] + 0.5f * (ctx.instrumentFretPosition[i] - ctx.instrumentFretPosition[i - 1]);

#ifdef SHR3D_FONT_BITMAP
      GL(glUniformMatrix4fv(Shader::fontWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniformMatrix4fv(Shader::fontMSDFWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_MSDF

      GL(glBindTexture(GL_TEXTURE_2D, Font1::fretNumberTextureCache[i]));

      GL(glDrawElements_(planeZFlippedV));
    }
    else if (Const::hasFretDotInlay[i])
    {
#ifdef SHR3D_FONT_BITMAP
      GL(glUniform4f(Shader::fontWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[2].r, Settings::highwayFretboardFretNumberColor[2].g, Settings::highwayFretboardFretNumberColor[2].b, Settings::highwayFretboardFretNumberColor[2].a));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniform4f(Shader::fontMSDFWorldFadeFarAndNearUniformColor, Settings::highwayFretboardFretNumberColor[2].r, Settings::highwayFretboardFretNumberColor[2].g, Settings::highwayFretboardFretNumberColor[2].b, Settings::highwayFretboardFretNumberColor[2].a));
#endif // SHR3D_FONT_MSDF

      model.m03 = ctx.instrumentFretPosition[i - 1] + 0.5f * (ctx.instrumentFretPosition[i] - ctx.instrumentFretPosition[i - 1]);

#ifdef SHR3D_FONT_BITMAP
      GL(glUniformMatrix4fv(Shader::fontWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_BITMAP
#ifdef SHR3D_FONT_MSDF
      GL(glUniformMatrix4fv(Shader::fontMSDFWorldFadeFarAndNearUniformModel, 1, GL_FALSE, &model.m00));
#endif // SHR3D_FONT_MSDF

      GL(glBindTexture(GL_TEXTURE_2D, Font1::fretNumberTextureCache[i]));

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

static void drawGroundBeat(const Highway::Ctx& ctx, TimeNS noteTimeBegin, TimeNS noteTimeEnd, bool isPrimaryBeat, i32 chordBoxLeft, i32 chordBoxWidth)
{
  const f32 left = ctx.instrumentFretPosition[chordBoxLeft - 1];
  const f32 right = ctx.instrumentFretPosition[chordBoxLeft + chordBoxWidth - 1];
  const f32 front = TimeNS_To_Seconds(noteTimeBegin) * Settings::highwayScrollSpeed;
  const f32 back = TimeNS_To_Seconds(noteTimeEnd) * Settings::highwayScrollSpeed;

  if (isPrimaryBeat)
    GL(glUniform4f(Shader::groundBeatUniformColor, Settings::highwayBeatColor[0].r, Settings::highwayBeatColor[0].g, Settings::highwayBeatColor[0].b, Settings::highwayBeatColor[0].a));
  else
    GL(glUniform4f(Shader::groundBeatUniformColor, Settings::highwayBeatColor[1].r, Settings::highwayBeatColor[1].g, Settings::highwayBeatColor[1].b, Settings::highwayBeatColor[1].a));


  const mat4 model = {
    .m00 = 0.5f * (right - left),
    .m22 = 0.5f * (front - back),
    .m03 = left + 0.5f * (right - left),
    .m13 = -0.35f,
    .m23 = back + 0.5f * (front - back)
  };

  GL(glUniformMatrix4fv(Shader::groundBeatUniformModel, 1, GL_FALSE, &model.m00));

  GL(glDrawElements_(planeY));
}

static void drawGroundBeats(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, i32 currentAnchorIndex, i32 beatBeginIndex, i32 beatEndIndex)
{
  if (beatBeginIndex == -1)
    return;

  for (i32 i = beatEndIndex; i >= beatBeginIndex; --i)
  {
    const Song::Beat& beat = Global::songTracks[selectedArrangementIndex].beats[i];

    const TimeNS noteTimeBegin = -beat.timeNS + Global::musicTimeElapsedNS;
    const TimeNS noteTimeEnd = noteTimeBegin - Const::highwayBeatDuration;

    if (noteTimeEnd > 0)
      continue;

    const Song::Anchor& anchor = findAnchorByTime(beat.timeNS, selectedArrangementIndex, currentAnchorIndex);

    if (beat.isPrimary)
    {
      drawGroundBeatFretNumber(ctx, highwayViewProjectionMat, TimeNS_To_Seconds(noteTimeEnd), anchor.fret, anchor.width);
    }

    GL(glUseProgram(Shader::groundBeat));

    GL(glUniform1f(Shader::groundBeatUniformViewDistance, Settings::highwayViewDistance));
    GL(glUniform1f(Shader::groundBeatUniformFadeFarDistance, Settings::highwayFadeFarDistance));
    GL(glUniformMatrix4fv(Shader::groundBeatUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));

    drawGroundBeat(ctx, noteTimeBegin, noteTimeEnd, beat.isPrimary, anchor.fret, anchor.width);
  }
}

static void drawGroundStrumDirection(const Highway::Ctx& ctx, const TimeNS noteTimeBegin, const TimeNS noteTimeEnd, const StrumDirection strumDirection, const i32 chordBoxLeft, const i32 chordBoxWidth)
{
  const f32 left = ctx.instrumentFretPosition[chordBoxLeft - 1];
  const f32 right = ctx.instrumentFretPosition[chordBoxLeft + chordBoxWidth - 1];
  const f32 front = TimeNS_To_Seconds(noteTimeBegin) * Settings::highwayScrollSpeed;
  const f32 back = TimeNS_To_Seconds(noteTimeEnd) * Settings::highwayScrollSpeed;

  if (strumDirection == StrumDirection::downStroke)
    GL(glUniform4f(Shader::groundBeatStrumDirectionUniformColor, Settings::highwayBeatStrumDirectionColor[0].r, Settings::highwayBeatStrumDirectionColor[0].g, Settings::highwayBeatStrumDirectionColor[0].b, Settings::highwayBeatStrumDirectionColor[0].a));
  else
    GL(glUniform4f(Shader::groundBeatStrumDirectionUniformColor, Settings::highwayBeatStrumDirectionColor[1].r, Settings::highwayBeatStrumDirectionColor[1].g, Settings::highwayBeatStrumDirectionColor[1].b, Settings::highwayBeatStrumDirectionColor[1].a));


  const mat4 model = {
    .m00 = 0.5f * (right - left),
    .m22 = 0.5f * (front - back),
    .m03 = left + 0.5f * (right - left),
    .m13 = -0.34f,
    .m23 = back + 0.5f * (front - back)
  };

  GL(glUniformMatrix4fv(Shader::groundBeatStrumDirectionUniformModel, 1, GL_FALSE, &model.m00));

  GL(glDrawElements_(planeY));
}

static void drawGroundStrumDirections(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, i32 currentAnchorIndex, i32 beatBeginIndex, i32 beatEndIndex)
{
  if (beatBeginIndex == -1)
    return;

  GL(glUseProgram(Shader::groundBeatStrumDirection));

  GL(glUniform1f(Shader::groundBeatStrumDirectionUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::groundBeatStrumDirectionUniformFadeFarDistance, Settings::highwayFadeFarDistance));
  GL(glUniformMatrix4fv(Shader::groundBeatStrumDirectionUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));

  for (i32 i = beatEndIndex; i >= beatBeginIndex; --i)
  {
    const Song::Beat& beat = Global::songTracks[selectedArrangementIndex].beats[i];

    const TimeNS noteTimeBegin = -beat.timeNS + Global::musicTimeElapsedNS;
    const TimeNS noteTimeEnd = noteTimeBegin - Const::highwayBeatDuration;

    if (noteTimeEnd > 0)
      continue;

    const Song::Anchor& anchor = findAnchorByTime(beat.timeNS, selectedArrangementIndex, currentAnchorIndex);

    i32 distanceFromPrimaryBeat = 0;
    if (!beat.isPrimary)
    {
      for (i32 j = i - 1; j >= 0; --j)
      {
        ++distanceFromPrimaryBeat;
        const Song::Beat& previousBeat = Global::songTracks[selectedArrangementIndex].beats[j];
        if (previousBeat.isPrimary)
          break;
      }
    }

    const StrumDirection beatStrumDirection = distanceFromPrimaryBeat % 2 == 0 ? Settings::highwayBeatStrumDirectionPrimary : Settings::highwayBeatStrumDirectionNext;
    drawGroundStrumDirection(ctx, noteTimeBegin, noteTimeEnd, beatStrumDirection, anchor.fret, anchor.width);

    // draw In strums in between beats
    const bool needExtraStrumInBetween = Settings::highwayBeatStrumDirectionPrimary == Settings::highwayBeatStrumDirectionNext;
    const i32 extraStrumCount = needExtraStrumInBetween + 2 * Settings::highwayBeatStrumsBetweenBeats;

    if (i >= 1 && extraStrumCount >= 1)
    {
      const Song::Beat& prevBeat = Global::songTracks[selectedArrangementIndex].beats[i - 1];

      const TimeNS offsetBeatTimeNS = (beat.timeNS - prevBeat.timeNS) / f32(extraStrumCount + 1);

      for (i32 k = 0; k < extraStrumCount; ++k)
      {
        const TimeNS inBetweenTimeNS = beat.timeNS - offsetBeatTimeNS * f32(k + 1);

        const TimeNS noteTimeBegin = -inBetweenTimeNS + Global::musicTimeElapsedNS;
        const TimeNS noteTimeEnd = noteTimeBegin - Const::highwayBeatDuration;

        if (noteTimeEnd > 0)
          continue;

        const StrumDirection inBetweenBeatStrumDirection = StrumDirection((i32(beatStrumDirection) + k + 1) % 2);
        const Song::Anchor& anchor = findAnchorByTime(inBetweenTimeNS, selectedArrangementIndex, currentAnchorIndex);

        drawGroundStrumDirection(ctx, noteTimeBegin, noteTimeEnd, inBetweenBeatStrumDirection, anchor.fret, anchor.width);
      }
    }
        
  }
}

static void drawGroundQuickRepeaterBegin(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const i32 currentAnchorIndex)
{
  ASSERT(Global::quickRepeaterBeginTimeNS != 0);

  const TimeNS noteTime = -Global::quickRepeaterBeginTimeNS + Global::musicTimeElapsedNS;

  if (noteTime >= 0)
    return;
  if (noteTime < timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
    return;

  const Song::Anchor& anchor = findAnchorByTime(Global::quickRepeaterBeginTimeNS, selectedArrangementIndex, currentAnchorIndex);

  const f32 left = ctx.instrumentFretPosition[max_(0, anchor.fret - 3)];
  const f32 right = ctx.instrumentFretPosition[min_(24, anchor.fret + anchor.width + 1)];
  const f32 front = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed + 0.6f;
  const f32 back = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

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

  const mat4 model = {
    .m00 = 0.5f * (right - left),
    .m22 = 0.5f * (front - back),
    .m03 = left + 0.5f * (right - left),
    .m13 = -0.355f,
    .m23 = back + 0.5f * (front - back)
  };

  GL(glUniformMatrix4fv(Shader::groundBeatUniformModel, 1, GL_FALSE, &model.m00));

  GL(glDrawElements_(planeY));
}

static void drawGroundQuickRepeaterEnd(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const i32 currentAnchorIndex)
{
  ASSERT(Global::quickRepeaterEndTimeNS != 0);

  const TimeNS noteTime = -Global::quickRepeaterEndTimeNS + Global::musicTimeElapsedNS;

  if (noteTime > 0)
    return;
  if (noteTime < timeNS_From_Seconds(-Settings::highwayViewDistance / Settings::highwayScrollSpeed))
    return;

  const Song::Anchor& anchor = findAnchorByTime(Global::quickRepeaterEndTimeNS, selectedArrangementIndex, currentAnchorIndex);

  const f32 left = ctx.instrumentFretPosition[max_(0, anchor.fret - 3)];
  const f32 right = ctx.instrumentFretPosition[min_(24, anchor.fret + anchor.width + 1)];
  const f32 front = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed + 0.6f;
  const f32 back = TimeNS_To_Seconds(noteTime) * Settings::highwayScrollSpeed;

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

  const mat4 model = {
    .m00 = 0.5f * (right - left),
    .m22 = 0.5f * (front - back),
    .m03 = left + 0.5f * (right - left),
    .m13 = -0.355f,
    .m23 = back + 0.5f * (front - back)
  };

  GL(glUniformMatrix4fv(Shader::groundBeatUniformModel, 1, GL_FALSE, &model.m00));

  GL(glDrawElements_(planeY));
}

static void drawGroundQuickRepeater(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, const i32 currentAnchorIndex)
{
  if (Global::quickRepeaterBeginTimeNS == 0 || Global::quickRepeaterEndTimeNS == 0)
    return;

  GL(glUseProgram(Shader::groundBeat));

  GL(glUniform1f(Shader::groundBeatUniformViewDistance, Settings::highwayViewDistance));
  GL(glUniform1f(Shader::groundBeatUniformFadeFarDistance, Settings::highwayFadeFarDistance));
  GL(glUniformMatrix4fv(Shader::groundBeatUniformViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));

  if (Global::quickRepeaterBeginTimeNS != 0)
    drawGroundQuickRepeaterBegin(ctx, selectedArrangementIndex, currentAnchorIndex);
  if (Global::quickRepeaterEndTimeNS != 0)
    drawGroundQuickRepeaterEnd(ctx, selectedArrangementIndex, currentAnchorIndex);
}

//static void drawCircle1()
//{
//  const i32 circleSegments = 32;
//  const f32 r = 0.15f;
//  GLfloat v[circleSegments * 5];
//  for (i32 i = 0; i < circleSegments; ++i)
//  {
//    const f32 alpha = 2.0f * PI_ * f32(i) / f32(circleSegments);
//    v[i * 5] = r * cosf(alpha); // x
//    v[i * 5 + 1] = r * sinf(alpha); // y
//    v[i * 5 + 2] = 0.0f; // z
//    v[i * 5 + 3] = 0.0f; // u
//    v[i * 5 + 4] = 0.0f; // v
//  }
//
//  GL(glBufferData(GL_ARRAY_BUFFER, sizeof(v), v, GL_STATIC_DRAW));
//  GL(glDrawArrays(GL_TRIANGLE_FAN, 0, circleSegments));
//}

static void drawFretboardPlayedNote(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const i8 stringIndex, const i8 fretIndex, const f32 cents, const i8 anchorFret, const i8 anchorWidth, const f32 playedNoteVolume = 1.0f)
{
  if (fretIndex != 0)
  { // draw note dots
#ifdef SHR3D_PARTICLE
    if (Settings::highwayParticlePlayedNotes)
    {
      const Particle::HighwaySpawn highwaySpawn = {
        .position = {
           .x = ctx.instrumentFretPosition[fretIndex - 1] + 0.5f * (ctx.instrumentFretPosition[fretIndex] - ctx.instrumentFretPosition[fretIndex - 1]),
           .y = f32(reverseStringFix(ctx, stringIndex)) * *ctx.instrumentStringSpacing,
           .z = 0.1f
        },
        .width = ctx.instrumentFretPosition[fretIndex] - ctx.instrumentFretPosition[fretIndex - 1],
        .height = *ctx.instrumentStringSpacing,
        .color = ctx.instrumentStringColors[stringIndex + ctx.instrumentHideFirstStrings],
        .spawnMultiplier = 0.5f * playedNoteVolume
      };

      Particle::highwayFretboardSpawn(highwaySpawn);
    }
#endif // SHR3D_PARTICLE
    if (Settings::highwayFretboardPlayedNotesDot)
    {
      GL(glUseProgram(Shader::dotInlay));

      GL(glUniform4f(Shader::dotInlayUniformColor, Settings::highwayFretboardPlayedNotesDotColor[0].r, Settings::highwayFretboardPlayedNotesDotColor[0].g, Settings::highwayFretboardPlayedNotesDotColor[0].b, Settings::highwayFretboardPlayedNotesDotColor[0].a * playedNoteVolume));
      GL(glUniform4f(Shader::dotInlayUniformColor2, Settings::highwayFretboardPlayedNotesDotColor[1].r, Settings::highwayFretboardPlayedNotesDotColor[1].g, Settings::highwayFretboardPlayedNotesDotColor[1].b, Settings::highwayFretboardPlayedNotesDotColor[1].a * playedNoteVolume));

      {
        const mat4 model = {
          .m00 = 0.12f,
          .m11 = 0.12f,
          .m03 = ctx.instrumentFretPosition[fretIndex] - 0.2f,
          .m13 = f32(reverseStringFix(ctx, stringIndex)) * *ctx.instrumentStringSpacing,
          .m23 = 0.101f // make sure inlays don't intersect with strings
        };
        const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
        GL(glUniformMatrix4fv(Shader::dotInlayUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
      }

      GL(glDrawElements_(planeZ));
    }
  }
  else
  { // draw zero note line
#ifdef SHR3D_PARTICLE
    if (Settings::highwayParticlePlayedNotes)
    {
      const Particle::HighwaySpawn highwaySpawn = {
        .position = {
           .x = ctx.instrumentFretPosition[anchorFret - 1] + 0.5f * (ctx.instrumentFretPosition[anchorFret + anchorWidth - 1] - ctx.instrumentFretPosition[anchorFret - 1]),
           .y = f32(reverseStringFix(ctx, stringIndex)) * *ctx.instrumentStringSpacing,
           .z = 0.1f
        },
        .width = ctx.instrumentFretPosition[anchorFret + anchorWidth - 1] - ctx.instrumentFretPosition[anchorFret - 1],
        .height = *ctx.instrumentStringSpacing,
        .color = ctx.instrumentStringColors[stringIndex + ctx.instrumentHideFirstStrings],
        .spawnMultiplier = 1.0f * playedNoteVolume
      };

      Particle::highwayFretboardSpawn(highwaySpawn);
    }
#endif // SHR3D_PARTICLE
    if (Settings::highwayFretboardPlayedNotesDot)
    {
      GL(glUseProgram(Shader::worldNoTexture));

      GL(glUniform4f(Shader::worldNoTextureUniformColor, Settings::highwayFretboardPlayedNotesDotColor[0].r, Settings::highwayFretboardPlayedNotesDotColor[0].g, Settings::highwayFretboardPlayedNotesDotColor[0].b, Settings::highwayFretboardPlayedNotesDotColor[0].a * playedNoteVolume));

      {
        const mat4 model = {
          //.m00 = 0.5f * ctx.instrumentFretPosition[24],
          .m00 = 0.5f * ((ctx.instrumentFretPosition[anchorFret - 1 + anchorWidth] - ctx.instrumentFretPosition[anchorFret - 1])),
          .m11 = 0.03f,
          //.m03 = 0.5f * ctx.instrumentFretPosition[24],
          .m03 = ctx.instrumentFretPosition[anchorFret] + 0.25f * (ctx.instrumentFretPosition[anchorFret + anchorWidth] - ctx.instrumentFretPosition[anchorFret]),
          .m13 = f32(reverseStringFix(ctx, stringIndex)) * *ctx.instrumentStringSpacing,
          .m23 = 0.101f // make sure inlays don't intersect with strings
        };

        const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
        GL(glUniformMatrix4fv(Shader::worldNoTextureUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
      }

      GL(glDrawElements_(planeZ));
    }
  }
}

static void drawFretboardPlayedNotesByMidi(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const u64 notesPlayed, const i8 anchorFret, const i8 anchorWidth)
{
  for (u8 i = 0; i < 64; ++i)
  {
    if ((notesPlayed & (1_u64 << i)) == 0)
      continue;

    const MidiNote notePlayed = MidiNote(i + to_underlying_(MidiNote::E_1));

    //if (ctx.instrumentStringOffset >= 0)
    {
      const MidiNote stringNoteZero = getStringNoteZeroTuning(ctx, 0);
      const MidiNote stringNoteFret24 = MidiNote(to_underlying_(stringNoteZero) + 24);
      if (stringNoteZero <= notePlayed && notePlayed <= stringNoteFret24)
      {
        const i8 fretIndex = to_underlying_(notePlayed) - to_underlying_(stringNoteZero);
        drawFretboardPlayedNote(ctx, highwayViewProjectionMat, 0 + ctx.instrumentStringOffset, fretIndex, 0.0f, anchorFret, anchorWidth);
      }
    }

    //if (ctx.instrumentStringOffset >= -1)
    {
      const MidiNote stringNoteZero = getStringNoteZeroTuning(ctx, 1);
      const MidiNote stringNoteFret24 = MidiNote(to_underlying_(stringNoteZero) + 24);
      if (stringNoteZero <= notePlayed && notePlayed <= stringNoteFret24)
      {
        const i8 fretIndex = to_underlying_(notePlayed) - to_underlying_(stringNoteZero);
        drawFretboardPlayedNote(ctx, highwayViewProjectionMat, 1 + ctx.instrumentStringOffset, fretIndex, 0.0f, anchorFret, anchorWidth);
      }
    }

    {
      const MidiNote stringNoteZero = getStringNoteZeroTuning(ctx, 2);
      const MidiNote stringNoteFret24 = MidiNote(to_underlying_(stringNoteZero) + 24);
      if (stringNoteZero <= notePlayed && notePlayed <= stringNoteFret24)
      {
        const i8 fretIndex = to_underlying_(notePlayed) - to_underlying_(stringNoteZero);
        drawFretboardPlayedNote(ctx, highwayViewProjectionMat, 2 + ctx.instrumentStringOffset, fretIndex, 0.0f, anchorFret, anchorWidth);
      }
    }

    {
      const MidiNote stringNoteZero = getStringNoteZeroTuning(ctx, 3);
      const MidiNote stringNoteFret24 = MidiNote(to_underlying_(stringNoteZero) + 24);
      if (stringNoteZero <= notePlayed && notePlayed <= stringNoteFret24)
      {
        const i8 fretIndex = to_underlying_(notePlayed) - to_underlying_(stringNoteZero);
        drawFretboardPlayedNote(ctx, highwayViewProjectionMat, 3 + ctx.instrumentStringOffset, fretIndex, 0.0f, anchorFret, anchorWidth);
      }
    }

    //if (ctx.instrumentStringCount >= 5)
    {
      const MidiNote stringNoteZero = getStringNoteZeroTuning(ctx, 4);
      const MidiNote stringNoteFret24 = MidiNote(to_underlying_(stringNoteZero) + 24);
      if (stringNoteZero <= notePlayed && notePlayed <= stringNoteFret24)
      {
        const i8 fretIndex = to_underlying_(notePlayed) - to_underlying_(stringNoteZero);
        drawFretboardPlayedNote(ctx, highwayViewProjectionMat, 4 + ctx.instrumentStringOffset, fretIndex, 0.0f, anchorFret, anchorWidth);
      }
    }

    //if (ctx.instrumentStringCount >= 6)
    {
      const MidiNote stringNoteZero = getStringNoteZeroTuning(ctx, 5);
      const MidiNote stringNoteFret24 = MidiNote(to_underlying_(stringNoteZero) + 24);
      if (stringNoteZero <= notePlayed && notePlayed <= stringNoteFret24)
      {
        const i8 fretIndex = to_underlying_(notePlayed) - to_underlying_(stringNoteZero);
        drawFretboardPlayedNote(ctx, highwayViewProjectionMat, 5 + ctx.instrumentStringOffset, fretIndex, 0.0f, anchorFret, anchorWidth);
      }
    }

    if (ctx.instrumentStringCount >= 7)
    {
      const MidiNote stringNoteZero = getStringNoteZeroTuning(ctx, 6);
      const MidiNote stringNoteFret24 = MidiNote(to_underlying_(stringNoteZero) + 24);
      if (stringNoteZero <= notePlayed && notePlayed <= stringNoteFret24)
      {
        const i8 fretIndex = to_underlying_(notePlayed) - to_underlying_(stringNoteZero);
        drawFretboardPlayedNote(ctx, highwayViewProjectionMat, 6 + ctx.instrumentStringOffset, fretIndex, 0.0f, anchorFret, anchorWidth);
      }
    }
  }
}

static void updateNotesPlayed(u64& notesPlayed)
{
  ASSERT(Global::playedNotesFromAudioIndex > 0);

  for (i32 i = 0; i < Global::playedNotesFromAudioIndex; ++i)
  {
    //printf("%02x %02x %02x\n", Global::playedNotesFromAudio[i].status, to_underlying_(Global::playedNotesFromAudio[i].note), Global::playedNotesFromAudio[i].velocity);

    switch (Global::playedNotesFromAudio[i].status)
    {
    case MidiStatus::chan1NoteOn:
    {
      ASSERT(Global::playedNotesFromAudio[i].note >= MidiNote::E_1);
      //ASSERT(Global::playedNotesFromAudio[i].note <= MidiNote::D_sharp_6);

      const u8 noteIndex = to_underlying_(Global::playedNotesFromAudio[i].note) - to_underlying_(MidiNote::E_1);
      ASSERT(noteIndex < 64);

      notesPlayed |= (1_u64 << noteIndex);
    }
    break;
    case MidiStatus::chan1NoteOff:
    {
      ASSERT(Global::playedNotesFromAudio[i].note >= MidiNote::E_1);
      //ASSERT(Global::playedNotesFromAudio[i].note <= MidiNote::D_sharp_6);

      const u8 noteIndex = to_underlying_(Global::playedNotesFromAudio[i].note) - to_underlying_(MidiNote::E_1);
      ASSERT(noteIndex < 64);

      notesPlayed &= ~(1_u64 << noteIndex);
    }
    break;
    default:
      unreachable();
    }
  }
}

#ifdef SHR3D_SFX_CORE_HEXFIN
static void drawFretboardPlayedNotesByFrequency(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const i8 anchorFret, const i8 anchorWidth)
{
  const f32 frequencyMono = Global::frequencyMono;

  if (frequencyMono != 0.0f)
  {
    const f32 cf = -12.0f * log2f(Global::a4ReferenceFrequency / frequencyMono);
    const f32 referenceFrequency = tunerReferenceFrequency(cf, Global::a4ReferenceFrequency);
    const f32 cents = tunerCents(frequencyMono, referenceFrequency);
    const MidiNote notePlayed = MidiNote(tunerNote(cf));
    for (i8 i = 0; i < ctx.instrumentStringCount; ++i)
    {
      const MidiNote stringNoteZero = getStringNoteZeroTuning(ctx, i);
      const MidiNote stringNoteFret24 = MidiNote(to_underlying_(stringNoteZero) + 24);
      if (stringNoteZero <= notePlayed && notePlayed <= stringNoteFret24)
      {
        const i8 fretIndex = to_underlying_(notePlayed) - to_underlying_(stringNoteZero);
        drawFretboardPlayedNote(ctx, highwayViewProjectionMat, i + ctx.instrumentStringOffset, fretIndex, cents, anchorFret, anchorWidth, Global::inputVolumeMono);
      }
    }
  }
}

#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
static void drawTunerInFret(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const i8 stringIndex, const i8 fretIndex, const f32 cents, const i8 anchorFret, const i8 anchorWidth, const f32 frequency, const f32 volume = -1.0f)
{
  const f32 centsPos = 0.5f + (cents / 100.0f);

  GL(glUseProgram(Shader::worldTunerInFret));

  GL(glUniform1f(Shader::worldTunerInFretUniformCentsPos, centsPos));
  setStringColor(ctx, Shader::worldTunerInFretUniformColor, stringIndex + ctx.instrumentHideFirstStrings, volume);

  if (fretIndex == 0)
  {
    const mat4 model = {
      .m00 = 0.5f * (ctx.instrumentFretPosition[1] - ctx.instrumentFretPosition[0]),
      .m11 = 0.5f * *ctx.instrumentStringSpacing,
      .m13 = f32(reverseStringFix(ctx, stringIndex)) * *ctx.instrumentStringSpacing,
      .m23 = 0.11f
    };

    const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
    GL(glUniformMatrix4fv(Shader::worldTunerInFretUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
  }
  else
  {
    const mat4 model = {
      .m00 = 0.5f * (ctx.instrumentFretPosition[fretIndex] - ctx.instrumentFretPosition[fretIndex - 1]),
      .m11 = 0.3f * *ctx.instrumentStringSpacing,
      .m03 = ctx.instrumentFretPosition[fretIndex - 1] + 0.5f * (ctx.instrumentFretPosition[fretIndex] - ctx.instrumentFretPosition[fretIndex - 1]),
      .m13 = f32(reverseStringFix(ctx, stringIndex)) * *ctx.instrumentStringSpacing,
      .m23 = 0.05f
    };

    const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
    GL(glUniformMatrix4fv(Shader::worldTunerInFretUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
  }

  GL(glDrawElements_(planeZ));
}

static void drawFretboardPlayedNotesByFrequencyDividedForString(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const i8 string, const f32 frequency, const i8 anchorFret, const i8 anchorWidth, const f32 particleSpawnMultiplier)
{
  ASSERT(frequency != 0.0f);

  const f32 cf = -12.0f * log2f(Global::a4ReferenceFrequency / frequency);
  const f32 referenceFrequency = tunerReferenceFrequency(cf, Global::a4ReferenceFrequency);
  const f32 cents = tunerCents(frequency, referenceFrequency);
  const MidiNote notePlayed = MidiNote(tunerNote(cf));
  const MidiNote stringNoteZero = getStringNoteZeroTuning(ctx, string);
  const MidiNote stringNoteFret24 = MidiNote(to_underlying_(stringNoteZero) + 24);
  if (stringNoteZero <= notePlayed && notePlayed <= stringNoteFret24)
  {
    const i8 fretIndex = to_underlying_(notePlayed) - to_underlying_(stringNoteZero);
    drawFretboardPlayedNote(ctx, highwayViewProjectionMat, string + ctx.instrumentStringOffset, fretIndex, cents, anchorFret, anchorWidth, particleSpawnMultiplier);
    if (Settings::highwayFretboardPlayedNotesTuner)
      drawTunerInFret(ctx, highwayViewProjectionMat, string + ctx.instrumentStringOffset, fretIndex, cents, anchorFret, anchorWidth, frequency);
  }
}

static void drawFretboardPlayedNotesByFrequencyDivided(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const i8 anchorFret, const i8 anchorWidth)
{
  const f32 frequency0 = Global::frequency[0];
  const f32 frequency1 = Global::frequency[1];
  const f32 frequency2 = Global::frequency[2];
  const f32 frequency3 = Global::frequency[3];
  const f32 frequency4 = Global::frequency[4];
  const f32 frequency5 = Global::frequency[5];

  if (frequency0 != 0.0f)
    drawFretboardPlayedNotesByFrequencyDividedForString(ctx, highwayViewProjectionMat, 0, frequency0, anchorFret, anchorWidth, map_(clamp(tunerDBFromVolume(Global::volume[0]), -60.0f, 0.0f), -60.0f, 0.0f, 0.0f, 1.0f));
  if (frequency1 != 0.0f)
    drawFretboardPlayedNotesByFrequencyDividedForString(ctx, highwayViewProjectionMat, 1, frequency1, anchorFret, anchorWidth, map_(clamp(tunerDBFromVolume(Global::volume[1]), -60.0f, 0.0f), -60.0f, 0.0f, 0.0f, 1.0f));
  if (frequency2 != 0.0f)
    drawFretboardPlayedNotesByFrequencyDividedForString(ctx, highwayViewProjectionMat, 2, frequency2, anchorFret, anchorWidth, map_(clamp(tunerDBFromVolume(Global::volume[2]), -60.0f, 0.0f), -60.0f, 0.0f, 0.0f, 1.0f));
  if (frequency3 != 0.0f)
    drawFretboardPlayedNotesByFrequencyDividedForString(ctx, highwayViewProjectionMat, 3, frequency3, anchorFret, anchorWidth, map_(clamp(tunerDBFromVolume(Global::volume[3]), -60.0f, 0.0f), -60.0f, 0.0f, 0.0f, 1.0f));
  if (frequency4 != 0.0f)
    drawFretboardPlayedNotesByFrequencyDividedForString(ctx, highwayViewProjectionMat, 4, frequency4, anchorFret, anchorWidth, map_(clamp(tunerDBFromVolume(Global::volume[4]), -60.0f, 0.0f), -60.0f, 0.0f, 0.0f, 1.0f));
  if (frequency5 != 0.0f)
    drawFretboardPlayedNotesByFrequencyDividedForString(ctx, highwayViewProjectionMat, 5, frequency5, anchorFret, anchorWidth, map_(clamp(tunerDBFromVolume(Global::volume[5]), -60.0f, 0.0f), -60.0f, 0.0f, 0.0f, 1.0f));
}
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED
#endif // SHR3D_SFX_CORE_HEXFIN

static void drawFretboardPlayedNotes(const Highway::Ctx& ctx, const mat4& highwayViewProjectionMat, const i8 anchorFret, const i8 anchorWidth)
{
  switch (Settings::tunerNoteDetectionSource)
  {
#ifdef SHR3D_SFX_CORE_HEXFIN
  case NoteDetectionSource::pickup:
#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
    if (IS_DIVIDED_PICKUP_ENABLED)
      drawFretboardPlayedNotesByFrequencyDivided(ctx, highwayViewProjectionMat, anchorFret, anchorWidth);
    else
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED
      drawFretboardPlayedNotesByFrequency(ctx, highwayViewProjectionMat, anchorFret, anchorWidth);
    break;
#endif // SHR3D_SFX_CORE_HEXFIN
#ifdef SHR3D_MIDI
  case NoteDetectionSource::midiDevice:
#endif // SHR3D_MIDI
  case NoteDetectionSource::midiPlugin:
    static u64 notesPlayed;
    if (Global::playedNotesFromAudioIndex > 0)
    {
      const std::lock_guard lock(Global::playedNotesFromAudioMutex);
      updateNotesPlayed(notesPlayed);
    }
    drawFretboardPlayedNotesByMidi(ctx, highwayViewProjectionMat, notesPlayed, anchorFret, anchorWidth);
    break;
  }

  Global::playedNotesFromAudioIndex = 0;
}

static void drawFretboardCollisionNote(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, i32 currentAnchorIndex, const Song::Note& note)
{
  const TimeNS noteTime = -note.timeNS + Global::musicTimeElapsedNS;

  if (noteTime - note.sustainNS > 0)
    return;
  if (noteTime + timeNS_From_Seconds(Const::highwayNoteDetectionTimeOffset) < 0)
    return;

  GL(glUseProgram(Shader::collisionNote));

  GL(glUniform1f(Shader::collisionNoteUniformSustain, TimeNS_To_Seconds(noteTime)));

  setStringColor(ctx, Shader::collisionNoteUniformColor, note.string + ctx.instrumentStringOffset + ctx.instrumentHideFirstStrings, 1.0f);

  if (note.fret == 0)
  {
    for (; currentAnchorIndex < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors.size()); ++currentAnchorIndex)
    {
      const TimeNS anchorNoteTimeEnd = -getAnchorTimeEndNS(selectedArrangementIndex, currentAnchorIndex) + Global::musicTimeElapsedNS;

      if (anchorNoteTimeEnd < noteTime)
        break;
    }

    const Song::Anchor& anchor = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex];

    { // Zero Note
      {
        const mat4 model = {
          .m00 = 0.5f * (ctx.instrumentFretPosition[anchor.fret + anchor.width - 1] - ctx.instrumentFretPosition[anchor.fret - 1]) - 0.02f,
          .m11 = 0.6f * *ctx.instrumentStringSpacing,
          .m03 = ctx.instrumentFretPosition[anchor.fret - 1] + 0.5f * (ctx.instrumentFretPosition[anchor.fret + anchor.width - 1] - ctx.instrumentFretPosition[anchor.fret - 1]),
          .m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing,
          .m23 = -0.092f
        };
        const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
        GL(glUniformMatrix4fv(Shader::collisionNoteUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
      }

      GL(glDrawElements_(planeZ));
    }

#ifdef SHR3D_PARTICLE
    if (Settings::highwayParticleCollisionNotes)
    {
      const Particle::HighwaySpawn highwaySpawn = {
        .position = {
           .x = ctx.instrumentFretPosition[anchor.fret - 1] + 0.5f * (ctx.instrumentFretPosition[anchor.fret + anchor.width - 1] - ctx.instrumentFretPosition[anchor.fret - 1]),
           .y = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing,
           .z = 0.1f
        },
        .width = ctx.instrumentFretPosition[anchor.fret + anchor.width - 1] - ctx.instrumentFretPosition[anchor.fret - 1],
        .height = *ctx.instrumentStringSpacing,
        .color = ctx.instrumentStringColors[note.string + ctx.instrumentStringOffset + ctx.instrumentHideFirstStrings],
        .spawnMultiplier = 1.0f
      };

      Particle::highwayFretboardSpawn(highwaySpawn);
    }
#endif // SHR3D_PARTICLE
  }
  else
  {
    { // Note
      {
        const mat4 model = {
          .m00 = 0.27f * Settings::highwayFretboardNoteWidth,
          .m11 = 0.6f * *ctx.instrumentStringSpacing * Settings::highwayFretboardNoteHeight,
          .m03 = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]),
          .m13 = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing,
          .m23 = 0.092f
        };
        const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
        GL(glUniformMatrix4fv(Shader::collisionNoteUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
      }

      GL(glDrawElements_(planeZ));
    }

    { // Left and Right Fret
      GL(glUseProgram(Shader::collisionFret));

      GL(glUniform4f(Shader::collisionFretUniformColor, Settings::highwayFretboardPlayedNotesDotColor[0].r, Settings::highwayFretboardPlayedNotesDotColor[0].g, Settings::highwayFretboardPlayedNotesDotColor[0].b, Settings::highwayFretboardPlayedNotesDotColor[0].a));

      mat4 model = {
        .m00 = 0.05f,
        .m11 = 0.5f * (f32(ctx.instrumentStringCount - 1) * *ctx.instrumentStringSpacing) + 0.40f,
        .m13 = 0.5f * (f32(ctx.instrumentStringCount - 1) * *ctx.instrumentStringSpacing),
        .m23 = 0.18f
      };

      { // Left Fret
        {
          model.m03 = ctx.instrumentFretPosition[note.fret - 1];
          const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
          GL(glUniformMatrix4fv(Shader::collisionFretUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
        }

        GL(glDrawElements_(planeZ));
      }
      { // Right Fret
        {
          model.m03 = ctx.instrumentFretPosition[note.fret];
          const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
          GL(glUniformMatrix4fv(Shader::collisionFretUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
        }

        GL(glDrawElements_(planeZ));
      }
    }

#ifdef SHR3D_PARTICLE
    if (Settings::highwayParticleCollisionNotes)
    {
      const Particle::HighwaySpawn highwaySpawn = {
        .position = {
           .x = ctx.instrumentFretPosition[note.fret - 1] + 0.5f * (ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1]),
           .y = f32(reverseStringFix(ctx, note.string + ctx.instrumentStringOffset)) * *ctx.instrumentStringSpacing,
           .z = 0.1f
        },
        .width = ctx.instrumentFretPosition[note.fret] - ctx.instrumentFretPosition[note.fret - 1],
        .height = *ctx.instrumentStringSpacing,
        .color = ctx.instrumentStringColors[note.string + ctx.instrumentStringOffset + ctx.instrumentHideFirstStrings],
        .spawnMultiplier = 0.5f
      };

      Particle::highwayFretboardSpawn(highwaySpawn);
    }
#endif // SHR3D_PARTICLE
  }
}

static void drawFretboardCollisionHandshapeAndChords(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, i32 currentAnchorIndex, i32 sustainIndex, i32 chordBeginIndex, i32 chordEndIndex)
{
  const Song::Sustain& sustain = Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains[sustainIndex];

  const TimeNS noteTimeBegin = -sustain.startTimeNS + Global::musicTimeElapsedNS;
  const TimeNS noteTimeEnd = -sustain.endTimeNS + Global::musicTimeElapsedNS;

  if (noteTimeEnd > 0)
    return;
  if (noteTimeBegin + timeNS_From_Seconds(Const::highwayNoteDetectionTimeOffset) < 0)
    return;

  if (chordEndIndex == -1)
    return;

  {
    for (; currentAnchorIndex < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors.size()); ++currentAnchorIndex)
    {
      const TimeNS anchorNoteTimeEnd = -getAnchorTimeEndNS(selectedArrangementIndex, currentAnchorIndex) + Global::musicTimeElapsedNS;

      if (anchorNoteTimeEnd < noteTimeBegin)
        break;
    }
    const Song::Anchor& anchor = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex];

    f32 sustainSec = F32::inf;
    for (i32 j = chordEndIndex; j >= chordBeginIndex; --j)
    {
      const Song::Chord& chord = Global::songTrackLevelAdjusted[selectedArrangementIndex].chords[j];

      const TimeNS noteTime = -chord.timeNS + Global::musicTimeElapsedNS;

      if (noteTime + timeNS_From_Seconds(Const::highwayNoteDetectionTimeOffset) < 0)
        continue;

      sustainSec = TimeNS_To_Seconds(noteTime);
      break;
    }
    //ASSERT(sustain != F32::inf);

    { // draw ChordBox
      GL(glUseProgram(Shader::collisionChord));
      GL(glUniform4f(Shader::collisionChordUniformColor, Settings::highwayFretboardPlayedNotesDotColor[1].r, Settings::highwayFretboardPlayedNotesDotColor[1].g, Settings::highwayFretboardPlayedNotesDotColor[1].b, Settings::highwayFretboardPlayedNotesDotColor[1].a));
      GL(glUniform1f(Shader::collisionChordUniformSustain, sustainSec));
      GL(glUniformMatrix4fv(Shader::collisionChordUniformModelViewProjection, 1, GL_FALSE, &highwayViewProjectionMat.m00));

      {
        const mat4 model = {
          .m00 = 0.5f * (ctx.instrumentFretPosition[anchor.fret + anchor.width - 1] - ctx.instrumentFretPosition[anchor.fret - 1]) + 0.1f,
          .m11 = 0.5f * (f32(ctx.instrumentStringCount) * *ctx.instrumentStringSpacing + 0.20f * *ctx.instrumentStringSpacing) + 0.1f,
          .m03 = 0.5f * (ctx.instrumentFretPosition[anchor.fret + anchor.width - 1] + ctx.instrumentFretPosition[anchor.fret - 1]),
          .m13 = *ctx.instrumentStringSpacing * (0.5f * (f32(ctx.instrumentStringCount) - 0.40f)),
          .m23 = 0.185f
        };
        const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
        GL(glUniformMatrix4fv(Shader::collisionChordUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
      }

      GL(glDrawElements_(planeZ));
    }

#ifdef SHR3D_PARTICLE
    if (Settings::highwayParticleCollisionNotes)
    {
      { // left
        const Particle::HighwaySpawn highwaySpawn = {
          .position = {
             .x = ctx.instrumentFretPosition[anchor.fret - 1],
             .y = -0.35f,
             .z = -0.1f
          },
          .width = 0.2f,
          .height = 0.1f,
          .color = Settings::highwaySustainColor,
          .spawnMultiplier = 0.05f
        };

        Particle::highwayFretboardSpawn(highwaySpawn);
      }
      { // right
        const Particle::HighwaySpawn highwaySpawn = {
          .position = {
             .x = ctx.instrumentFretPosition[anchor.fret + anchor.width - 1],
             .y = -0.35f,
             .z = -0.1f
          },
          .width = 0.2f,
          .height = 0.1f,
          .color = Settings::highwaySustainColor,
          .spawnMultiplier = 0.05f
        };

        Particle::highwayFretboardSpawn(highwaySpawn);
      }
    }
#endif // SHR3D_PARTICLE
  }
}

static void drawFretboardCollisionNotes(const Highway::Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat, i32 currentAnchorIndex, i32 noteIndex, i32 sustainIndex, i32 chordIndex)
{
  if (noteIndex >= 0)
  {
    i32 noteBeginIndex = noteIndex;
    while (noteBeginIndex >= 1 && Global::songTrackLevelAdjusted[selectedArrangementIndex].notes[noteBeginIndex].timeNS - Global::musicTimeElapsedNS > timeNS_From_Seconds(-Const::highwayNoteDetectionTimeOffset))
      --noteBeginIndex;
    ASSERT(noteBeginIndex >= 0);

    i32 noteEndIndex = noteIndex;
    while (noteEndIndex < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].notes.size()) - 1 && Global::songTrackLevelAdjusted[selectedArrangementIndex].notes[noteEndIndex].timeNS - Global::musicTimeElapsedNS < timeNS_From_Seconds(Const::highwayNoteDetectionTimeOffset))
      ++noteEndIndex;
    ASSERT(noteEndIndex < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].notes.size()));

    for (i32 i = noteEndIndex; i >= noteBeginIndex; --i)
      drawFretboardCollisionNote(ctx, selectedArrangementIndex, highwayViewProjectionMat, currentAnchorIndex, Global::songTrackLevelAdjusted[selectedArrangementIndex].notes[i]);
  }
  if (sustainIndex >= 0 && chordIndex >= 0)
  {
    i32 sustainBeginIndex = sustainIndex;
    while (sustainBeginIndex >= 1 && Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains[sustainBeginIndex].startTimeNS - Global::musicTimeElapsedNS > timeNS_From_Seconds(-Const::highwayNoteDetectionTimeOffset))
      --sustainBeginIndex;
    ASSERT(sustainBeginIndex >= 0);

    i32 sustainEndIndex = sustainIndex;
    while (sustainEndIndex < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains.size()) - 1 && Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains[sustainEndIndex].startTimeNS - Global::musicTimeElapsedNS < timeNS_From_Seconds(Const::highwayNoteDetectionTimeOffset))
      ++sustainEndIndex;
    ASSERT(sustainEndIndex < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].sustains.size()));

    i32 chordBeginIndex = chordIndex;
    while (chordBeginIndex >= 1 && Global::songTrackLevelAdjusted[selectedArrangementIndex].chords[chordBeginIndex].timeNS - Global::musicTimeElapsedNS > timeNS_From_Seconds(-Const::highwayNoteDetectionTimeOffset))
      --chordBeginIndex;
    ASSERT(chordBeginIndex >= 0);

    i32 chordEndIndex = chordIndex;
    while (chordEndIndex < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].chords.size()) - 1 && Global::songTrackLevelAdjusted[selectedArrangementIndex].chords[chordEndIndex].timeNS - Global::musicTimeElapsedNS < timeNS_From_Seconds(Const::highwayNoteDetectionTimeOffset))
      ++chordEndIndex;
    ASSERT(chordEndIndex < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].chords.size()));

    for (i32 i = sustainEndIndex; i >= sustainBeginIndex; --i)
      drawFretboardCollisionHandshapeAndChords(ctx, selectedArrangementIndex, highwayViewProjectionMat, currentAnchorIndex, i, chordBeginIndex, chordEndIndex);
  }
}

void Highway::render(const Ctx& ctx, const ArrangementIndex selectedArrangementIndex, const mat4& highwayViewProjectionMat)
{
  if (Global::selectedSongIndex == -1)
    return;
  if (ctx.instrumentStringColors == nullptr)
    return;

  f32 fretboardNearestNoteDistance[Const::highwayInstrumentGuitarStringCount][25] = { };

  const TrackIndices trackIndices = getTrackIndices(selectedArrangementIndex);

  //{
  //  GL(glUseProgram(Shader::worldNoTexture));
  //  setStringColor(ctx, Shader::worldNoTextureUniformColor, 3, 1.0f);
  //  mat4 model;
  //  const mat4 mvp = vec::multiply(highwayViewProjectionMat, model);
  //  GL(glUniformMatrix4fv(Shader::worldNoTextureUniformModelViewProjection, 1, GL_FALSE, &mvp.m00));
  //  GL(glDrawElements_(planeZ));
  //}

  drawGroundFrets(ctx, selectedArrangementIndex, highwayViewProjectionMat, trackIndices.currentAnchor);
  if (Settings::highwayBeat)
    drawGroundBeats(ctx, selectedArrangementIndex, highwayViewProjectionMat, trackIndices.currentAnchor, trackIndices.beatBegin, trackIndices.beatEnd);
  if (Settings::highwayBeatStrumDirection)
    drawGroundStrumDirections(ctx, selectedArrangementIndex, highwayViewProjectionMat, trackIndices.currentAnchor, trackIndices.beatBegin, trackIndices.beatEnd);
  drawGroundQuickRepeater(ctx, selectedArrangementIndex, highwayViewProjectionMat, trackIndices.currentAnchor);
  drawGroundAnchors(ctx, selectedArrangementIndex, highwayViewProjectionMat, trackIndices.currentAnchor);

  if (trackIndices.arpeggioBegin >= 0)
    drawArpeggioGroundSustain(ctx, selectedArrangementIndex, highwayViewProjectionMat, trackIndices.currentAnchor, trackIndices.arpeggioBegin, trackIndices.arpeggioEnd);
  if (trackIndices.sustainBegin >= 0)
    drawChordGroundSustain(ctx, selectedArrangementIndex, highwayViewProjectionMat, trackIndices.currentAnchor, trackIndices.sustainBegin, trackIndices.sustainEnd);

  drawNotes(ctx, selectedArrangementIndex, highwayViewProjectionMat, fretboardNearestNoteDistance, trackIndices.currentAnchor, trackIndices.noteBegin, trackIndices.noteEnd, trackIndices.sustainBegin, trackIndices.sustainEnd);

  drawChordsAndArpeggios(ctx, selectedArrangementIndex, highwayViewProjectionMat, fretboardNearestNoteDistance, trackIndices.currentAnchor, trackIndices.sustainBegin, trackIndices.sustainEnd, trackIndices.noteBegin, trackIndices.noteEnd, trackIndices.chordBegin, trackIndices.chordEnd, trackIndices.arpeggioBegin, trackIndices.arpeggioEnd);

  drawFretboardCapo(ctx, highwayViewProjectionMat);
  if (Settings::highwayFretboardStrings)
    drawFretboardStrings(ctx, highwayViewProjectionMat, fretboardNearestNoteDistance);
  if (Settings::highwayFretboardFrets)
    drawFretboardFrets(ctx, highwayViewProjectionMat);
  drawFretboardNearestNote(ctx, highwayViewProjectionMat, fretboardNearestNoteDistance);
  drawFretboardDotInlays(ctx, selectedArrangementIndex, highwayViewProjectionMat, trackIndices.currentAnchor);
  drawFretboardFretNumbers(ctx, selectedArrangementIndex, highwayViewProjectionMat, trackIndices.currentAnchor);

  if (Settings::highwayFretboardStringNoteNames)
    drawFretboardStringNoteNames(ctx, highwayViewProjectionMat);
  if (Settings::highwayFretboardNoteNames)
    drawFretboardNearestNoteNames(ctx, highwayViewProjectionMat);

#ifdef SHR3D_SFX_CORE_HEXFIN
  if (Settings::highwayTuner)
    drawTuners(ctx, highwayViewProjectionMat);
#endif // SHR3D_SFX_CORE_HEXFIN

  if (Settings::highwayVUMeter)
    drawVUMeters(ctx, highwayViewProjectionMat);

  if (Settings::highwayFretboardPlayedNotesDot
#ifdef SHR3D_SFX_CORE_HEXFIN
    || Settings::highwayFretboardPlayedNotesTuner
#endif // SHR3D_SFX_CORE_HEXFIN
#ifdef SHR3D_PARTICLE
    || Settings::highwayParticlePlayedNotes
#endif // SHR3D_PARTICLE
    )
  {
    const Song::Anchor& currentAnchor = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[trackIndices.currentAnchor];
    drawFretboardPlayedNotes(ctx, highwayViewProjectionMat, currentAnchor.fret, currentAnchor.width);
  }

  if (Settings::highwayFretboardCollisionNotes
#ifdef SHR3D_PARTICLE
    || Settings::highwayParticleCollisionNotes
#endif // SHR3D_PARTICLE
    )
    drawFretboardCollisionNotes(ctx, selectedArrangementIndex, highwayViewProjectionMat, trackIndices.currentAnchor, trackIndices.noteBegin, trackIndices.sustainBegin, trackIndices.chordBegin);
}
