Files
OrcaSlicer/src/slic3r/GUI/SceneBenchmark.cpp
T
HanifKoh 5a95ba4bd5 Add Missing Includes to Code Merged Since the Include Cleanup (#16106)
* Add Missing Includes to Code Merged Since the Include Cleanup

* Add Missing Includes to Code Merged Since the Previous Sweep
2026-10-04 14:50:15 +08:00

574 lines
22 KiB
C++

#include "SceneBenchmark.hpp"
#include "3DScene.hpp"
#include "BuildCommit.hpp"
#include "Camera.hpp"
#include "GLCanvas3D.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "GUI_Factories.hpp"
#include "I18N.hpp"
#include "IMSlider.hpp"
#include "MainFrame.hpp"
#include "MsgDialog.hpp"
#include "NotificationManager.hpp"
#include "OpenGLManager.hpp"
#include "Plater.hpp"
#include "Widgets/DialogButtons.hpp"
#include "Widgets/Label.hpp"
#include "Widgets/ProgressBar.hpp"
#include "Widgets/StateColor.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/libslic3r.h"
#include "libslic3r_version.h"
#include <array>
#include <glad/gl.h>
#include <vector>
#include <string>
#include "slic3r/GUI/FrameProfiler.hpp"
#include "slic3r/GUI/GUI_Utils.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/BoundingBox.hpp"
#include <ratio>
#include "libslic3r/Preset.hpp"
#include <ios>
#include <wx/clipbrd.h>
#include <wx/gdicmn.h>
#include <wx/colour.h>
#include <wx/event.h>
#include <wx/dcclient.h>
#include <wx/dataobj.h>
#include <wx/glcanvas.h>
#include <wx/string.h>
#include <wx/textctrl.h>
#include <wx/timer.h>
#include <wx/toplevel.h>
#include <wx/utils.h>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstring>
#include <iomanip>
#include <memory>
#include <numeric>
#include <sstream>
namespace Slic3r {
namespace GUI {
FrameTimeStats frame_time_stats(std::vector<double> frame_ms)
{
FrameTimeStats stats;
if (frame_ms.empty())
return stats;
std::sort(frame_ms.begin(), frame_ms.end());
const double total_ms = std::accumulate(frame_ms.begin(), frame_ms.end(), 0.0);
auto percentile = [&frame_ms](double p) {
const size_t rank = size_t(std::ceil(p * double(frame_ms.size()) / 100.0));
return frame_ms[std::clamp<size_t>(rank, 1, frame_ms.size()) - 1];
};
stats.fps = total_ms > 0.0 ? 1000.0 * double(frame_ms.size()) / total_ms : 0.0;
stats.average_ms = total_ms / double(frame_ms.size());
stats.median_ms = percentile(50.0);
stats.p95_ms = percentile(95.0);
stats.p99_ms = percentile(99.0);
stats.max_ms = frame_ms.back();
return stats;
}
namespace {
constexpr size_t WARMUP_FRAMES = 30;
// A scene runs the camera path twice: timing the frames, then averaging the render timings.
constexpr size_t PASS_FRAMES = 360;
constexpr size_t SCENE_FRAMES = WARMUP_FRAMES + 2 * PASS_FRAMES;
struct SceneResult
{
std::string name;
std::vector<double> frame_ms;
std::vector<FrameProfiler::Section> sections;
};
class SceneBenchmarkDialog : public DPIDialog
{
public:
SceneBenchmarkDialog();
~SceneBenchmarkDialog() override;
bool is_running() const { return m_stage != Stage::Done; }
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
private:
enum class Stage { Loading, Prepare, Slicing, Preview, Layers, Done };
void on_timer(wxTimerEvent& evt);
void on_idle(wxIdleEvent& evt);
void on_first_frame();
void begin_scene(GLCanvas3D* canvas, Stage stage);
void end_scene();
void stop_scene();
void start_slicing();
void set_camera(size_t pass_frame);
void set_top_layer(size_t pass_frame);
void set_status(const wxString& status);
void finish(const wxString& error = wxString());
void show_report(const wxString& error);
std::string report() const;
Stage m_stage{ Stage::Loading };
std::unique_ptr<wxWindowDisabler> m_disabler;
wxTimer m_timer;
int m_polls{ 0 };
Label* m_status{ nullptr };
ProgressBar* m_progress{ nullptr };
GLCanvas3D* m_canvas{ nullptr };
Camera m_saved_camera;
Vec3d m_target{ Vec3d::Zero() };
double m_zoom{ 1.0 };
size_t m_frame{ 0 };
std::chrono::steady_clock::time_point m_last_frame;
// The layer slider's top before the Layers scene moved it, -1 outside that scene.
int m_saved_top_layer{ -1 };
int m_swap_interval{ wxGLCanvas::DefaultSwapInterval };
bool m_restore_swap_interval{ false };
std::vector<SceneResult> m_results;
// Read on the first frame, with the context current.
std::string m_vsync;
int m_msaa_samples{ 0 };
int m_viewport_width{ 0 };
int m_viewport_height{ 0 };
std::string m_camera_type;
};
SceneBenchmarkDialog* s_benchmark = nullptr;
SceneBenchmarkDialog::SceneBenchmarkDialog()
: DPIDialog(static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY, _L("Benchmark 3D Scene"), wxDefaultPosition, wxDefaultSize,
wxCAPTION | wxCLOSE_BOX)
, m_timer(this)
{
s_benchmark = this;
SetBackgroundColour(*wxWHITE);
SetFont(Label::Body_14);
auto* sizer = new wxBoxSizer(wxVERTICAL);
sizer->SetMinSize(wxSize(FromDIP(360), -1));
m_status = new Label(this, _L("Loading the benchmark model") + dots);
m_progress = new ProgressBar(this, wxID_ANY, 100);
m_progress->SetHeight(FromDIP(8));
m_progress->SetMaxSize(wxSize(-1, FromDIP(8)));
m_progress->SetProgressForedColour(StateColor::darkModeColorFor(wxColour("#DFDFDF")));
sizer->Add(m_status, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(15));
sizer->Add(m_progress, 0, wxEXPAND | wxALL, FromDIP(15));
auto* buttons = new DialogButtons(this, {"Cancel"});
buttons->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { Close(); });
sizer->Add(buttons, 0, wxEXPAND);
SetSizerAndFit(sizer);
wxGetApp().UpdateDlgDarkUI(this);
Bind(wxEVT_TIMER, &SceneBenchmarkDialog::on_timer, this);
Bind(wxEVT_IDLE, &SceneBenchmarkDialog::on_idle, this);
Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& evt) {
if (evt.GetKeyCode() == WXK_ESCAPE)
Close();
else
evt.Skip();
});
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) {
m_timer.Stop();
stop_scene();
m_disabler.reset();
Destroy();
});
// A corner of the 3D view, away from the model.
const wxRect view = wxGetApp().plater()->get_current_canvas3D()->get_wxglcanvas()->GetScreenRect();
SetPosition(wxPoint(view.GetLeft() + FromDIP(20), view.GetBottom() - GetSize().GetHeight() - FromDIP(20)));
m_disabler = std::make_unique<wxWindowDisabler>(this);
m_timer.Start(100);
}
SceneBenchmarkDialog::~SceneBenchmarkDialog()
{
// At shutdown the windows it disabled may be gone already.
if (wxGetApp().is_closing())
(void) m_disabler.release();
if (s_benchmark == this)
s_benchmark = nullptr;
}
void SceneBenchmarkDialog::on_dpi_changed(const wxRect&)
{
Fit();
Refresh();
}
void SceneBenchmarkDialog::on_timer(wxTimerEvent&)
{
Plater* plater = wxGetApp().plater();
if (wxGetApp().is_closing() || plater == nullptr) {
m_timer.Stop();
return;
}
if (m_stage == Stage::Loading) {
if (!plater->get_ui_job_worker().is_idle())
return;
m_timer.Stop();
GLCanvas3D* canvas = plater->get_view3D_canvas3D();
const BoundingBoxf3 box = canvas->volumes_bounding_box(true);
if (!box.defined) {
finish(_L("The benchmark model could not be loaded."));
return;
}
m_target = box.center();
const Size size = canvas->get_canvas_size();
m_zoom = double(std::min(size.get_width(), size.get_height())) / (1.1 * box.size().norm());
plater->deselect_all();
begin_scene(canvas, Stage::Prepare);
} else if (m_stage == Stage::Slicing) {
if (plater->is_background_process_slicing())
return;
GLCanvas3D* canvas = plater->get_preview_canvas3D();
if (canvas->get_gcode_viewer().has_data() && canvas->get_wxglcanvas()->IsShownOnScreen()) {
m_timer.Stop();
plater->get_notification_manager()->set_slicing_progress_hidden();
begin_scene(canvas, Stage::Preview);
} else if (++m_polls > 30)
finish(_L("Slicing failed, so only Prepare was measured."));
}
}
void SceneBenchmarkDialog::on_idle(wxIdleEvent& evt)
{
evt.Skip();
if (m_canvas == nullptr || wxGetApp().is_closing())
return;
if (!m_canvas->get_wxglcanvas()->IsShownOnScreen()) {
finish(_L("The benchmark stopped because the 3D view was hidden."));
return;
}
const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
if (m_frame > WARMUP_FRAMES && m_frame <= WARMUP_FRAMES + PASS_FRAMES)
m_results.back().frame_ms.push_back(std::chrono::duration<double, std::milli>(now - m_last_frame).count());
m_last_frame = now;
FrameProfiler& profiler = m_canvas->get_frame_profiler();
if (m_frame == WARMUP_FRAMES + PASS_FRAMES)
profiler.start_averaging();
if (m_stage == Stage::Layers)
// The warm-up ends where the path starts, so the slider moves in every frame.
set_top_layer(m_frame < WARMUP_FRAMES ? PASS_FRAMES - WARMUP_FRAMES + m_frame : (m_frame - WARMUP_FRAMES) % PASS_FRAMES);
else
set_camera(m_frame < WARMUP_FRAMES ? 0 : (m_frame - WARMUP_FRAMES) % PASS_FRAMES);
m_canvas->render();
if (m_frame == 0)
on_first_frame();
if (++m_frame == SCENE_FRAMES) {
m_results.back().sections = profiler.finish_averaging(true);
end_scene();
// The next scene starts right away, with no timer to bring the next idle event.
if (m_canvas != nullptr)
evt.RequestMore();
return;
}
const int scene = m_stage == Stage::Prepare ? 0 : m_stage == Stage::Preview ? 1 : 2;
if (m_frame % 30 == 0)
m_progress->SetValue(int(100 * (scene * SCENE_FRAMES + m_frame) / (3 * SCENE_FRAMES)));
evt.RequestMore();
}
void SceneBenchmarkDialog::on_first_frame()
{
// A benchmark measures the frames the GPU can draw, not the display refresh rate.
wxGLCanvas* glcanvas = m_canvas->get_wxglcanvas();
m_swap_interval = glcanvas->GetSwapInterval();
const bool known = m_swap_interval != wxGLCanvas::DefaultSwapInterval;
if (known && m_swap_interval != 0)
m_restore_swap_interval = glcanvas->SetSwapInterval(0) != wxGLCanvas::SwapInterval::NotSet;
if (m_results.size() > 1)
return;
m_vsync = !known ? "unknown" : m_swap_interval == 0 || m_restore_swap_interval ? "off" : "on";
GLint samples = 0;
glsafe(::glGetIntegerv(GL_SAMPLES, &samples));
m_msaa_samples = samples;
const Camera& camera = wxGetApp().plater()->get_camera();
const std::array<int, 4>& viewport = camera.get_viewport();
m_viewport_width = viewport[2];
m_viewport_height = viewport[3];
m_camera_type = camera.get_type_as_string();
}
void SceneBenchmarkDialog::begin_scene(GLCanvas3D* canvas, Stage stage)
{
m_stage = stage;
m_canvas = canvas;
m_saved_camera = wxGetApp().plater()->get_camera();
m_frame = 0;
m_restore_swap_interval = false;
m_results.push_back({stage == Stage::Prepare ? "Prepare" : stage == Stage::Preview ? "Preview" : "Layers", {}, {}});
m_results.back().frame_ms.reserve(PASS_FRAMES);
set_status(stage == Stage::Prepare ? _L("Turning the camera in Prepare") :
stage == Stage::Preview ? _L("Turning the camera in Preview") :
_L("Moving through the layers in Preview"));
if (stage == Stage::Layers) {
m_saved_top_layer = canvas->get_gcode_viewer().get_layers_slider()->GetHigherValue();
set_camera(0);
}
canvas->set_benchmarking(true);
}
void SceneBenchmarkDialog::end_scene()
{
const Stage stage = m_stage;
GLCanvas3D* canvas = m_canvas;
stop_scene();
if (stage == Stage::Prepare)
start_slicing();
else if (stage == Stage::Preview)
begin_scene(canvas, Stage::Layers);
else
finish();
}
void SceneBenchmarkDialog::stop_scene()
{
if (m_canvas == nullptr)
return;
if (m_frame < SCENE_FRAMES)
m_results.pop_back();
FrameProfiler& profiler = m_canvas->get_frame_profiler();
if (profiler.is_averaging())
profiler.finish_averaging(false);
if (m_restore_swap_interval && m_canvas->make_current_for_postinit())
m_canvas->get_wxglcanvas()->SetSwapInterval(m_swap_interval);
if (m_saved_top_layer >= 0) {
m_canvas->get_gcode_viewer().get_layers_slider()->SetHigherValue(m_saved_top_layer);
m_saved_top_layer = -1;
}
m_canvas->set_benchmarking(false);
m_canvas->set_as_dirty();
wxGetApp().plater()->get_camera() = m_saved_camera;
m_canvas = nullptr;
}
void SceneBenchmarkDialog::start_slicing()
{
m_stage = Stage::Slicing;
set_status(_L("Slicing"));
m_progress->SetValue(33);
Plater* plater = wxGetApp().plater();
plater->reslice();
plater->select_view_3D("Preview", false);
wxGetApp().mainframe->select_tab(TAB_ID_PREVIEW);
m_polls = 0;
m_timer.Start(100);
}
void SceneBenchmarkDialog::set_camera(size_t pass_frame)
{
// Two turns around the model while the view goes three times from below the plate to nearly
// straight above it, zooming in and out twice.
const double t = double(pass_frame) / double(PASS_FRAMES);
const double azimuth = (-0.75 + 4.0 * t) * PI;
const double elevation = (30.0 + 55.0 * std::sin(6.0 * PI * t)) * PI / 180.0;
const Vec3d dir(std::cos(elevation) * std::cos(azimuth), std::cos(elevation) * std::sin(azimuth), std::sin(elevation));
Camera& camera = wxGetApp().plater()->get_camera();
camera.look_at(m_target + Camera::DefaultDistance * dir, m_target, Vec3d::UnitZ());
camera.set_zoom(m_zoom * (1.0 + 0.4 * std::sin(4.0 * PI * t)));
}
void SceneBenchmarkDialog::set_top_layer(size_t pass_frame)
{
// Down to the first layer and back up, as dragging the top of the layer slider does.
IMSlider* slider = m_canvas->get_gcode_viewer().get_layers_slider();
const double t = double(pass_frame) / double(PASS_FRAMES);
const int span = slider->GetMaxValue() - slider->GetMinValue();
slider->SetHigherValue(slider->GetMinValue() + int(std::lround(span * std::abs(1.0 - 2.0 * t))));
}
void SceneBenchmarkDialog::set_status(const wxString& status)
{
m_status->SetLabel(status + dots);
Fit();
}
void SceneBenchmarkDialog::finish(const wxString& error)
{
m_timer.Stop();
stop_scene();
m_disabler.reset();
m_stage = Stage::Done;
show_report(error);
}
void SceneBenchmarkDialog::show_report(const wxString& error)
{
DestroyChildren();
m_status = nullptr;
m_progress = nullptr;
auto* sizer = new wxBoxSizer(wxVERTICAL);
if (!error.IsEmpty()) {
auto* message = new Label(this, error);
message->Wrap(FromDIP(500));
sizer->Add(message, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(15));
}
std::string text;
if (!m_results.empty()) {
text = report();
auto* content = new wxTextCtrl(this, wxID_ANY, from_u8(text), wxDefaultPosition, wxDefaultSize,
wxTE_MULTILINE | wxTE_READONLY | wxTE_DONTWRAP | wxBORDER_SIMPLE);
content->SetFont(wxGetApp().code_font());
wxClientDC dc(content);
dc.SetFont(content->GetFont());
const wxSize extent = dc.GetMultiLineTextExtent(content->GetValue()) + FromDIP(wxSize(40, 20));
content->SetMinSize(wxSize(extent.GetWidth(), std::min(extent.GetHeight(), FromDIP(560))));
sizer->Add(content, 1, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, FromDIP(15));
}
auto* buttons = new DialogButtons(this, text.empty() ? std::vector<wxString>{"OK"} : std::vector<wxString>{"Copy", "OK"});
if (Button* copy = buttons->GetButtonFromID(wxID_COPY)) {
copy->SetToolTip(_L("Copy the results to the clipboard"));
copy->Bind(wxEVT_BUTTON, [text](wxCommandEvent&) {
wxClipboardLocker lock;
if (!lock)
return;
wxTheClipboard->SetData(new wxTextDataObject(from_u8(text)));
});
}
buttons->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { Close(); });
sizer->Add(buttons, 0, wxEXPAND);
SetSizerAndFit(sizer);
CenterOnParent();
wxGetApp().UpdateDlgDarkUI(this);
Raise();
}
std::string SceneBenchmarkDialog::report() const
{
const AppConfig& config = *wxGetApp().app_config;
const OpenGLManager::GLInfo& gl = OpenGLManager::get_gl_info();
auto on_off = [](bool on) { return on ? "on" : "off"; };
std::ostringstream out;
out << SLIC3R_APP_NAME << " 3D scene benchmark\n"
<< "Version: " << SoftFever_VERSION << " (" << build_commit_label << ")\n"
<< "GPU: " << gl.get_renderer() << '\n'
<< "OpenGL: " << gl.get_version() << '\n'
<< "Viewport: " << m_viewport_width << " x " << m_viewport_height << " px, " << m_camera_type << " camera\n"
<< "Settings: MSAA " << (m_msaa_samples > 1 ? std::to_string(m_msaa_samples) + "x" : "off")
<< ", FXAA " << on_off(config.get_bool(SETTING_OPENGL_FXAA_ENABLED))
<< ", scene cache " << on_off(config.get_bool(SETTING_OPENGL_SCENE_CACHE)) << ", VSync " << m_vsync << '\n'
<< "Realistic: ";
if (config.get_bool(SETTING_OPENGL_REALISTIC_MODE))
out << "on, Phong " << on_off(config.get_bool(SETTING_OPENGL_REALISTIC_PHONG)) << ", SSAO "
<< on_off(config.get_bool(SETTING_OPENGL_PHONG_SSAO)) << ", shadows " << config.get(SETTING_OPENGL_REALISTIC_SHADOWS)
<< ", in Preview " << on_off(config.get_bool(SETTING_OPENGL_REALISTIC_PREVIEW)) << '\n';
else
out << "off\n";
// The Preview scenes depend on the toolpaths, which depend on the presets it was sliced with.
const PresetBundle& presets = *wxGetApp().preset_bundle;
auto preset_name = [](const PresetCollection& collection) {
return collection.get_selected_preset_name() + (collection.get_edited_preset().is_dirty ? " (modified)" : "");
};
out << "Presets: " << preset_name(presets.printers) << ", " << preset_name(presets.prints) << '\n'
<< "Scene: OrcaSliced Combo, " << PASS_FRAMES << " frames per pass\n";
const GCodeViewer& gcode_viewer = wxGetApp().plater()->get_preview_canvas3D()->get_gcode_viewer();
if (gcode_viewer.has_data())
out << "Toolpaths: " << gcode_viewer.get_vertices_count() << " vertices, " << gcode_viewer.get_layers_count() << " layers\n";
out << '\n';
std::vector<FrameTimeStats> stats;
for (const SceneResult& result : m_results)
stats.push_back(frame_time_stats(result.frame_ms));
auto row = [&out, &stats](const char* label, double FrameTimeStats::*value) {
out << std::left << std::setw(22) << label << std::right;
for (const FrameTimeStats& s : stats)
out << std::setw(10) << s.*value;
out << '\n';
};
out << std::fixed << std::setprecision(1) << std::setw(22) << "";
for (const SceneResult& result : m_results)
out << std::right << std::setw(10) << result.name;
out << '\n';
row("Average FPS", &FrameTimeStats::fps);
out << std::setprecision(2) << "Frame time (ms)\n";
row(" average", &FrameTimeStats::average_ms);
row(" median", &FrameTimeStats::median_ms);
row(" 95th percentile", &FrameTimeStats::p95_ms);
row(" 99th percentile", &FrameTimeStats::p99_ms);
row(" maximum", &FrameTimeStats::max_ms);
for (const SceneResult& result : m_results) {
out << "\nRender timings, " << result.name << " (ms per frame)\n";
if (result.sections.empty()) {
out << " not supported by the graphics driver\n";
continue;
}
out << std::setw(22) << "" << std::setw(10) << "CPU" << std::setw(10) << "GPU" << '\n';
double cpu_ms = 0.0;
double gpu_ms = 0.0;
for (const FrameProfiler::Section& section : result.sections) {
out << " " << std::left << std::setw(20) << section.name << std::right << std::setw(10) << section.cpu_ms << std::setw(10)
<< section.gpu_ms << '\n';
cpu_ms += section.cpu_ms;
gpu_ms += section.gpu_ms;
}
out << " " << std::left << std::setw(20) << "total" << std::right << std::setw(10) << cpu_ms << std::setw(10) << gpu_ms << '\n';
}
return out.str();
}
} // namespace
void run_scene_benchmark()
{
Plater* plater = wxGetApp().plater();
if (plater == nullptr || !wxGetApp().is_editor())
return;
if (s_benchmark != nullptr) {
if (s_benchmark->is_running()) {
s_benchmark->Raise();
return;
}
s_benchmark->Close();
}
MessageDialog confirm(wxGetApp().mainframe,
_L("The benchmark replaces the current project with the OrcaSliced Combo and slices it. It turns the camera "
"around the model in Prepare and in Preview, then moves the layer slider through the sliced layers, timing "
"every frame, and shows the results.") + "\n\n" +
_L("It uses the current graphics settings and usually takes less than a minute. The main window stays "
"locked until it finishes."),
_L("Benchmark 3D Scene"), wxICON_INFORMATION | wxOK | wxCANCEL);
confirm.SetButtonLabel(wxID_OK, _L("Start"), true);
if (confirm.ShowModal() != wxID_OK || plater->new_project() == wxID_CANCEL)
return;
const std::vector<MenuFactory::HandyModel>& models = MenuFactory::handy_models();
const auto combo = std::find_if(models.begin(), models.end(),
[](const MenuFactory::HandyModel& model) { return std::strcmp(model.key, "orcasliced_combo") == 0; });
MenuFactory::load_handy_model(size_t(combo - models.begin()));
(new SceneBenchmarkDialog())->Show();
}
} // namespace GUI
} // namespace Slic3r