Browse Source

Rerender FramebufferWidget when subpixel position changes, fixing slight pixel offset rendering issue. Rewrite oversampled framebuffer rendering code, which solves rendering issue when oversampled buffer sometimes replaces the normal sized framebuffer.

tags/v2.0.0
Andrew Belt 5 years ago
parent
commit
3026f113e3
1 changed files with 70 additions and 40 deletions
  1. +70
    -40
      src/widget/FramebufferWidget.cpp

+ 70
- 40
src/widget/FramebufferWidget.cpp View File

@@ -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);
}




Loading…
Cancel
Save