Answer the Preview's Per-Frame Time Query From a Cached Sum

The G-code preview's cost is linear in the number of toolpath vertices, and on a
tall multi-filament print the wipe tower dominates that count: it emits a roughly
constant 160-180 moves on every layer whatever the object is, measured at 57-61%
of all moves on a three-filament print.

Four places scanned or allocated across the whole vertex array. None of them
needed to.

get_estimated_time_at re-accumulated the estimated time from vertex 0 on every
call, and its caller is the tool marker tooltip, which ImGui re-renders every
frame while the properties panel is unfolded. It now reads a running sum built
during load from the total the load loop was already keeping, so the value is
the same addition in the same order and the float result is unchanged. At the
351k vertices of a 636-layer test print this drops the call from 238us to
0.006us.

update_view_full_range walked from vertex 0 to find where the layer range starts,
on every slider tick. It now starts at the first vertex of that layer. The index
is derived from the vertices rather than from Layers::Item::range, because
Layers::update folds a vertex whose layer_id arrives out of order into whichever
bucket is open, which makes that range the wrong answer in general; the index
costs four bytes per layer, not per vertex.

update_colors_texture allocated one float per vertex of the whole print on every
slider tick. It now reuses a buffer.

render_legend fetched the layer Zs and the per-layer times from inside loops over
the custom G-code items, and built whole vectors only to test them for emptiness.
The times are hoisted, the Zs are built lazily so a print with no colour change
does not pay for them at all, and the emptiness tests use the existing counters.

No rendering behaviour changes. Verified by loading the same project in this
build and a build of the parent commit under Xvfb and comparing frames across
five interaction states, including the unfolded tooltip whose Time row is the
output of the function that changed: pixel identical.
This commit is contained in:
Hanif Koh
2026-09-09 21:01:14 +08:00
parent 8af92214d0
commit c2c572d228
3 changed files with 78 additions and 7 deletions

View File

@@ -875,6 +875,12 @@ void ViewerImpl::reset()
m_travels_time = { 0.0f, 0.0f }; m_travels_time = { 0.0f, 0.0f };
m_vertices.clear(); m_vertices.clear();
m_vertices_colors.clear(); m_vertices_colors.clear();
// swap rather than clear: these are sized by the print, and a reset means the memory
// should go back, not sit reserved until the next load
for (std::vector<float>& times : m_cumulative_times)
std::vector<float>().swap(times);
std::vector<uint32_t>().swap(m_layer_first_vertex);
std::vector<float>().swap(m_colors_scratch);
m_valid_lines_bitset.clear(); m_valid_lines_bitset.clear();
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
m_cog_marker.reset(); m_cog_marker.reset();
@@ -996,6 +1002,8 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
m_tool_colors = std::move(gcode_data.tools_colors); m_tool_colors = std::move(gcode_data.tools_colors);
m_color_print_colors = std::move(gcode_data.color_print_colors); m_color_print_colors = std::move(gcode_data.color_print_colors);
m_vertices_colors.resize(m_vertices.size()); m_vertices_colors.resize(m_vertices.size());
for (std::vector<float>& times : m_cumulative_times)
times.resize(m_vertices.size());
m_settings.spiral_vase_mode = gcode_data.spiral_vase_mode; m_settings.spiral_vase_mode = gcode_data.spiral_vase_mode;
@@ -1006,6 +1014,9 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
for (size_t j = 0; j < TIME_MODES_COUNT; ++j) { for (size_t j = 0; j < TIME_MODES_COUNT; ++j) {
m_total_time[j] += v.times[j]; m_total_time[j] += v.times[j];
// the running total up to and including this vertex is exactly what
// get_estimated_time_at() has to return for it
m_cumulative_times[j][i] = m_total_time[j];
if (v.type == EMoveType::Travel) if (v.type == EMoveType::Travel)
m_travels_time[j] += v.times[j]; m_travels_time[j] += v.times[j];
} }
@@ -1048,6 +1059,20 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
v.layer_duration = m_layers.get_layer_time(m_settings.time_mode, static_cast<size_t>(v.layer_id)); v.layer_duration = m_layers.get_layer_time(m_settings.time_mode, static_cast<size_t>(v.layer_id));
} }
// Index of the first vertex of each layer, walked back to front so that a layer with no
// vertex of its own inherits the next layer's index and the array stays non-decreasing.
if (!m_layers.empty()) {
const uint32_t vertices_count = static_cast<uint32_t>(m_vertices.size());
m_layer_first_vertex.assign(m_layers.count(), vertices_count);
for (uint32_t i = vertices_count; i > 0; --i) {
const uint32_t layer_id = m_vertices[i - 1].layer_id;
if (layer_id < m_layer_first_vertex.size())
m_layer_first_vertex[layer_id] = i - 1;
}
for (size_t i = m_layer_first_vertex.size() - 1; i > 0; --i)
m_layer_first_vertex[i - 1] = std::min(m_layer_first_vertex[i - 1], m_layer_first_vertex[i]);
}
if (!m_layers.empty()) if (!m_layers.empty())
m_layers.set_view_range(0, static_cast<uint32_t>(m_layers.count()) - 1); m_layers.set_view_range(0, static_cast<uint32_t>(m_layers.count()) - 1);
@@ -1261,7 +1286,10 @@ void ViewerImpl::update_colors_texture()
// Based on current settings and slider position, we might want to render some // Based on current settings and slider position, we might want to render some
// vertices as dark grey (or darkened, see above). Use either that or the normal color (from the cache). // vertices as dark grey (or darkened, see above). Use either that or the normal color (from the cache).
std::vector<float> colors(m_vertices_colors.size()); // Reused across calls: this runs on every slider tick, and the allocation alone is
// 4 bytes per vertex of the whole print each time.
std::vector<float>& colors = m_colors_scratch;
colors.resize(m_vertices_colors.size());
assert(colors.size() == m_vertices.size() && m_vertices_colors.size() == m_vertices.size()); assert(colors.size() == m_vertices.size() && m_vertices_colors.size() == m_vertices.size());
for (size_t i=0; i<m_vertices.size(); ++i) { for (size_t i=0; i<m_vertices.size(); ++i) {
const PathVertex& v = m_vertices[i]; const PathVertex& v = m_vertices[i];
@@ -1516,8 +1544,10 @@ void ViewerImpl::set_view_visible_range(Interval::value_type min, Interval::valu
float ViewerImpl::get_estimated_time_at(size_t id) const float ViewerImpl::get_estimated_time_at(size_t id) const
{ {
return std::accumulate(m_vertices.begin(), m_vertices.begin() + id + 1, 0.0f, const size_t mode = static_cast<size_t>(m_settings.time_mode);
[this](float a, const PathVertex& v) { return a + v.times[static_cast<size_t>(m_settings.time_mode)]; }); if (mode >= TIME_MODES_COUNT || id >= m_cumulative_times[mode].size())
return 0.0f;
return m_cumulative_times[mode][id];
} }
Color ViewerImpl::get_vertex_color(const PathVertex& v) const Color ViewerImpl::get_vertex_color(const PathVertex& v) const
@@ -1722,6 +1752,10 @@ size_t ViewerImpl::get_used_cpu_memory() const
ret += sizeof(m_extrusion_roles_colors); ret += sizeof(m_extrusion_roles_colors);
ret += sizeof(m_options_colors); ret += sizeof(m_options_colors);
ret += STDVEC_MEMSIZE(m_vertices, PathVertex); ret += STDVEC_MEMSIZE(m_vertices, PathVertex);
for (const std::vector<float>& times : m_cumulative_times)
ret += STDVEC_MEMSIZE(times, float);
ret += STDVEC_MEMSIZE(m_layer_first_vertex, uint32_t);
ret += STDVEC_MEMSIZE(m_colors_scratch, float);
ret += m_valid_lines_bitset.size_in_bytes_cpu(); ret += m_valid_lines_bitset.size_in_bytes_cpu();
ret += m_height_range.size_in_bytes_cpu(); ret += m_height_range.size_in_bytes_cpu();
ret += m_width_range.size_in_bytes_cpu(); ret += m_width_range.size_in_bytes_cpu();
@@ -1787,7 +1821,13 @@ void ViewerImpl::update_view_full_range()
const bool travels_visible = m_settings.options_visibility[size_t(EOptionType::Travels)]; const bool travels_visible = m_settings.options_visibility[size_t(EOptionType::Travels)];
const bool wipes_visible = m_settings.options_visibility[size_t(EOptionType::Wipes)]; const bool wipes_visible = m_settings.options_visibility[size_t(EOptionType::Wipes)];
// Every vertex before m_layer_first_vertex[layers_range[0]] has a smaller layer_id, so the
// loop below would skip all of them on its first condition alone. Starting there turns a scan
// from vertex 0 on every slider tick into a scan of the visible part only; what the loop
// settles on is unchanged.
auto first_it = m_vertices.begin(); auto first_it = m_vertices.begin();
if (layers_range[0] < m_layer_first_vertex.size())
first_it += m_layer_first_vertex[layers_range[0]];
while (first_it != m_vertices.end() && while (first_it != m_vertices.end() &&
(first_it->layer_id < layers_range[0] || !is_visible(*first_it, m_settings))) { (first_it->layer_id < layers_range[0] || !is_visible(*first_it, m_settings))) {
++first_it; ++first_it;

View File

@@ -234,6 +234,27 @@ private:
// //
std::array<float, TIME_MODES_COUNT> m_total_time{ 0.0f, 0.0f }; std::array<float, TIME_MODES_COUNT> m_total_time{ 0.0f, 0.0f };
// //
// Running sum of the vertex estimated times, one entry per vertex for each time mode.
// get_estimated_time_at() answers from this instead of re-accumulating the whole vertex
// array, which it was doing once per frame from the tool marker tooltip. The sums are
// built by the same left-to-right addition the accumulate performed, so the value handed
// back is bit-identical, float rounding included.
//
std::array<std::vector<float>, TIME_MODES_COUNT> m_cumulative_times;
//
// For each layer L, the index of the first vertex whose layer_id is >= L (m_vertices.size()
// if there is none). Every vertex before it is guaranteed to belong to an earlier layer, so
// the scan in update_view_full_range() can start there rather than at vertex 0. Derived from
// the vertices themselves rather than from Layers, which buckets an out-of-order vertex into
// the layer that happens to be open, so this stays exact whatever order the vertices arrive in.
//
std::vector<uint32_t> m_layer_first_vertex;
//
// Scratch buffer for update_colors_texture(), kept alive so that a slider drag does not
// allocate and free one float per vertex of the print on every step.
//
std::vector<float> m_colors_scratch;
//
// Detected travel moves times // Detected travel moves times
// //
std::array<float, TIME_MODES_COUNT> m_travels_time{ 0.0f, 0.0f }; std::array<float, TIME_MODES_COUNT> m_travels_time{ 0.0f, 0.0f };

View File

@@ -1622,7 +1622,7 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
glsafe(::glEnable(GL_DEPTH_TEST)); glsafe(::glEnable(GL_DEPTH_TEST));
render_shells(canvas_width, canvas_height); render_shells(canvas_width, canvas_height);
if (m_viewer.get_extrusion_roles().empty()) if (m_viewer.get_extrusion_roles_count() == 0)
return; return;
render_toolpaths(); render_toolpaths();
@@ -3408,6 +3408,12 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
std::vector<std::pair<ColorRGBA, std::pair<double, double>>> ret; std::vector<std::pair<ColorRGBA, std::pair<double, double>>> ret;
ret.reserve(custom_gcode_per_print_z.size()); ret.reserve(custom_gcode_per_print_z.size());
// Loop invariant, but built lazily: this lambda runs once per extruder on every frame
// and most prints reach neither colour change below, so fetching it up front would cost
// more than the per-item fetch it replaces.
std::vector<float> zs;
bool zs_built = false;
for (const auto& item : custom_gcode_per_print_z) { for (const auto& item : custom_gcode_per_print_z) {
if (extruder_id + 1 != static_cast<unsigned char>(item.extruder)) if (extruder_id + 1 != static_cast<unsigned char>(item.extruder))
continue; continue;
@@ -3415,7 +3421,10 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
if (item.type != ColorChange) if (item.type != ColorChange)
continue; continue;
const std::vector<float> zs = m_viewer.get_layers_zs(); if (!zs_built) {
zs = m_viewer.get_layers_zs();
zs_built = true;
}
auto lower_b = std::lower_bound(zs.begin(), zs.end(), auto lower_b = std::lower_bound(zs.begin(), zs.end(),
static_cast<float>(item.print_z - epsilon())); static_cast<float>(item.print_z - epsilon()));
if (lower_b == zs.end()) if (lower_b == zs.end())
@@ -4562,6 +4571,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
// ORCA: Get layer Zs as doubles // ORCA: Get layer Zs as doubles
std::vector<double> layer_zs = get_layers_zs(); std::vector<double> layer_zs = get_layers_zs();
// loop invariant, same reason as the layer Zs above
const std::vector<float> layer_times = m_viewer.get_layers_estimated_times();
for (Slic3r::CustomGCode::Item custom_gcode : custom_gcode_per_print_z) { for (Slic3r::CustomGCode::Item custom_gcode : custom_gcode_per_print_z) {
ImGui::Dummy({window_padding, window_padding}); ImGui::Dummy({window_padding, window_padding});
@@ -4581,7 +4592,6 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
imgui.text(buf); imgui.text(buf);
ImGui::SameLine(max_len * 1.5); ImGui::SameLine(max_len * 1.5);
std::vector<float> layer_times = m_viewer.get_layers_estimated_times();
float custom_gcode_time = 0; float custom_gcode_time = 0;
if (layer > 0) if (layer > 0)
{ {
@@ -4630,7 +4640,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
std::string print_str = _u8L("Model printing time"); std::string print_str = _u8L("Model printing time");
std::string total_str = _u8L("Total time"); std::string total_str = _u8L("Total time");
float max_len = window_padding + 2 * ImGui::GetStyle().ItemSpacing.x; float max_len = window_padding + 2 * ImGui::GetStyle().ItemSpacing.x;
if (m_viewer.get_layers_estimated_times().empty()) if (m_viewer.get_layers_count() == 0)
max_len += ImGui::CalcTextSize(total_str.c_str()).x; max_len += ImGui::CalcTextSize(total_str.c_str()).x;
else { else {
if (m_viewer.get_view_type() == libvgcode::EViewType::FeatureType) if (m_viewer.get_view_type() == libvgcode::EViewType::FeatureType)