#include #include #include namespace rack { namespace widget { FramebufferWidget::FramebufferWidget() { } FramebufferWidget::~FramebufferWidget() { if (fb) nvgluDeleteFramebuffer(fb); } void FramebufferWidget::step() { Widget::step(); // It's more important to not lag the frame than to draw the framebuffer if (APP->window->isFrameOverdue()) return; // Check that scale has been set by `draw()` yet. if (scale.isZero()) return; // Only redraw if FramebufferWidget is dirty if (!dirty) return; // 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; fbScale = 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(). scale = math::Vec(); // Get subpixel offset in range [0, 1) math::Vec offsetI = offset.floor(); fbOffset = offset.minus(offsetI); math::Rect localBox; if (children.empty()) { localBox = box.zeroPos(); } else { localBox = getChildrenBoundingBox(); } // DEBUG("%g %g %g %g, %g %g, %g %g", RECT_ARGS(localBox), VEC_ARGS(fbOffset), VEC_ARGS(fbScale)); // Transform to world coordinates, then expand to nearest integer coordinates math::Vec min = localBox.getTopLeft().mult(fbScale).plus(fbOffset).floor(); math::Vec max = localBox.getBottomRight().mult(fbScale).plus(fbOffset).ceil(); fbBox = math::Rect::fromMinMax(min, max); // DEBUG("%g %g %g %g", RECT_ARGS(fbBox)); math::Vec newFbSize = fbBox.size.mult(APP->window->pixelRatio).ceil(); // Create framebuffer if a new size is needed if (!fb || !newFbSize.isEqual(fbSize)) { fbSize = newFbSize; // Delete old framebuffer if (fb) nvgluDeleteFramebuffer(fb); // Create a framebuffer at the oversampled size if (fbSize.isFinite() && !fbSize.isZero()) fb = nvgluCreateFramebuffer(vg, fbSize.x * oversample, fbSize.y * oversample, 0); } if (!fb) { WARN("Framebuffer of size (%f, %f) * %f could not be created for FramebufferWidget.", VEC_ARGS(fbSize), oversample); return; } nvgluBindFramebuffer(fb); drawFramebuffer(); nvgluBindFramebuffer(NULL); // If oversampling, create another framebuffer and copy it to actual size. if (oversample != 1.0) { NVGLUframebuffer* newFb = nvgluCreateFramebuffer(vg, fbSize.x, fbSize.y, 0); if (!newFb) { WARN("Non-oversampled framebuffer of size (%f, %f) could not be created for FramebufferWidget.", VEC_ARGS(fbSize)); return; } // Use NanoVG for resizing framebuffers nvgluBindFramebuffer(newFb); nvgBeginFrame(vg, fbBox.size.x, fbBox.size.y, 1.0); // Draw oversampled framebuffer nvgBeginPath(vg); nvgRect(vg, 0.0, 0.0, fbSize.x, fbSize.y); NVGpaint paint = nvgImagePattern(vg, 0.0, 0.0, fbSize.x, fbSize.y, 0.0, fb->image, 1.0); nvgFillPaint(vg, paint); nvgFill(vg); glViewport(0.0, 0.0, fbSize.x, fbSize.y); glClearColor(0.0, 0.0, 0.0, 0.0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); nvgEndFrame(vg); nvgReset(vg); nvgluBindFramebuffer(NULL); // Swap the framebuffers nvgluDeleteFramebuffer(fb); fb = newFb; } } void FramebufferWidget::draw(const DrawArgs& args) { // Draw directly if already drawing in a framebuffer if (bypass || args.fb) { Widget::draw(args); return; } // Get world transform float xform[6]; nvgCurrentTransform(args.vg, xform); // Skew and rotate is not supported if (!math::isNear(xform[1], 0.f) || !math::isNear(xform[2], 0.f)) { WARN("Skew and rotation detected but not supported in FramebufferWidget."); return; } // Extract scale and offset from world transform scale = math::Vec(xform[0], xform[3]); offset = math::Vec(xform[4], xform[5]); math::Vec offsetI = offset.floor(); math::Vec scaleRatio = math::Vec(1, 1); if (!fbScale.isZero() && !scale.isEqual(fbScale)) { dirty = true; // Continue to draw but at the wrong scale. In the next frame, the framebuffer will be redrawn. scaleRatio = scale.div(fbScale); } if (!fb) return; // Draw framebuffer image, using world coordinates nvgSave(args.vg); nvgResetTransform(args.vg); nvgBeginPath(args.vg); nvgRect(args.vg, offsetI.x + fbBox.pos.x, offsetI.y + fbBox.pos.y, fbBox.size.x * scaleRatio.x, fbBox.size.y * scaleRatio.y); NVGpaint paint = nvgImagePattern(args.vg, offsetI.x + fbBox.pos.x, offsetI.y + fbBox.pos.y, fbBox.size.x * scaleRatio.x, fbBox.size.y * scaleRatio.y, 0.0, fb->image, 1.0); nvgFillPaint(args.vg, paint); nvgFill(args.vg); // For debugging the bounding box of the framebuffer // nvgStrokeWidth(args.vg, 2.0); // nvgStrokeColor(args.vg, nvgRGBAf(1, 1, 0, 0.5)); // nvgStroke(args.vg); nvgRestore(args.vg); } void FramebufferWidget::drawFramebuffer() { NVGcontext* vg = APP->window->vg; float pixelRatio = fbSize.x * oversample / fbBox.size.x; nvgBeginFrame(vg, fbBox.size.x, fbBox.size.y, pixelRatio); // Use local scaling nvgTranslate(vg, -fbBox.pos.x, -fbBox.pos.y); nvgTranslate(vg, fbOffset.x, fbOffset.y); nvgScale(vg, fbScale.x, fbScale.y); DrawArgs args; args.vg = vg; args.clipBox = box.zeroPos(); args.fb = fb; Widget::draw(args); glViewport(0.0, 0.0, fbSize.x * oversample, fbSize.y * oversample); glClearColor(0.0, 0.0, 0.0, 0.0); // glClearColor(0.0, 1.0, 1.0, 0.5); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); nvgEndFrame(vg); // Clean up the NanoVG state so that calls to nvgTextBounds() etc during step() don't use a dirty state. nvgReset(vg); } int FramebufferWidget::getImageHandle() { if (!fb) return -1; return fb->image; } } // namespace widget } // namespace rack