diff --git a/src/widget/FramebufferWidget.cpp b/src/widget/FramebufferWidget.cpp index f0a65aee..418c6ba2 100644 --- a/src/widget/FramebufferWidget.cpp +++ b/src/widget/FramebufferWidget.cpp @@ -9,10 +9,16 @@ namespace widget { struct FramebufferWidget::Internal { NVGLUframebuffer* fb = NULL; + + // Set by draw() + /** Scale relative to the world */ math::Vec scale; - /** Offset in world coordinates */ - math::Vec offset; + /** Subpixel offset in world coordinates */ + math::Vec offsetF; + + // Set by step() and drawFramebuffer() + /** Pixel dimensions of the allocated framebuffer */ math::Vec fbSize; /** Bounding box in world coordinates of where the framebuffer should be painted. @@ -22,7 +28,7 @@ struct FramebufferWidget::Internal { /** Framebuffer's scale relative to the world */ math::Vec fbScale; /** Framebuffer's subpixel offset relative to fbBox in world coordinates */ - math::Vec fbOffset; + math::Vec fbOffsetF; }; @@ -64,12 +70,7 @@ void FramebufferWidget::step() { NVGcontext* vg = APP->window->vg; internal->fbScale = internal->scale; - // Set scale to zero so we must wait for the next draw() call before drawing the framebuffer again. - // Otherwise, if the zoom level is changed while the FramebufferWidget is off-screen, the next draw() call will be skipped, the `dirty` flag will be true, and the framebuffer will be redrawn, but at the wrong scale, since it was not set in draw(). - internal->scale = math::Vec(); - // Get subpixel offset in range [0, 1) - math::Vec offsetI = internal->offset.floor(); - internal->fbOffset = internal->offset.minus(offsetI); + internal->fbOffsetF = internal->offsetF; math::Rect localBox; if (children.empty()) { @@ -79,12 +80,12 @@ void FramebufferWidget::step() { localBox = getChildrenBoundingBox(); } - // DEBUG("%g %g %g %g, %g %g, %g %g", RECT_ARGS(localBox), VEC_ARGS(internal->fbOffset), VEC_ARGS(internal->fbScale)); + // DEBUG("%g %g %g %g, %g %g, %g %g", 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->fbOffset).floor(); - math::Vec max = localBox.getBottomRight().mult(internal->fbScale).plus(internal->fbOffset).ceil(); + math::Vec min = localBox.getTopLeft().mult(internal->fbScale).plus(internal->fbOffsetF).floor(); + math::Vec max = localBox.getBottomRight().mult(internal->fbScale).plus(internal->fbOffsetF).ceil(); internal->fbBox = math::Rect::fromMinMax(min, max); - // DEBUG("%g %g %g %g", RECT_ARGS(fbBox)); + // DEBUG("%g %g %g %g", RECT_ARGS(internal->fbBox)); math::Vec newFbSize = internal->fbBox.size.mult(APP->window->pixelRatio).ceil(); @@ -94,38 +95,52 @@ void FramebufferWidget::step() { // Delete old framebuffer if (internal->fb) nvgluDeleteFramebuffer(internal->fb); - // Create a framebuffer at the oversampled size - if (internal->fbSize.isFinite() && !internal->fbSize.isZero()) - internal->fb = nvgluCreateFramebuffer(vg, internal->fbSize.x * oversample, internal->fbSize.y * oversample, 0); + // Create a framebuffer + if (internal->fbSize.isFinite() && !internal->fbSize.isZero()) { + internal->fb = nvgluCreateFramebuffer(vg, internal->fbSize.x, internal->fbSize.y, 0); + } } - if (!internal->fb) { - WARN("Framebuffer of size (%f, %f) * %f could not be created for FramebufferWidget.", VEC_ARGS(internal->fbSize), oversample); + WARN("Framebuffer of size (%f, %f) could not be created for FramebufferWidget %p.", VEC_ARGS(internal->fbSize), this); return; } - nvgluBindFramebuffer(internal->fb); - drawFramebuffer(); - nvgluBindFramebuffer(NULL); + // Render to framebuffer + if (oversample == 1.0) { + // If not oversampling, render directly to framebuffer. + nvgluBindFramebuffer(internal->fb); + drawFramebuffer(); + nvgluBindFramebuffer(NULL); + } + else { + NVGLUframebuffer* fb = internal->fb; + // If oversampling, create another framebuffer and copy it to actual size. + math::Vec oversampledFbSize = internal->fbSize.mult(oversample).ceil(); + NVGLUframebuffer* oversampledFb = nvgluCreateFramebuffer(vg, oversampledFbSize.x, oversampledFbSize.y, 0); - // If oversampling, create another framebuffer and copy it to actual size. - if (oversample != 1.0) { - NVGLUframebuffer* newFb = nvgluCreateFramebuffer(vg, internal->fbSize.x, internal->fbSize.y, 0); - if (!newFb) { - WARN("Non-oversampled framebuffer of size (%f, %f) could not be created for FramebufferWidget.", VEC_ARGS(internal->fbSize)); + if (!oversampledFb) { + WARN("Oversampled framebuffer of size (%f, %f) could not be created for FramebufferWidget %p.", VEC_ARGS(oversampledFbSize), this); return; } + // Render to oversampled framebuffer. + nvgluBindFramebuffer(oversampledFb); + internal->fb = oversampledFb; + drawFramebuffer(); + internal->fb = fb; + nvgluBindFramebuffer(NULL); + // Use NanoVG for resizing framebuffers - nvgluBindFramebuffer(newFb); + nvgluBindFramebuffer(internal->fb); nvgBeginFrame(vg, internal->fbBox.size.x, internal->fbBox.size.y, 1.0); // Draw oversampled framebuffer nvgBeginPath(vg); nvgRect(vg, 0.0, 0.0, internal->fbSize.x, internal->fbSize.y); - NVGpaint paint = nvgImagePattern(vg, 0.0, 0.0, internal->fbSize.x, internal->fbSize.y, - 0.0, internal->fb->image, 1.0); + NVGpaint paint = nvgImagePattern(vg, 0.0, 0.0, + internal->fbSize.x, internal->fbSize.y, + 0.0, oversampledFb->image, 1.0); nvgFillPaint(vg, paint); nvgFill(vg); @@ -136,10 +151,7 @@ void FramebufferWidget::step() { nvgReset(vg); nvgluBindFramebuffer(NULL); - - // Swap the framebuffers - nvgluDeleteFramebuffer(internal->fb); - internal->fb = newFb; + nvgluDeleteFramebuffer(oversampledFb); } } @@ -159,17 +171,30 @@ void FramebufferWidget::draw(const DrawArgs& args) { WARN("Skew and rotation detected but not supported in FramebufferWidget."); return; } + // Extract scale and offset from world transform internal->scale = math::Vec(xform[0], xform[3]); - internal->offset = math::Vec(xform[4], xform[5]); - math::Vec offsetI = internal->offset.floor(); + math::Vec offset = math::Vec(xform[4], xform[5]); + math::Vec offsetI = offset.floor(); + internal->offsetF = offset.minus(offsetI); + + if (!math::isNear(internal->offsetF.x, internal->fbOffsetF.x, 0.01f) || !math::isNear(internal->offsetF.y, internal->fbOffsetF.y, 0.01f)) { + // If drawing to a new subpixel location, rerender in the next frame. + // DEBUG("%p dirty subpixel", this); + dirty = true; + } + if (!internal->scale.isEqual(internal->fbScale)) { + // If rescaled, rerender in the next frame. + // DEBUG("%p dirty scale", this); + dirty = true; + } math::Vec scaleRatio = math::Vec(1, 1); if (!internal->fbScale.isZero() && !internal->scale.isEqual(internal->fbScale)) { - dirty = true; - // Continue to draw but at the wrong scale. In the next frame, the framebuffer will be redrawn. + // Continue to draw with the last framebuffer, but stretch it to rescale. scaleRatio = internal->scale.div(internal->fbScale); } + // DEBUG("%f %f %f %f", scaleRatio.x, scaleRatio.y, offsetF.x, offsetF.y); if (!internal->fb) return; @@ -178,15 +203,18 @@ void FramebufferWidget::draw(const DrawArgs& args) { nvgSave(args.vg); nvgResetTransform(args.vg); + // DEBUG("%f %f %f %f, %f %f", RECT_ARGS(internal->fbBox), VEC_ARGS(internal->fbSize)); nvgBeginPath(args.vg); nvgRect(args.vg, offsetI.x + internal->fbBox.pos.x, offsetI.y + internal->fbBox.pos.y, - internal->fbBox.size.x * scaleRatio.x, internal->fbBox.size.y * scaleRatio.y); + internal->fbBox.size.x * scaleRatio.x, + internal->fbBox.size.y * scaleRatio.y); NVGpaint paint = nvgImagePattern(args.vg, offsetI.x + internal->fbBox.pos.x, offsetI.y + internal->fbBox.pos.y, - internal->fbBox.size.x * scaleRatio.x, internal->fbBox.size.y * scaleRatio.y, + internal->fbBox.size.x * scaleRatio.x, + internal->fbBox.size.y * scaleRatio.y, 0.0, internal->fb->image, 1.0); nvgFillPaint(args.vg, paint); nvgFill(args.vg); @@ -202,13 +230,14 @@ void FramebufferWidget::draw(const DrawArgs& args) { void FramebufferWidget::drawFramebuffer() { NVGcontext* vg = APP->window->vg; + nvgSave(vg); float pixelRatio = internal->fbSize.x * oversample / internal->fbBox.size.x; nvgBeginFrame(vg, internal->fbBox.size.x, internal->fbBox.size.y, pixelRatio); // Use local scaling nvgTranslate(vg, -internal->fbBox.pos.x, -internal->fbBox.pos.y); - nvgTranslate(vg, internal->fbOffset.x, internal->fbOffset.y); + nvgTranslate(vg, internal->fbOffsetF.x, internal->fbOffsetF.y); nvgScale(vg, internal->fbScale.x, internal->fbScale.y); DrawArgs args; @@ -225,6 +254,7 @@ void FramebufferWidget::drawFramebuffer() { // Clean up the NanoVG state so that calls to nvgTextBounds() etc during step() don't use a dirty state. nvgReset(vg); + nvgRestore(vg); }