Jelajahi Sumber

Re-add strain graphs

kiwec 3 bulan lalu
induk
melakukan
a5b84087b5

TEMPAT SAMPAH
libraries/rosu-pp-c/Win32/rosu_pp_c.lib


TEMPAT SAMPAH
libraries/rosu-pp-c/x64/rosu_pp_c.lib


+ 66 - 5
rosu-pp-c/src/lib.rs

@@ -6,18 +6,66 @@ use libc;
 use rosu_pp;
 
 #[repr(C)]
-#[derive(Default)]
 pub struct pp_info {
     pub total_stars: f64,
     pub aim_stars: f64,
     pub speed_stars: f64,
+
     pub pp: f64,
+
     pub num_objects: u32,
     pub num_circles: u32,
     pub num_spinners: u32,
+
+    pub aim_strains: *mut f64,
+    pub aim_strains_len: usize,
+    pub aim_strains_capacity: usize,
+    pub speed_strains: *mut f64,
+    pub speed_strains_len: usize,
+    pub speed_strains_capacity: usize,
+
     pub ok: bool,
 }
 
+impl Default for pp_info {
+    fn default() -> Self {
+        pp_info {
+            total_stars: 0.0,
+            aim_stars: 0.0,
+            speed_stars: 0.0,
+
+            pp: 0.0,
+
+            num_objects: 0,
+            num_circles: 0,
+            num_spinners: 0,
+
+            aim_strains: std::ptr::null_mut(),
+            aim_strains_len: 0,
+            aim_strains_capacity: 0,
+            speed_strains: std::ptr::null_mut(),
+            speed_strains_len: 0,
+            speed_strains_capacity: 0,
+
+            ok: false,
+        }
+    }
+}
+
+#[no_mangle]
+pub extern "C" fn free_pp_info(info: pp_info) {
+    if !info.aim_strains.is_null() {
+        unsafe {
+            Vec::from_raw_parts(info.aim_strains, info.aim_strains_len, info.aim_strains_capacity);
+        }
+    }
+    if !info.speed_strains.is_null() {
+        unsafe {
+            Vec::from_raw_parts(info.speed_strains, info.speed_strains_len, info.speed_strains_capacity);
+        }
+    }
+}
+
 #[no_mangle]
 pub extern "C" fn calculate_full_pp(path: *const libc::c_char, mod_flags: u32, ar: f32, cs: f32, od: f32, speed_multiplier: f64) -> pp_info {
     let c_path = unsafe { std::ffi::CStr::from_ptr(path) };
@@ -33,23 +81,36 @@ pub extern "C" fn calculate_full_pp(path: *const libc::c_char, mod_flags: u32, a
         Err(_) => return pp_info::default()
     };
 
-    let diff_attrs = rosu_pp::Difficulty::new()
+    let diff = rosu_pp::Difficulty::new()
         .mods(mod_flags)
         .ar(ar, false)
         .cs(cs, false)
         .od(od, false)
-        .clock_rate(speed_multiplier)
-        .with_mode()
-        .calculate(&osu_map);
+        .clock_rate(speed_multiplier);
+
+    let diff_attrs = diff.with_mode().calculate(&osu_map);
+    let diff_strains = diff.with_mode().strains(&osu_map);
+    let mut aim_strains = std::mem::ManuallyDrop::new(diff_strains.aim);
+    let mut speed_strains = std::mem::ManuallyDrop::new(diff_strains.speed);
 
     let mut result = pp_info {
         total_stars: diff_attrs.stars,
         aim_stars: diff_attrs.aim,
         speed_stars: diff_attrs.speed,
+
         pp: 0.0,
+
         num_objects: diff_attrs.n_circles + diff_attrs.n_sliders + diff_attrs.n_spinners,
         num_circles: diff_attrs.n_circles,
         num_spinners: diff_attrs.n_spinners,
+
+        aim_strains: aim_strains.as_mut_ptr(),
+        aim_strains_len: aim_strains.len(),
+        aim_strains_capacity: aim_strains.capacity(),
+        speed_strains: speed_strains.as_mut_ptr(),
+        speed_strains_len: speed_strains.len(),
+        speed_strains_capacity: speed_strains.capacity(),
+
         ok: true,
     };
 

+ 16 - 5
src/App/Osu/Beatmap.cpp

@@ -681,6 +681,8 @@ void Beatmap::select() {
 }
 
 void Beatmap::selectDifficulty2(DatabaseBeatmap *difficulty2) {
+    auto previous_diff = m_selectedDifficulty2;
+
     if(difficulty2 != NULL) {
         m_selectedDifficulty2 = difficulty2;
 
@@ -691,12 +693,21 @@ void Beatmap::selectDifficulty2(DatabaseBeatmap *difficulty2) {
 
     if(osu_beatmap_preview_mods_live.getBool()) onModUpdate();
 
-    // Request full pp recomputation
-    if(m_selectedDifficulty2 && !m_selectedDifficulty2->do_not_store) {
-        m_selectedDifficulty2->m_calculate_full_pp =
-            std::async(std::launch::async, calculate_full_pp, m_selectedDifficulty2->m_sFilePath.c_str(),
-                       osu->getScore()->getModsLegacy(), getAR(), getCS(), getOD(), osu->getSpeedMultiplier());
+    if(previous_diff != difficulty2) {
+        // TODO @kiwec: commented out because this still gets triggered when
+        //              starting a map, thus erasing strains from schrubbing timeline
+        // m_aimStrains.clear();
+        // m_speedStrains.clear();
+
+        // Request full pp recomputation
+        if(m_selectedDifficulty2 && !m_selectedDifficulty2->do_not_store) {
+            // TODO @kiwec: free pp_info of previous m_calculate_full_pp, if applicable
+            m_selectedDifficulty2->m_calculate_full_pp =
+                std::async(std::launch::async, calculate_full_pp, m_selectedDifficulty2->m_sFilePath.c_str(),
+                           osu->getScore()->getModsLegacy(), getAR(), getCS(), getOD(), osu->getSpeedMultiplier());
+        }
     }
+
 }
 
 void Beatmap::deselect() {

+ 3 - 0
src/App/Osu/Beatmap.h

@@ -178,6 +178,9 @@ class Beatmap {
                                                               m_speedNotesForNumHitObjects.size() - 1)]
                     : 0);
     }
+    
+    std::vector<double> m_aimStrains;
+    std::vector<double> m_speedStrains;
 
     // set to false when using non-vanilla mods (disables score submission)
     bool vanilla = true;

+ 1 - 0
src/App/Osu/Changelog.cpp

@@ -32,6 +32,7 @@ Changelog::Changelog() : ScreenBackable() {
     latest.changes.push_back("- Changed \"Open Skins folder\" button to open the currently selected skin's folder");
     latest.changes.push_back("- Fixed sliderslide and spinnerspin sounds not looping");
     latest.changes.push_back("- Improved sound engine reliability");
+    latest.changes.push_back("- Re-added strain graphs");
     latest.changes.push_back("- Removed sliderhead fadeout animation (set osu_slider_sliderhead_fadeout to 1 for old behavior)");
     changelogs.push_back(latest);
 

+ 1 - 0
src/App/Osu/DatabaseBeatmap.cpp

@@ -178,6 +178,7 @@ DatabaseBeatmap::~DatabaseBeatmap() {
     if(m_calculate_full_pp.valid()) {
         m_calculate_full_pp.wait();
         m_calculate_full_pp = std::future<pp_info>();
+        // TODO @kiwec: free pp_info
     }
 
     if(m_difficulties != NULL) {

+ 94 - 0
src/App/Osu/HUD.cpp

@@ -214,6 +214,7 @@ ConVar osu_draw_accuracy("osu_draw_accuracy", true, FCVAR_DEFAULT);
 ConVar osu_draw_target_heatmap("osu_draw_target_heatmap", true, FCVAR_DEFAULT);
 ConVar osu_draw_scrubbing_timeline("osu_draw_scrubbing_timeline", true, FCVAR_DEFAULT);
 ConVar osu_draw_scrubbing_timeline_breaks("osu_draw_scrubbing_timeline_breaks", true, FCVAR_DEFAULT);
+ConVar osu_draw_scrubbing_timeline_strain_graph("osu_draw_scrubbing_timeline_strain_graph", false, FCVAR_DEFAULT);
 ConVar osu_draw_continue("osu_draw_continue", true, FCVAR_DEFAULT);
 ConVar osu_draw_scoreboard("osu_draw_scoreboard", true, FCVAR_DEFAULT);
 ConVar osu_draw_scoreboard_mp("osu_draw_scoreboard_mp", true, FCVAR_DEFAULT);
@@ -2260,6 +2261,99 @@ void HUD::drawScrubbingTimeline(Graphics *g, unsigned long beatmapTime, unsigned
     const unsigned long endTimeMS = startTimeMS + lengthMS;
     const unsigned long currentTimeMS = beatmapTime;
 
+    // draw strain graph
+    if(osu_draw_scrubbing_timeline_strain_graph.getBool()) {
+        const std::vector<double> &aimStrains = osu->getSelectedBeatmap()->m_aimStrains;
+        const std::vector<double> &speedStrains = osu->getSelectedBeatmap()->m_speedStrains;
+        const float speedMultiplier = osu->getSpeedMultiplier();
+
+        if(aimStrains.size() > 0 && aimStrains.size() == speedStrains.size()) {
+            const float strainStepMS = 400.0f * speedMultiplier;
+
+            // get highest strain values for normalization
+            double highestAimStrain = 0.0;
+            double highestSpeedStrain = 0.0;
+            double highestStrain = 0.0;
+            int highestStrainIndex = -1;
+            for(int i = 0; i < aimStrains.size(); i++) {
+                const double aimStrain = aimStrains[i];
+                const double speedStrain = speedStrains[i];
+                const double strain = aimStrain + speedStrain;
+
+                if(strain > highestStrain) {
+                    highestStrain = strain;
+                    highestStrainIndex = i;
+                }
+                if(aimStrain > highestAimStrain) highestAimStrain = aimStrain;
+                if(speedStrain > highestSpeedStrain) highestSpeedStrain = speedStrain;
+            }
+
+            // draw strain bar graph
+            if(highestAimStrain > 0.0 && highestSpeedStrain > 0.0 && highestStrain > 0.0) {
+                const float msPerPixel =
+                    (float)lengthMS /
+                    (float)(osu->getScreenWidth() - (((float)startTimeMS / (float)endTimeMS)) * osu->getScreenWidth());
+                const float strainWidth = strainStepMS / msPerPixel;
+                const float strainHeightMultiplier = osu_hud_scrubbing_timeline_strains_height.getFloat() * dpiScale;
+
+                const float offsetX =
+                    ((float)startTimeMS / (float)strainStepMS) * strainWidth;  // compensate for startTimeMS
+
+                const float alpha = osu_hud_scrubbing_timeline_strains_alpha.getFloat() * galpha;
+                const Color aimStrainColor =
+                    COLORf(alpha, osu_hud_scrubbing_timeline_strains_aim_color_r.getInt() / 255.0f,
+                           osu_hud_scrubbing_timeline_strains_aim_color_g.getInt() / 255.0f,
+                           osu_hud_scrubbing_timeline_strains_aim_color_b.getInt() / 255.0f);
+                const Color speedStrainColor =
+                    COLORf(alpha, osu_hud_scrubbing_timeline_strains_speed_color_r.getInt() / 255.0f,
+                           osu_hud_scrubbing_timeline_strains_speed_color_g.getInt() / 255.0f,
+                           osu_hud_scrubbing_timeline_strains_speed_color_b.getInt() / 255.0f);
+
+                g->setDepthBuffer(true);
+                for(int i = 0; i < aimStrains.size(); i++) {
+                    const double aimStrain = (aimStrains[i]) / highestStrain;
+                    const double speedStrain = (speedStrains[i]) / highestStrain;
+                    // const double strain = (aimStrains[i] + speedStrains[i]) / highestStrain;
+
+                    const double aimStrainHeight = aimStrain * strainHeightMultiplier;
+                    const double speedStrainHeight = speedStrain * strainHeightMultiplier;
+                    // const double strainHeight = strain * strainHeightMultiplier;
+
+                    g->setColor(aimStrainColor);
+                    g->fillRect(i * strainWidth + offsetX, cursorPos.y - aimStrainHeight,
+                                max(1.0f, std::round(strainWidth + 0.5f)), aimStrainHeight);
+
+                    g->setColor(speedStrainColor);
+                    g->fillRect(i * strainWidth + offsetX, cursorPos.y - aimStrainHeight - speedStrainHeight,
+                                max(1.0f, std::round(strainWidth + 0.5f)), speedStrainHeight + 1);
+                }
+                g->setDepthBuffer(false);
+
+                // highlight highest total strain value (+- section block)
+                if(highestStrainIndex > -1) {
+                    const double aimStrain = (aimStrains[highestStrainIndex]) / highestStrain;
+                    const double speedStrain = (speedStrains[highestStrainIndex]) / highestStrain;
+                    // const double strain = (aimStrains[i] + speedStrains[i]) / highestStrain;
+
+                    const double aimStrainHeight = aimStrain * strainHeightMultiplier;
+                    const double speedStrainHeight = speedStrain * strainHeightMultiplier;
+                    // const double strainHeight = strain * strainHeightMultiplier;
+
+                    Vector2 topLeftCenter = Vector2(highestStrainIndex * strainWidth + offsetX + strainWidth / 2.0f,
+                                                    cursorPos.y - aimStrainHeight - speedStrainHeight);
+
+                    const float margin = 5.0f * dpiScale;
+
+                    g->setColor(0xffffffff);
+                    g->setAlpha(alpha);
+                    g->drawRect(topLeftCenter.x - margin * strainWidth, topLeftCenter.y - margin * strainWidth,
+                                strainWidth * 2 * margin,
+                                aimStrainHeight + speedStrainHeight + 2 * margin * strainWidth);
+                }
+            }
+        }
+    }
+
     // breaks
     g->setColor(greyTransparent);
     g->setAlpha(galpha);

+ 9 - 0
src/App/Osu/OptionsMenu.cpp

@@ -578,6 +578,15 @@ OptionsMenu::OptionsMenu() : ScreenBackable() {
                 "from ALL scores?\n(Even from ones you got in your database because you watched a replay?)",
                 convar->getConVarByName("osu_user_switcher_include_legacy_scores_for_names"));
 
+    addSubSection("Songbrowser");
+    addCheckbox("Draw Strain Graph in Songbrowser",
+                "Hold either SHIFT/CTRL to show only speed/aim strains.\nSpeed strain is red, aim strain is "
+                "green.\n(See osu_hud_scrubbing_timeline_strains_*)",
+                convar->getConVarByName("osu_draw_songbrowser_strain_graph"));
+    addCheckbox("Draw Strain Graph in Scrubbing Timeline",
+                "Speed strain is red, aim strain is green.\n(See osu_hud_scrubbing_timeline_strains_*)",
+                convar->getConVarByName("osu_draw_scrubbing_timeline_strain_graph"));
+
     addSubSection("Window");
     addCheckbox("Pause on Focus Loss", "Should the game pause when you switch to another application?",
                 convar->getConVarByName("osu_pause_on_focus_loss"));

+ 148 - 0
src/App/Osu/SongBrowser/SongBrowser.cpp

@@ -67,6 +67,7 @@ ConVar osu_songbrowser_bottombar_percent("osu_songbrowser_bottombar_percent", 0.
 
 ConVar osu_draw_songbrowser_background_image("osu_draw_songbrowser_background_image", true, FCVAR_DEFAULT);
 ConVar osu_draw_songbrowser_menu_background_image("osu_draw_songbrowser_menu_background_image", true, FCVAR_DEFAULT);
+ConVar osu_draw_songbrowser_strain_graph("osu_draw_songbrowser_strain_graph", false, FCVAR_DEFAULT);
 ConVar osu_songbrowser_scorebrowser_enabled("osu_songbrowser_scorebrowser_enabled", true, FCVAR_DEFAULT);
 ConVar osu_songbrowser_background_fade_in_duration("osu_songbrowser_background_fade_in_duration", 0.1f, FCVAR_DEFAULT);
 
@@ -345,6 +346,25 @@ SongBrowser::SongBrowser() : ScreenBackable() {
     m_osu_scores_enabled = convar->getConVarByName("osu_scores_enabled");
     m_name_ref = convar->getConVarByName("name");
 
+    m_osu_draw_scrubbing_timeline_strain_graph_ref =
+        convar->getConVarByName("osu_draw_scrubbing_timeline_strain_graph");
+    m_osu_hud_scrubbing_timeline_strains_height_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_height");
+    m_osu_hud_scrubbing_timeline_strains_alpha_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_alpha");
+    m_osu_hud_scrubbing_timeline_strains_aim_color_r_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_aim_color_r");
+    m_osu_hud_scrubbing_timeline_strains_aim_color_g_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_aim_color_g");
+    m_osu_hud_scrubbing_timeline_strains_aim_color_b_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_aim_color_b");
+    m_osu_hud_scrubbing_timeline_strains_speed_color_r_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_speed_color_r");
+    m_osu_hud_scrubbing_timeline_strains_speed_color_g_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_speed_color_g");
+    m_osu_hud_scrubbing_timeline_strains_speed_color_b_ref =
+        convar->getConVarByName("osu_hud_scrubbing_timeline_strains_speed_color_b");
+
     m_osu_draw_statistics_perfectpp_ref = convar->getConVarByName("osu_draw_statistics_perfectpp");
     m_osu_draw_statistics_totalstars_ref = convar->getConVarByName("osu_draw_statistics_totalstars");
 
@@ -629,6 +649,122 @@ void SongBrowser::draw(Graphics *g) {
     // draw score browser
     m_scoreBrowser->draw(g);
 
+    // draw strain graph of currently selected beatmap
+    if(osu_draw_songbrowser_strain_graph.getBool()) {
+        const std::vector<double> &aimStrains = getSelectedBeatmap()->m_aimStrains;
+        const std::vector<double> &speedStrains = getSelectedBeatmap()->m_speedStrains;
+        const float speedMultiplier = osu->getSpeedMultiplier();
+
+        if(aimStrains.size() > 0 && aimStrains.size() == speedStrains.size()) {
+            const float strainStepMS = 400.0f * speedMultiplier;
+
+            const unsigned long lengthMS = strainStepMS * aimStrains.size();
+
+            // get highest strain values for normalization
+            double highestAimStrain = 0.0;
+            double highestSpeedStrain = 0.0;
+            double highestStrain = 0.0;
+            int highestStrainIndex = -1;
+            for(int i = 0; i < aimStrains.size(); i++) {
+                const double aimStrain = aimStrains[i];
+                const double speedStrain = speedStrains[i];
+                const double strain = aimStrain + speedStrain;
+
+                if(strain > highestStrain) {
+                    highestStrain = strain;
+                    highestStrainIndex = i;
+                }
+                if(aimStrain > highestAimStrain) highestAimStrain = aimStrain;
+                if(speedStrain > highestSpeedStrain) highestSpeedStrain = speedStrain;
+            }
+
+            // draw strain bar graph
+            if(highestAimStrain > 0.0 && highestSpeedStrain > 0.0 && highestStrain > 0.0) {
+                const float dpiScale = Osu::getUIScale();
+
+                const float graphWidth = m_scoreBrowser->getSize().x;
+
+                const float msPerPixel = (float)lengthMS / graphWidth;
+                const float strainWidth = strainStepMS / msPerPixel;
+                const float strainHeightMultiplier =
+                    m_osu_hud_scrubbing_timeline_strains_height_ref->getFloat() * dpiScale;
+
+                McRect graphRect(0, m_bottombar->getPos().y - strainHeightMultiplier, graphWidth,
+                                 strainHeightMultiplier);
+
+                const float alpha = (graphRect.contains(engine->getMouse()->getPos())
+                                         ? 1.0f
+                                         : m_osu_hud_scrubbing_timeline_strains_alpha_ref->getFloat());
+
+                const Color aimStrainColor =
+                    COLORf(alpha, m_osu_hud_scrubbing_timeline_strains_aim_color_r_ref->getInt() / 255.0f,
+                           m_osu_hud_scrubbing_timeline_strains_aim_color_g_ref->getInt() / 255.0f,
+                           m_osu_hud_scrubbing_timeline_strains_aim_color_b_ref->getInt() / 255.0f);
+                const Color speedStrainColor =
+                    COLORf(alpha, m_osu_hud_scrubbing_timeline_strains_speed_color_r_ref->getInt() / 255.0f,
+                           m_osu_hud_scrubbing_timeline_strains_speed_color_g_ref->getInt() / 255.0f,
+                           m_osu_hud_scrubbing_timeline_strains_speed_color_b_ref->getInt() / 255.0f);
+
+                g->setDepthBuffer(true);
+                for(int i = 0; i < aimStrains.size(); i++) {
+                    const double aimStrain = (aimStrains[i]) / highestStrain;
+                    const double speedStrain = (speedStrains[i]) / highestStrain;
+                    // const double strain = (aimStrains[i] + speedStrains[i]) / highestStrain;
+
+                    const double aimStrainHeight = aimStrain * strainHeightMultiplier;
+                    const double speedStrainHeight = speedStrain * strainHeightMultiplier;
+                    // const double strainHeight = strain * strainHeightMultiplier;
+
+                    if(!engine->getKeyboard()->isShiftDown()) {
+                        g->setColor(aimStrainColor);
+                        g->fillRect(i * strainWidth, m_bottombar->getPos().y - aimStrainHeight,
+                                    max(1.0f, std::round(strainWidth + 0.5f)), aimStrainHeight);
+                    }
+
+                    if(!engine->getKeyboard()->isControlDown()) {
+                        g->setColor(speedStrainColor);
+                        g->fillRect(i * strainWidth,
+                                    m_bottombar->getPos().y -
+                                        (engine->getKeyboard()->isShiftDown() ? 0 : aimStrainHeight) -
+                                        speedStrainHeight,
+                                    max(1.0f, std::round(strainWidth + 0.5f)), speedStrainHeight + 1);
+                    }
+                }
+                g->setDepthBuffer(false);
+
+                // highlight highest total strain value (+- section block)
+                if(highestStrainIndex > -1) {
+                    const double aimStrain = (aimStrains[highestStrainIndex]) / highestStrain;
+                    const double speedStrain = (speedStrains[highestStrainIndex]) / highestStrain;
+                    // const double strain = (aimStrains[i] + speedStrains[i]) / highestStrain;
+
+                    const double aimStrainHeight = aimStrain * strainHeightMultiplier;
+                    const double speedStrainHeight = speedStrain * strainHeightMultiplier;
+                    // const double strainHeight = strain * strainHeightMultiplier;
+
+                    Vector2 topLeftCenter = Vector2(highestStrainIndex * strainWidth + strainWidth / 2.0f,
+                                                    m_bottombar->getPos().y - aimStrainHeight - speedStrainHeight);
+
+                    const float margin = 5.0f * dpiScale;
+
+                    g->setColor(0xffffffff);
+                    g->setAlpha(alpha);
+                    g->drawRect(topLeftCenter.x - margin * strainWidth, topLeftCenter.y - margin * strainWidth,
+                                strainWidth * 2 * margin,
+                                aimStrainHeight + speedStrainHeight + 2 * margin * strainWidth);
+                    g->setAlpha(alpha * 0.5f);
+                    g->drawRect(topLeftCenter.x - margin * strainWidth - 2, topLeftCenter.y - margin * strainWidth - 2,
+                                strainWidth * 2 * margin + 4,
+                                aimStrainHeight + speedStrainHeight + 2 * margin * strainWidth + 4);
+                    g->setAlpha(alpha * 0.25f);
+                    g->drawRect(topLeftCenter.x - margin * strainWidth - 4, topLeftCenter.y - margin * strainWidth - 4,
+                                strainWidth * 2 * margin + 8,
+                                aimStrainHeight + speedStrainHeight + 2 * margin * strainWidth + 8);
+                }
+            }
+        }
+    }
+
     // draw song browser
     m_songBrowser->draw(g);
 
@@ -765,6 +901,18 @@ void SongBrowser::mouse_update(bool *propagate_clicks) {
         if(db_diff->m_calculate_full_pp.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
             db_diff->m_pp_info = db_diff->m_calculate_full_pp.get();
             db_diff->m_calculate_full_pp = std::future<pp_info>();
+
+            m_selectedBeatmap->m_aimStrains = std::vector<f64>(
+                db_diff->m_pp_info.aim_strains,
+                &db_diff->m_pp_info.aim_strains[db_diff->m_pp_info.aim_strains_len]
+            );
+            m_selectedBeatmap->m_speedStrains = std::vector<f64>(
+                db_diff->m_pp_info.speed_strains,
+                &db_diff->m_pp_info.speed_strains[db_diff->m_pp_info.speed_strains_len]
+            );
+
+            // Free the strains, which we already copied into vectors
+            free_pp_info(db_diff->m_pp_info);
         }
     }
 

+ 10 - 0
src/App/Osu/SongBrowser/SongBrowser.h

@@ -240,6 +240,16 @@ class SongBrowser : public ScreenBackable {
     ConVar *m_osu_scores_enabled;
     ConVar *m_name_ref;
 
+    ConVar *m_osu_draw_scrubbing_timeline_strain_graph_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_height_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_alpha_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_aim_color_r_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_aim_color_g_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_aim_color_b_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_speed_color_r_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_speed_color_g_ref;
+    ConVar *m_osu_hud_scrubbing_timeline_strains_speed_color_b_ref;
+
     ConVar *m_osu_draw_statistics_perfectpp_ref;
     ConVar *m_osu_draw_statistics_totalstars_ref;
 

+ 11 - 0
src/App/Osu/pp.h

@@ -9,14 +9,25 @@ typedef struct {
     f64 total_stars;
     f64 aim_stars;
     f64 speed_stars;
+
     f64 pp;
+
     u32 num_objects;
     u32 num_circles;
     u32 num_spinners;
+
+    f64 *aim_strains;
+    size_t aim_strains_len;
+    size_t aim_strains_capacity;
+    f64 *speed_strains;
+    size_t speed_strains_len;
+    size_t speed_strains_capacity;
+
     bool ok;
 } pp_info;
 
 pp_info calculate_full_pp(const char* path, u32 mod_flags, f32 ar, f32 cs, f32 od, f64 speed_multiplier);
+void free_pp_info(pp_info info);
 
 typedef struct gradual_pp gradual_pp;
 typedef struct {