Keep the Drawn Preview While Nothing in It Moves

Every preview frame redrew the whole scene, whether the camera had moved or a
tooltip had faded. A cursor drifting over the canvas, a hovered legend row, a
notification or the frame rate overlay each cost a full draw of every toolpath,
so a large print stayed slow with nothing happening.

Behind a preference, the window's colour and depth are now copied into a
framebuffer of the same size, sample count and depth format right after the
scene pass, and while nothing that pass draws has changed they are copied
back instead of redrawn; only the panels, the marker and the overlays are
drawn on top. What counts as a change is the camera's matrices, the canvas
size, the hovered and current plate, the world axes, a colour mode change,
and two counters from the viewer: one that libvgcode bumps whenever it
applies an update or binds another set, one the G-code viewer bumps on load,
reset, shell changes and its own toggles. A pending viewer update also forces
a redraw. A driver that refuses the blit disables the cache for the session
and the preview falls back to drawing every frame.

The frames that follow a change cost what they did before; what changes is
that a frame with no change costs a blit. On the tall fixture under llvmpipe,
cursor motion over the canvas produced a frame every 600 ms and now every
110 ms, limited by the input rate. At rest, after a slider click and after a
wheel burst the preview is pixel identical with the preference on and off,
and with it off the preview is pixel identical to before.
This commit is contained in:
Hanif Koh
2026-09-11 19:53:32 +08:00
parent 038e86f02a
commit c2eea1cada
10 changed files with 251 additions and 10 deletions

View File

@@ -212,6 +212,10 @@ void AppConfig::set_defaults()
set("preview_reduced_detail_mode", "no_infill");
}
// ORCA: keep the drawn preview scene while nothing in it changes
if (get("preview_cache_static_scene").empty())
set_bool("preview_cache_static_scene", false);
{
const std::string mode = get("preview_rest_detail_mode");
if (mode != "full" && mode != "no_infill" && mode != "shell")

View File

@@ -131,6 +131,13 @@ public:
//
bool is_rest_view_from_above() const;
void set_rest_view_from_above(bool value);
//
// ORCA: a counter that changes whenever what render() draws changes, and whether an update is
// still pending that the next render() will apply. Together they tell a caller whether a frame
// it has kept from an earlier render() can be shown again.
//
uint64_t get_state_version() const;
bool has_pending_updates() const;
float get_dim_previous_layers_brightness() const;
void set_dim_previous_layers_brightness(float value);
//

View File

@@ -137,6 +137,16 @@ void Viewer::set_rest_view_from_above(bool value)
m_impl->set_rest_view_from_above(value);
}
uint64_t Viewer::get_state_version() const
{
return m_impl->get_state_version();
}
bool Viewer::has_pending_updates() const
{
return m_impl->has_pending_updates();
}
void Viewer::set_dim_previous_layers(bool value)
{
m_impl->set_dim_previous_layers(value);

View File

@@ -899,6 +899,7 @@ void ViewerImpl::reset()
m_enabled_segments_reduced_count = 0;
m_enabled_options_reduced_count = 0;
m_enabled_segments_rest_count = 0;
++m_state_version;
m_shell_bitset = BitSet<>();
m_near_shell_bitset = BitSet<>();
m_top_visible_bitset = BitSet<>();
@@ -1844,6 +1845,7 @@ void ViewerImpl::update_enabled_entities()
#endif // ENABLE_OPENGL_ES
m_settings.update_enabled_entities = false;
++m_state_version;
}
static float encode_color(const Color& color) {
@@ -1953,6 +1955,7 @@ void ViewerImpl::update_colors()
update_colors_texture();
m_settings.update_colors = false;
++m_state_version;
}
void ViewerImpl::render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix)
@@ -2562,6 +2565,7 @@ void ViewerImpl::update_view_full_range()
}
m_settings.update_view_full_range = false;
++m_state_version;
}
void ViewerImpl::update_color_ranges()

View File

@@ -95,7 +95,12 @@ public:
// Draw the preview from the reduced set of entities. Meant to be held only while the user is
// dragging; the sets are already built, so this is just a buffer binding and never rebuilds.
//
void set_reduced_detail(bool value) { m_settings.reduced_detail = value; }
void set_reduced_detail(bool value) {
if (m_settings.reduced_detail != value) {
m_settings.reduced_detail = value;
++m_state_version;
}
}
bool is_reduced_detail() const { return m_settings.reduced_detail; }
EReducedDetailMode get_reduced_detail_mode() const { return m_settings.reduced_detail_mode; }
void set_reduced_detail_mode(EReducedDetailMode mode);
@@ -107,6 +112,10 @@ public:
void set_rest_layer_stride(uint32_t value);
bool is_rest_view_from_above() const { return m_settings.rest_view_from_above; }
void set_rest_view_from_above(bool value);
uint64_t get_state_version() const { return m_state_version; }
bool has_pending_updates() const {
return m_settings.update_view_full_range || m_settings.update_enabled_entities || m_settings.update_colors;
}
float get_dim_previous_layers_brightness() const { return m_settings.dim_previous_layers_brightness; }
void set_dim_previous_layers_brightness(float value);
@@ -339,6 +348,11 @@ private:
// fills the step of a sloped surface between one layer's outer wall and the next, too narrow
// for the grid to see
BitSet<> m_near_shell_bitset;
//
// ORCA: bumped whenever what render() draws changes: on every applied update, on a change of
// the bound set, on load and reset
//
uint64_t m_state_version{ 0 };
#endif // ENABLE_OPENGL_ES
//
// Variables used for toolpaths coloring

View File

@@ -1178,6 +1178,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
m_reduced_detail_layer_stride = static_cast<unsigned int>(std::max(1, std::stoi(get_app_config()->get("preview_reduced_detail_layer_stride"))));
m_rest_detail_mode = reduced_detail_mode_from_string(get_app_config()->get("preview_rest_detail_mode"));
apply_reduced_detail_settings();
++m_scene_version;
// the median z step between layers, robust to the first layer and to variable layer height
{
std::vector<float> steps;
@@ -1594,11 +1595,21 @@ void GCodeViewer::load_as_preview(libvgcode::GCodeInputData&& data)
void GCodeViewer::update_shells_color_by_extruder(const DynamicPrintConfig *config)
{
++m_scene_version;
if (config != nullptr)
m_shells.volumes.update_colors_by_extruder(config, false);
}
void GCodeViewer::set_shell_transparency(float alpha) { m_shells.volumes.set_transparency(alpha); }
void GCodeViewer::set_shell_transparency(float alpha)
{
m_shells.volumes.set_transparency(alpha);
++m_scene_version;
}
std::array<uint64_t, 2> GCodeViewer::scene_version() const
{
return { (m_scene_version << 2) | (m_shells.visible ? 1u : 0u) | (m_no_render_path ? 2u : 0u), m_viewer.get_state_version() };
}
//BBS: always load shell at preview
void GCodeViewer::reset_shell()
@@ -1610,6 +1621,7 @@ void GCodeViewer::reset_shell()
void GCodeViewer::reset()
{
++m_scene_version;
//BBS: should also reset the result id
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": current result id %1% ")%m_last_result_id;
m_last_result_id = -1;
@@ -1638,17 +1650,19 @@ void GCodeViewer::reset()
}
//BBS: GUI refactor: add canvas width and height
void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin, bool draw_scene)
{
glsafe(::glEnable(GL_DEPTH_TEST));
render_shells(canvas_width, canvas_height);
if (draw_scene)
render_shells(canvas_width, canvas_height);
if (m_viewer.get_extrusion_roles_count() == 0)
return;
update_rest_layer_stride();
render_toolpaths();
if (draw_scene)
render_toolpaths();
float legend_height = 0.0f;
render_legend(legend_height, canvas_width, canvas_height, right_margin);
@@ -2336,6 +2350,7 @@ void GCodeViewer::export_toolpaths_to_obj(const char* filename) const
void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_previewing)
{
++m_scene_version;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": initialized=%1%, force_previewing=%2%")%initialized %force_previewing;
if ((print.id().id == m_shells.print_id)&&(print.get_modified_count() == m_shells.print_modify_count)) {
//BBS: update force previewing logic

View File

@@ -251,6 +251,8 @@ private:
bool m_contained_in_bed{ true };
mutable bool m_no_render_path { false };
// ORCA: bumped on every change to what the scene pass draws that libvgcode does not track
uint64_t m_scene_version{ 0 };
bool m_is_dark = false;
libvgcode::Viewer m_viewer;
@@ -283,7 +285,12 @@ public:
//BBS: add all plates filament statistics
void render_all_plates_stats(const std::vector<const GCodeProcessorResult*>& gcode_result_list, bool show = true) const;
//BBS: GUI refactor: add canvas width and height
void render(int canvas_width, int canvas_height, int right_margin);
// draw_scene = false records the legend, the sliders and the marker only, for a frame whose
// toolpaths are shown again from a kept image
void render(int canvas_width, int canvas_height, int right_margin, bool draw_scene = true);
// ORCA: what the scene pass draws, as two counters that change whenever it would look different
std::array<uint64_t, 2> scene_version() const;
bool scene_update_pending() const { return m_viewer.has_pending_updates(); }
//BBS
// void _render_calibration_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);
// void _render_calibration_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);

View File

@@ -1224,6 +1224,7 @@ GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed)
GLCanvas3D::~GLCanvas3D()
{
_scene_cache_release();
if (_set_current()) {
if (m_fxaa_texture_id != 0) {
glsafe(::glDeleteTextures(1, &m_fxaa_texture_id));
@@ -1328,6 +1329,7 @@ bool GLCanvas3D::init()
void GLCanvas3D::on_change_color_mode(bool is_dark, bool reinit) {
m_is_dark = is_dark;
++m_scene_version;
// Bed color
m_bed.on_change_color_mode(is_dark);
// GcodeViewer color
@@ -2050,6 +2052,46 @@ void GLCanvas3D::render(bool only_init)
}
// draw scene
int hover_id = (m_hover_plate_idxs.size() > 0)?m_hover_plate_idxs.front():-1;
// ORCA: while the preference is on and nothing the scene pass draws has changed since the
// last full frame, that frame is shown again and only the overlays are drawn on top of it
const bool preview_scene = m_canvas_type == ECanvasType::CanvasPreview && m_render_preview && m_gcode_viewer.has_data();
bool scene_from_cache = false;
bool scene_to_cache = false;
if (preview_scene && _scene_cache_enabled() && !m_scene_cache.broken) {
const int current_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate_index();
const std::array<uint64_t, 2> viewer_version = m_gcode_viewer.scene_version();
const bool unchanged = m_scene_cache.valid &&
m_scene_cache.width == cnv_size.get_width() && m_scene_cache.height == cnv_size.get_height() &&
m_scene_cache.view == camera.get_view_matrix().matrix() && m_scene_cache.projection == camera.get_projection_matrix().matrix() &&
m_scene_cache.hover_plate == hover_id && m_scene_cache.current_plate == current_plate &&
m_scene_cache.world_axes == m_show_world_axes && m_scene_cache.canvas_version == m_scene_version &&
m_scene_cache.viewer_version == viewer_version && !m_gcode_viewer.scene_update_pending();
if (unchanged) {
_scene_cache_blit(false);
scene_from_cache = !m_scene_cache.broken;
}
if (!scene_from_cache && _scene_cache_prepare(cnv_size.get_width(), cnv_size.get_height())) {
m_scene_cache.valid = false;
m_scene_cache.view = camera.get_view_matrix().matrix();
m_scene_cache.projection = camera.get_projection_matrix().matrix();
m_scene_cache.hover_plate = hover_id;
m_scene_cache.current_plate = current_plate;
m_scene_cache.world_axes = m_show_world_axes;
m_scene_cache.canvas_version = m_scene_version;
m_scene_cache.viewer_version = viewer_version;
scene_to_cache = true;
}
}
else
m_scene_cache.valid = false;
if (scene_from_cache) {
// the preview's panels and its marker are drawn on top of the frame shown again
_render_gcode(cnv_size.get_width(), cnv_size.get_height(), false);
}
else {
glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
// Invalidate the shadow map each frame; only the View3D path below rebuilds it. This keeps
// the Preview / Assemble canvases from sampling a stale map with an outdated light matrix.
@@ -2070,7 +2112,6 @@ void GLCanvas3D::render(bool only_init)
show_grid = false;
/* view3D render*/
int hover_id = (m_hover_plate_idxs.size() > 0)?m_hover_plate_idxs.front():-1;
if (m_canvas_type == ECanvasType::CanvasView3D) {
if (!no_partplate)
_render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), m_show_world_axes);
@@ -2147,6 +2188,13 @@ void GLCanvas3D::render(bool only_init)
if (_is_fxaa_enabled())
_render_fxaa_pass(static_cast<unsigned int>(cnv_size.get_width()), static_cast<unsigned int>(cnv_size.get_height()));
// ORCA: keep the finished scene, with its depth, for the frames that follow
if (scene_to_cache) {
_scene_cache_blit(true);
m_scene_cache.valid = !m_scene_cache.broken;
}
}
// draw overlays
_render_overlays();
@@ -8522,7 +8570,99 @@ void GLCanvas3D::_render_wireframe_overlay()
}
//BBS: GUI refactor: add canvas size as parameters
void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height)
bool GLCanvas3D::_scene_cache_enabled() const
{
return wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool("preview_cache_static_scene");
}
// Makes the cache's framebuffer match the window: same size, same sample count, a depth buffer of
// the same format, so that colour and depth can be blitted both ways. Returns false when it cannot.
bool GLCanvas3D::_scene_cache_prepare(int width, int height)
{
SceneCache& cache = m_scene_cache;
glsafe(::glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &cache.default_fbo));
int samples = 0;
glsafe(::glGetIntegerv(GL_SAMPLES, &samples));
int depth_bits = 0;
int stencil_bits = 0;
const GLenum depth_query = (cache.default_fbo == 0) ? GL_DEPTH : GL_DEPTH_ATTACHMENT;
const GLenum stencil_query = (cache.default_fbo == 0) ? GL_STENCIL : GL_DEPTH_ATTACHMENT;
glsafe(::glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, depth_query, GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depth_bits));
glsafe(::glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, stencil_query, GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencil_bits));
const bool stencil = stencil_bits > 0;
if (cache.fbo != 0 && cache.width == width && cache.height == height && cache.samples == samples && cache.stencil == stencil)
return true;
_scene_cache_release();
const GLenum depth_format = stencil ? GL_DEPTH24_STENCIL8 : (depth_bits <= 16) ? GL_DEPTH_COMPONENT16 : (depth_bits >= 32) ? GL_DEPTH_COMPONENT32 : GL_DEPTH_COMPONENT24;
glsafe(::glGenRenderbuffers(1, &cache.color));
glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, cache.color));
if (samples > 0)
glsafe(::glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, width, height));
else
glsafe(::glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height));
glsafe(::glGenRenderbuffers(1, &cache.depth));
glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, cache.depth));
if (samples > 0)
glsafe(::glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, depth_format, width, height));
else
glsafe(::glRenderbufferStorage(GL_RENDERBUFFER, depth_format, width, height));
glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, 0));
glsafe(::glGenFramebuffers(1, &cache.fbo));
glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, cache.fbo));
glsafe(::glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, cache.color));
glsafe(::glFramebufferRenderbuffer(GL_FRAMEBUFFER, stencil ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, cache.depth));
const GLenum status = ::glCheckFramebufferStatus(GL_FRAMEBUFFER);
glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(cache.default_fbo)));
if (status != GL_FRAMEBUFFER_COMPLETE) {
BOOST_LOG_TRIVIAL(warning) << "Preview scene cache disabled: framebuffer incomplete, status " << status;
_scene_cache_release();
cache.broken = true;
return false;
}
cache.width = width;
cache.height = height;
cache.samples = samples;
cache.stencil = stencil;
return true;
}
// Copies colour and depth from the window into the cache (store) or back (restore). A driver that
// refuses the blit marks the cache broken, and the preview falls back to drawing every frame.
void GLCanvas3D::_scene_cache_blit(bool store)
{
SceneCache& cache = m_scene_cache;
if (cache.fbo == 0)
return;
const GLuint window = static_cast<GLuint>(cache.default_fbo);
glsafe(::glBindFramebuffer(GL_READ_FRAMEBUFFER, store ? window : cache.fbo));
glsafe(::glBindFramebuffer(GL_DRAW_FRAMEBUFFER, store ? cache.fbo : window));
while (::glGetError() != GL_NO_ERROR) {}
::glBlitFramebuffer(0, 0, cache.width, cache.height, 0, 0, cache.width, cache.height, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, GL_NEAREST);
const GLenum error = ::glGetError();
glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, window));
if (error != GL_NO_ERROR) {
BOOST_LOG_TRIVIAL(warning) << "Preview scene cache disabled: framebuffer blit failed with GL error " << error;
cache.broken = true;
_scene_cache_release();
}
}
void GLCanvas3D::_scene_cache_release()
{
SceneCache& cache = m_scene_cache;
if (cache.fbo != 0)
glsafe(::glDeleteFramebuffers(1, &cache.fbo));
if (cache.color != 0)
glsafe(::glDeleteRenderbuffers(1, &cache.color));
if (cache.depth != 0)
glsafe(::glDeleteRenderbuffers(1, &cache.depth));
cache.fbo = cache.color = cache.depth = 0;
cache.width = cache.height = 0;
cache.valid = false;
}
void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height, bool draw_scene)
{
IMSlider *layers_slider = m_gcode_viewer.get_layers_slider();
IMSlider *moves_slider = m_gcode_viewer.get_moves_slider();
@@ -8541,7 +8681,7 @@ void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height)
schedule_extra_frame(static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(m_preview_interaction_until - now).count()) + 1);
}
m_gcode_viewer.render(canvas_width, canvas_height, SLIDER_RIGHT_MARGIN * GCODE_VIEWER_SLIDER_SCALE);
m_gcode_viewer.render(canvas_width, canvas_height, SLIDER_RIGHT_MARGIN * GCODE_VIEWER_SLIDER_SCALE, draw_scene);
if (layers_slider->is_need_post_tick_event()) {
auto evt = new wxCommandEvent(EVT_CUSTOMEVT_TICKSCHANGED, m_canvas->GetId());

View File

@@ -736,6 +736,33 @@ public:
std::array<unsigned int, 2> m_fxaa_texture_size{ 0, 0 };
unsigned int m_ssao_color_texture_id{ 0 };
unsigned int m_ssao_depth_texture_id{ 0 };
// ORCA: the last fully drawn preview scene, shown again while nothing it draws has changed
struct SceneCache
{
unsigned int fbo{ 0 };
unsigned int color{ 0 };
unsigned int depth{ 0 };
int default_fbo{ 0 };
int width{ 0 };
int height{ 0 };
int samples{ 0 };
bool stencil{ false };
// the kept image matches the key below
bool valid{ false };
// a blit failed on this driver: never try again this session
bool broken{ false };
Matrix4d view{ Matrix4d::Zero() };
Matrix4d projection{ Matrix4d::Zero() };
int hover_plate{ -1 };
int current_plate{ -1 };
bool world_axes{ false };
uint64_t canvas_version{ 0 };
std::array<uint64_t, 2> viewer_version{ 0, 0 };
};
SceneCache m_scene_cache;
// bumped on changes to what the scene pass draws that neither the camera nor the viewer track
uint64_t m_scene_version{ 0 };
std::array<unsigned int, 2> m_ssao_texture_size{ { 0, 0 } };
GLModel m_plate_shadow_mask;
std::string m_plate_shadow_mask_key;
@@ -1280,7 +1307,12 @@ private:
void _render_objects(GLVolumeCollection::ERenderType type, bool with_outline = true);
void _render_wireframe_overlay();
//BBS: GUI refactor: add canvas size as parameters
void _render_gcode(int canvas_width, int canvas_height);
void _render_gcode(int canvas_width, int canvas_height, bool draw_scene = true);
// ORCA: scene cache, see SceneCache
bool _scene_cache_enabled() const;
bool _scene_cache_prepare(int width, int height);
void _scene_cache_blit(bool store);
void _scene_cache_release();
//BBS: render a plane for assemble
void _render_plane() const;
void _render_selection();

View File

@@ -2040,6 +2040,14 @@ void PreferencesDialog::create_items()
);
g_sizer->Add(item_rest_detail_mode);
auto item_cache_static_scene = create_item_checkbox(
_L("Keep the drawn preview while nothing moves"),
_L("Keep the last drawn preview and show it again for frames in which neither the camera nor the toolpaths changed, "
"so that hovering, tooltips and notifications no longer redraw a large print. The scene is redrawn as soon as anything in it changes."),
"preview_cache_static_scene"
);
g_sizer->Add(item_cache_static_scene);
auto item_dim_previous_layers = create_item_checkbox(
_L("Dim lower layers"),
_L("When scrubbing the layer slider in the sliced preview, render the layers below the current one darkened so that only the layer being viewed is shown at full brightness."),