diff --git a/include/math.hpp b/include/math.hpp index b7b75c4d..6545e1a1 100644 --- a/include/math.hpp +++ b/include/math.hpp @@ -435,14 +435,16 @@ struct Rect { inline Vec Vec::clamp(Rect bound) const { return Vec( - math::clamp(x, bound.pos.x, bound.pos.x + bound.size.x), - math::clamp(y, bound.pos.y, bound.pos.y + bound.size.y)); + math::clamp(x, bound.pos.x, bound.pos.x + bound.size.x), + math::clamp(y, bound.pos.y, bound.pos.y + bound.size.y) + ); } inline Vec Vec::clampSafe(Rect bound) const { return Vec( - math::clampSafe(x, bound.pos.x, bound.pos.x + bound.size.x), - math::clampSafe(y, bound.pos.y, bound.pos.y + bound.size.y)); + math::clampSafe(x, bound.pos.x, bound.pos.x + bound.size.x), + math::clampSafe(y, bound.pos.y, bound.pos.y + bound.size.y) + ); } diff --git a/include/widget/FramebufferWidget.hpp b/include/widget/FramebufferWidget.hpp index f55912b1..f12b59c0 100644 --- a/include/widget/FramebufferWidget.hpp +++ b/include/widget/FramebufferWidget.hpp @@ -19,6 +19,10 @@ struct FramebufferWidget : Widget { float oversample = 1.0; /** Redraw when the world offset of the FramebufferWidget changes its fractional value. */ bool dirtyOnSubpixelChange = true; + /** If finite, the maximum size of the framebuffer is the viewport expanded by this margin. + The framebuffer is re-rendered when the viewport moves outside the margin. + */ + math::Vec viewportMargin = math::Vec(INFINITY, INFINITY); FramebufferWidget(); ~FramebufferWidget(); @@ -35,7 +39,7 @@ struct FramebufferWidget : Widget { /** Re-renders the framebuffer, re-creating it if necessary. Handles oversampling (if >1) by rendering to a temporary (larger) framebuffer and then downscaling it to the main persistent framebuffer. */ - void render(math::Vec scale, math::Vec offsetF); + void render(math::Vec scale = math::Vec(1, 1), math::Vec offsetF = math::Vec(0, 0), math::Rect clipBox = math::Rect::inf()); /** Initializes the current GL context and draws children to it. */ virtual void drawFramebuffer(); diff --git a/src/Window.cpp b/src/Window.cpp index 16921232..639d7d98 100644 --- a/src/Window.cpp +++ b/src/Window.cpp @@ -566,7 +566,7 @@ void Window::screenshotModules(const std::string& screenshotsDir, float zoom) { fbw->step(); // Draw to framebuffer - fbw->render(math::Vec(zoom, zoom), math::Vec(0, 0)); + fbw->render(math::Vec(zoom, zoom)); // Read pixels nvgluBindFramebuffer(fbw->getFramebuffer()); diff --git a/src/widget/FramebufferWidget.cpp b/src/widget/FramebufferWidget.cpp index 413bd93c..75dbb72f 100644 --- a/src/widget/FramebufferWidget.cpp +++ b/src/widget/FramebufferWidget.cpp @@ -23,6 +23,9 @@ struct FramebufferWidget::Internal { math::Vec fbScale; /** Framebuffer's subpixel offset relative to fbBox in world coordinates */ math::Vec fbOffsetF; + /** Local box where framebuffer content is valid. + */ + math::Rect fbClipBox = math::Rect::inf(); }; @@ -100,22 +103,26 @@ void FramebufferWidget::draw(const DrawArgs& args) { math::Vec offsetI = offset.floor(); math::Vec offsetF = offset.minus(offsetI); - // If drawing to a new subpixel location, rerender in the next frame. + // Re-render if drawing to a new subpixel location. // Anything less than 0.1 pixels isn't noticeable. math::Vec offsetFDelta = offsetF.minus(internal->fbOffsetF); if (dirtyOnSubpixelChange && APP->window->fbDirtyOnSubpixelChange() && offsetFDelta.square() >= std::pow(0.1f, 2)) { // DEBUG("%p dirty subpixel (%f, %f) (%f, %f)", this, VEC_ARGS(offsetF), VEC_ARGS(internal->fbOffsetF)); setDirty(); } - if (!scale.equals(internal->fbScale)) { - // If rescaled, rerender in the next frame. + // Re-render if rescaled. + else if (!scale.equals(internal->fbScale)) { // DEBUG("%p dirty scale", this); setDirty(); } + // Re-render if viewport is outside framebuffer's clipbox when it was rendered. + else if (!internal->fbClipBox.contains(args.clipBox)) { + setDirty(); + } // It's more important to not lag the frame than to draw the framebuffer if (dirty && APP->window->getFrameDurationRemaining() > 0.0) { - render(scale, offsetF); + render(scale, offsetF, args.clipBox); } if (!internal->fb) @@ -153,7 +160,7 @@ void FramebufferWidget::draw(const DrawArgs& args) { } -void FramebufferWidget::render(math::Vec scale, math::Vec offsetF) { +void FramebufferWidget::render(math::Vec scale, math::Vec offsetF, math::Rect clipBox) { // In case we fail drawing the framebuffer, don't try again the next frame, so reset `dirty` here. dirty = false; NVGcontext* vg = APP->window->vg; @@ -170,7 +177,13 @@ void FramebufferWidget::render(math::Vec scale, math::Vec offsetF) { localBox = getVisibleChildrenBoundingBox(); } - // DEBUG("rendering FramebufferWidget localBox (%g %g %g %g) fbOffset (%g %g) fbScale (%g %g)", RECT_ARGS(localBox), VEC_ARGS(internal->fbOffsetF), VEC_ARGS(internal->fbScale)); + // Intersect local box with viewport if viewportMargin is set + internal->fbClipBox = clipBox.grow(viewportMargin); + if (internal->fbClipBox.size.isFinite()) { + localBox = localBox.intersect(internal->fbClipBox); + } + + // DEBUG("rendering FramebufferWidget localBox (%f, %f; %f, %f) fbOffset (%f, %f) fbScale (%f, %f)", RECT_ARGS(localBox), VEC_ARGS(internal->fbOffsetF), VEC_ARGS(internal->fbScale)); // Transform to world coordinates, then expand to nearest integer coordinates math::Vec min = localBox.getTopLeft().mult(internal->fbScale).plus(internal->fbOffsetF).floor(); math::Vec max = localBox.getBottomRight().mult(internal->fbScale).plus(internal->fbOffsetF).ceil();