diff --git a/libs/ui/canvas/kis_canvas2.cpp b/libs/ui/canvas/kis_canvas2.cpp --- a/libs/ui/canvas/kis_canvas2.cpp +++ b/libs/ui/canvas/kis_canvas2.cpp @@ -1052,7 +1052,23 @@ void KisCanvas2::documentOffsetMoved(const QPoint &documentOffset) { QPointF offsetBefore = m_d->coordinatesConverter->imageRectInViewportPixels().topLeft(); - m_d->coordinatesConverter->setDocumentOffset(documentOffset); + + qreal devicePixelRatio = m_d->coordinatesConverter->devicePixelRatio(); + // The given offset is in widget logical pixels. In order to prevent fuzzy + // canvas rendering at 100% pixel-perfect zoom level when devicePixelRatio + // is not integral, we adjusts the offset to map to whole device pixels. + // We use qFloor here since the offset can be negative. + int deviceOffsetX = qFloor(documentOffset.x() * devicePixelRatio); + int deviceOffsetY = qFloor(documentOffset.y() * devicePixelRatio); + // These adjusted offsets will be in logical pixel but is aligned in device + // pixel space for pixel-perfect rendering. + qreal pixelPerfectOffsetX = deviceOffsetX / devicePixelRatio; + qreal pixelPerfectOffsetY = deviceOffsetY / devicePixelRatio; + // FIXME: This is a temporary hack for fixing the canvas under fractional + // DPI scaling before a new coordinate system is introduced. + QPointF offsetAdjusted(pixelPerfectOffsetX, pixelPerfectOffsetY); + + m_d->coordinatesConverter->setDocumentOffset(offsetAdjusted); QPointF offsetAfter = m_d->coordinatesConverter->imageRectInViewportPixels().topLeft(); QPointF moveOffset = offsetAfter - offsetBefore; diff --git a/libs/ui/canvas/kis_coordinates_converter.h b/libs/ui/canvas/kis_coordinates_converter.h --- a/libs/ui/canvas/kis_coordinates_converter.h +++ b/libs/ui/canvas/kis_coordinates_converter.h @@ -59,11 +59,12 @@ KisCoordinatesConverter(); ~KisCoordinatesConverter() override; - void setCanvasWidgetSize(QSize size); + void setCanvasWidgetSize(QSizeF size); void setDevicePixelRatio(qreal value); void setImage(KisImageWSP image); - void setDocumentOffset(const QPoint &offset); + void setDocumentOffset(const QPointF &offset); + qreal devicePixelRatio() const; QPoint documentOffset() const; qreal rotationAngle() const; diff --git a/libs/ui/canvas/kis_coordinates_converter.cpp b/libs/ui/canvas/kis_coordinates_converter.cpp --- a/libs/ui/canvas/kis_coordinates_converter.cpp +++ b/libs/ui/canvas/kis_coordinates_converter.cpp @@ -150,7 +150,7 @@ delete m_d; } -void KisCoordinatesConverter::setCanvasWidgetSize(QSize size) +void KisCoordinatesConverter::setCanvasWidgetSize(QSizeF size) { m_d->canvasWidgetSize = size; recalculateTransformations(); @@ -167,15 +167,20 @@ recalculateTransformations(); } -void KisCoordinatesConverter::setDocumentOffset(const QPoint& offset) +void KisCoordinatesConverter::setDocumentOffset(const QPointF& offset) { QPointF diff = m_d->documentOffset - offset; m_d->documentOffset = offset; m_d->flakeToWidget *= QTransform::fromTranslate(diff.x(), diff.y()); recalculateTransformations(); } +qreal KisCoordinatesConverter::devicePixelRatio() const +{ + return m_d->devicePixelRatio; +} + QPoint KisCoordinatesConverter::documentOffset() const { return QPoint(int(m_d->documentOffset.x()), int(m_d->documentOffset.y())); diff --git a/libs/ui/opengl/kis_opengl_canvas2.h b/libs/ui/opengl/kis_opengl_canvas2.h --- a/libs/ui/opengl/kis_opengl_canvas2.h +++ b/libs/ui/opengl/kis_opengl_canvas2.h @@ -119,6 +119,8 @@ void drawImage(); void drawCheckers(); void drawGrid(); + QSize viewportDevicePixelSize() const; + QSizeF widgetSizeAlignedToDevicePixel() const; private: diff --git a/libs/ui/opengl/kis_opengl_canvas2.cpp b/libs/ui/opengl/kis_opengl_canvas2.cpp --- a/libs/ui/opengl/kis_opengl_canvas2.cpp +++ b/libs/ui/opengl/kis_opengl_canvas2.cpp @@ -382,9 +382,11 @@ cfg.setCanvasState("OPENGL_FAILED"); } -void KisOpenGLCanvas2::resizeGL(int width, int height) +void KisOpenGLCanvas2::resizeGL(int /*width*/, int /*height*/) { - coordinatesConverter()->setCanvasWidgetSize(QSize(width, height)); + // The given size is the widget size but here we actually want to give + // KisCoordinatesConverter the viewport size aligned to device pixels. + coordinatesConverter()->setCanvasWidgetSize(widgetSizeAlignedToDevicePixel()); paintGL(); } @@ -421,10 +423,14 @@ return; } + QSizeF widgetSize = widgetSizeAlignedToDevicePixel(); + // setup the mvp transformation QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); - projectionMatrix.ortho(0, width(), height(), 0, NEAR_VAL, FAR_VAL); + // FIXME: It may be better to have the projection in device pixel, but + // this requires introducing a new coordinate system. + projectionMatrix.ortho(0, widgetSize.width(), widgetSize.height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(coordinatesConverter()->flakeToWidgetTransform()); @@ -521,9 +527,10 @@ QRectF textureRect; QRectF modelRect; + QSizeF widgetSize = widgetSizeAlignedToDevicePixel(); QRectF viewportRect = !d->wrapAroundMode ? converter->imageRectInViewportPixels() : - converter->widgetToViewport(this->rect()); + converter->widgetToViewport(QRectF(0, 0, widgetSize.width(), widgetSize.height())); if (!canvas()->renderingLimit().isEmpty()) { const QRect vrect = converter->imageToViewport(canvas()->renderingLimit()).toAlignedRect(); @@ -543,7 +550,9 @@ QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); - projectionMatrix.ortho(0, width(), height(), 0, NEAR_VAL, FAR_VAL); + // FIXME: It may be better to have the projection in device pixel, but + // this requires introducing a new coordinate system. + projectionMatrix.ortho(0, widgetSize.width(), widgetSize.height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(modelTransform); @@ -591,9 +600,13 @@ return; } + QSizeF widgetSize = widgetSizeAlignedToDevicePixel(); + QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); - projectionMatrix.ortho(0, width(), height(), 0, NEAR_VAL, FAR_VAL); + // FIXME: It may be better to have the projection in device pixel, but + // this requires introducing a new coordinate system. + projectionMatrix.ortho(0, widgetSize.width(), widgetSize.height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(coordinatesConverter()->imageToWidgetTransform()); @@ -613,7 +626,7 @@ d->lineBuffer.bind(); } - QRectF widgetRect(0,0, width(), height()); + QRectF widgetRect(0,0, widgetSize.width(), widgetSize.height()); QRectF widgetRectInImagePixels = coordinatesConverter()->documentToImage(coordinatesConverter()->widgetToDocument(widgetRect)); QRect wr = widgetRectInImagePixels.toAlignedRect(); @@ -666,9 +679,13 @@ d->displayShader->bind(); + QSizeF widgetSize = widgetSizeAlignedToDevicePixel(); + QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); - projectionMatrix.ortho(0, width(), height(), 0, NEAR_VAL, FAR_VAL); + // FIXME: It may be better to have the projection in device pixel, but + // this requires introducing a new coordinate system. + projectionMatrix.ortho(0, widgetSize.width(), widgetSize.height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(converter->imageToWidgetTransform()); @@ -680,7 +697,7 @@ textureMatrix.setToIdentity(); d->displayShader->setUniformValue(d->displayShader->location(Uniform::TextureMatrix), textureMatrix); - QRectF widgetRect(0,0, width(), height()); + QRectF widgetRect(0,0, widgetSize.width(), widgetSize.height()); QRectF widgetRectInImagePixels = converter->documentToImage(converter->widgetToDocument(widgetRect)); const QRect renderingLimit = canvas()->renderingLimit(); @@ -838,6 +855,23 @@ glDisable(GL_BLEND); } +QSize KisOpenGLCanvas2::viewportDevicePixelSize() const +{ + // This is how QOpenGLCanvas sets the FBO and the viewport size. If + // devicePixelRatioF() is non-integral, the result is truncated. + int viewportWidth = static_cast(width() * devicePixelRatioF()); + int viewportHeight = static_cast(height() * devicePixelRatioF()); + return QSize(viewportWidth, viewportHeight); +} + +QSizeF KisOpenGLCanvas2::widgetSizeAlignedToDevicePixel() const +{ + QSize viewportSize = viewportDevicePixelSize(); + qreal scaledWidth = viewportSize.width() / devicePixelRatioF(); + qreal scaledHeight = viewportSize.height() / devicePixelRatioF(); + return QSizeF(scaledWidth, scaledHeight); +} + void KisOpenGLCanvas2::slotConfigChanged() { KisConfig cfg(true);