diff --git a/data/pref.xml b/data/pref.xml
index 5185574e8..26f4796d6 100644
--- a/data/pref.xml
+++ b/data/pref.xml
@@ -127,6 +127,12 @@
+
+
+
+
+
+
@@ -176,6 +182,7 @@
some performance issue rendering huge sprites with small
zoom levels -->
+
diff --git a/laf b/laf
index f473cc6f8..7c819791e 160000
--- a/laf
+++ b/laf
@@ -1 +1 @@
-Subproject commit f473cc6f8c68629ef0e8c480afbba5659ef5de75
+Subproject commit 7c819791e50116b54e0666dbf96822e0993d1d1f
diff --git a/src/app/script/values.cpp b/src/app/script/values.cpp
index ddf14855a..490b33c81 100644
--- a/src/app/script/values.cpp
+++ b/src/app/script/values.cpp
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2019-2021 Igara Studio S.A.
+// Copyright (C) 2019-2022 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@@ -165,6 +165,7 @@ FOR_ENUM(app::gen::BgType)
FOR_ENUM(app::gen::BrushPreview)
FOR_ENUM(app::gen::BrushType)
FOR_ENUM(app::gen::ColorProfileBehavior)
+FOR_ENUM(app::gen::Downsampling)
FOR_ENUM(app::gen::EyedropperChannel)
FOR_ENUM(app::gen::EyedropperSample)
FOR_ENUM(app::gen::FillReferTo)
diff --git a/src/app/ui/context_bar.cpp b/src/app/ui/context_bar.cpp
index faa854f34..9c4775591 100644
--- a/src/app/ui/context_bar.cpp
+++ b/src/app/ui/context_bar.cpp
@@ -56,6 +56,7 @@
#include "doc/selected_objects.h"
#include "doc/slice.h"
#include "obs/connection.h"
+#include "os/sampling.h"
#include "os/surface.h"
#include "os/system.h"
#include "render/dithering.h"
@@ -133,6 +134,49 @@ private:
}
};
+class ContextBar::SamplingOptions : public HBox {
+public:
+ class Item : public ListItem {
+ public:
+ Item(const char* label,
+ const gen::Downsampling sampling)
+ : ListItem(label)
+ , m_sampling(sampling) {
+ }
+ const gen::Downsampling& sampling() const { return m_sampling; }
+ private:
+ gen::Downsampling m_sampling;
+ };
+
+ SamplingOptions()
+ : m_downsamplingLabel("Downsampling:")
+ {
+ addChild(&m_downsamplingLabel);
+ addChild(&m_downsampling);
+
+ m_downsampling.addItem(new Item("Nearest", gen::Downsampling::NEAREST));
+ m_downsampling.addItem(new Item("Bilinear", gen::Downsampling::BILINEAR));
+ m_downsampling.addItem(new Item("Bilinear mipmapping", gen::Downsampling::BILINEAR_MIPMAP));
+ m_downsampling.addItem(new Item("Trilinear mipmapping", gen::Downsampling::TRILINEAR_MIPMAP));
+ m_downsampling.setSelectedItemIndex(
+ (int)Preferences::instance().editor.downsampling());
+
+ m_downsampling.Change.connect([this]{ onDownsamplingChange(); });
+ }
+
+private:
+
+ void onDownsamplingChange() {
+ if (auto item = dynamic_cast- (m_downsampling.getSelectedItem())) {
+ Preferences::instance().editor.downsampling(
+ item->sampling());
+ }
+ }
+
+ Label m_downsamplingLabel;
+ ComboBox m_downsampling;
+};
+
class ContextBar::BrushBackField : public ButtonSet {
public:
BrushBackField()
@@ -1537,6 +1581,7 @@ ContextBar::ContextBar(TooltipManager* tooltipManager,
m_selectionOptionsBox->addChild(m_rotAlgo = new RotAlgorithmField());
addChild(m_zoomButtons = new ZoomButtons);
+ addChild(m_samplingOptions = new SamplingOptions);
addChild(m_brushBack = new BrushBackField);
addChild(m_brushType = new BrushTypeField(this));
@@ -1843,13 +1888,6 @@ void ContextBar::updateForTool(tools::Tool* tool)
(activeBrush()->type() == kSquareBrushType ||
activeBrush()->type() == kLineBrushType);
- // True if the current tool is eyedropper.
- const bool needZoomButtons = tool &&
- (tool->getInk(0)->isZoom() ||
- tool->getInk(1)->isZoom() ||
- tool->getInk(0)->isScrollMovement() ||
- tool->getInk(1)->isScrollMovement());
-
// True if the current tool is eyedropper.
const bool isEyedropper = tool &&
(tool->getInk(0)->isEyedropper() ||
@@ -1902,7 +1940,7 @@ void ContextBar::updateForTool(tools::Tool* tool)
const bool supportDynamics = (!hasImageBrush);
// Show/Hide fields
- m_zoomButtons->setVisible(needZoomButtons);
+ m_zoomButtons->setVisible(needZoomButtons(tool));
m_brushBack->setVisible(supportOpacity && hasImageBrush && !withDithering);
m_brushType->setVisible(supportOpacity && (!isFloodfill || (isFloodfill && hasImageBrush && !withDithering)));
m_brushSize->setVisible(supportOpacity && !isFloodfill && !hasImageBrush);
@@ -1940,7 +1978,11 @@ void ContextBar::updateForTool(tools::Tool* tool)
if (updateShade)
m_inkShades->updateShadeFromColorBarPicks();
- layout();
+ if (!updateSamplingVisibility(tool)) {
+ // updateSamplingVisibility() returns false if it doesn't layout()
+ // the ContextBar.
+ layout();
+ }
}
void ContextBar::updateForMovingPixels()
@@ -1980,6 +2022,26 @@ void ContextBar::updateToolLoopModifiersIndicators(tools::ToolLoopModifiers modi
m_selectionMode->setSelectionMode(mode);
}
+bool ContextBar::updateSamplingVisibility(tools::Tool* tool)
+{
+ if (!tool)
+ tool = App::instance()->activeTool();
+
+ const bool newVisibility =
+ needZoomButtons(tool) &&
+ current_editor &&
+ (current_editor->projection().scaleX() < 1.0 ||
+ current_editor->projection().scaleY() < 1.0) &&
+ current_editor->isUsingNewRenderEngine();
+
+ if (newVisibility == m_samplingOptions->hasFlags(HIDDEN)) {
+ m_samplingOptions->setVisible(newVisibility);
+ layout();
+ return true;
+ }
+ return false;
+}
+
void ContextBar::updateAutoSelectLayer(bool state)
{
m_autoSelectLayer->setSelected(state);
@@ -2285,4 +2347,13 @@ void ContextBar::showDynamics()
m_dynamics->switchPopup();
}
+bool ContextBar::needZoomButtons(tools::Tool* tool) const
+{
+ return tool &&
+ (tool->getInk(0)->isZoom() ||
+ tool->getInk(1)->isZoom() ||
+ tool->getInk(0)->isScrollMovement() ||
+ tool->getInk(1)->isScrollMovement());
+}
+
} // namespace app
diff --git a/src/app/ui/context_bar.h b/src/app/ui/context_bar.h
index 254d9ef9b..c62e76bb8 100644
--- a/src/app/ui/context_bar.h
+++ b/src/app/ui/context_bar.h
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2018-2020 Igara Studio S.A.
+// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@@ -67,6 +67,7 @@ namespace app {
void updateForMovingPixels();
void updateForSelectingBox(const std::string& text);
void updateToolLoopModifiersIndicators(tools::ToolLoopModifiers modifiers);
+ bool updateSamplingVisibility(tools::Tool* tool = nullptr);
void updateAutoSelectLayer(bool state);
bool isAutoSelectLayer() const;
@@ -129,8 +130,10 @@ namespace app {
void registerCommands();
void showBrushes();
void showDynamics();
+ bool needZoomButtons(tools::Tool* tool) const;
class ZoomButtons;
+ class SamplingOptions;
class BrushBackField;
class BrushTypeField;
class BrushAngleField;
@@ -158,6 +161,7 @@ namespace app {
class SliceFields;
ZoomButtons* m_zoomButtons;
+ SamplingOptions* m_samplingOptions;
BrushBackField* m_brushBack;
BrushTypeField* m_brushType;
BrushAngleField* m_brushAngle;
diff --git a/src/app/ui/editor/editor.cpp b/src/app/ui/editor/editor.cpp
index 1a98de703..7c9f97cf1 100644
--- a/src/app/ui/editor/editor.cpp
+++ b/src/app/ui/editor/editor.cpp
@@ -60,6 +60,7 @@
#include "doc/mask_boundaries.h"
#include "doc/slice.h"
#include "os/color_space.h"
+#include "os/sampling.h"
#include "os/surface.h"
#include "os/system.h"
#include "os/window.h"
@@ -174,6 +175,10 @@ Editor::Editor(Doc* document, EditorFlags flags)
Preferences::instance().colorBar.fgColor.AfterChange.connect(
[this]{ onFgColorChange(); });
+ m_samplingChangeConn =
+ Preferences::instance().editor.downsampling.AfterChange.connect(
+ [this]{ onSamplingChange(); });
+
m_contextBarBrushChangeConn =
App::instance()->contextBar()->BrushChange.connect(
[this]{ onContextBarBrushChange(); });
@@ -239,6 +244,18 @@ bool Editor::isActive() const
return (current_editor == this);
}
+bool Editor::isUsingNewRenderEngine() const
+{
+ ASSERT(m_sprite);
+ return
+ (Preferences::instance().experimental.newRenderEngine()
+ // Reference layers + zoom > 100% need the old render engine for
+ // sub-pixel rendering.
+ && (!m_sprite->hasVisibleReferenceLayers()
+ || (m_proj.scaleX() <= 1.0
+ && m_proj.scaleY() <= 1.0)));
+}
+
// static
WidgetType Editor::Type()
{
@@ -410,6 +427,9 @@ void Editor::setZoom(const render::Zoom& zoom)
if (m_proj.zoom() != zoom) {
m_proj.setZoom(zoom);
notifyZoomChanged();
+
+ if (isActive())
+ App::instance()->contextBar()->updateSamplingVisibility();
}
else {
// Just copy the zoom as the internal "Zoom::m_internalScale"
@@ -582,13 +602,8 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& sprite
return;
// rc2 is the rectangle used to create a temporal rendered image of the sprite
- const bool newEngine =
- (Preferences::instance().experimental.newRenderEngine()
- // Reference layers + zoom > 100% need the old render engine for
- // sub-pixel rendering.
- && (!m_sprite->hasVisibleReferenceLayers()
- || (m_proj.scaleX() <= 1.0
- && m_proj.scaleY() <= 1.0)));
+ const auto& pref = Preferences::instance();
+ const bool newEngine = isUsingNewRenderEngine();
gfx::Rect rc2;
if (newEngine) {
rc2 = expose; // New engine, exposed rectangle (without zoom)
@@ -614,11 +629,11 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& sprite
rendered.reset(Image::create(IMAGE_RGB, rc2.w, rc2.h,
m_renderEngine->getRenderImageBuffer()));
- m_renderEngine->setNewBlendMethod(Preferences::instance().experimental.newBlend());
+ m_renderEngine->setNewBlendMethod(pref.experimental.newBlend());
m_renderEngine->setRefLayersVisiblity(true);
m_renderEngine->setSelectedLayer(m_layer);
if (m_flags & Editor::kUseNonactiveLayersOpacityWhenEnabled)
- m_renderEngine->setNonactiveLayersOpacity(Preferences::instance().experimental.nonactiveLayersOpacity());
+ m_renderEngine->setNonactiveLayersOpacity(pref.experimental.nonactiveLayersOpacity());
else
m_renderEngine->setNonactiveLayersOpacity(255);
m_renderEngine->setProjection(
@@ -710,7 +725,30 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& sprite
tmp.get(), 0, 0, 0, 0, rc2.w, rc2.h);
if (newEngine) {
- g->drawSurface(tmp.get(), gfx::Rect(0, 0, rc2.w, rc2.h), dest);
+ os::Sampling sampling;
+ if (m_proj.scaleX() < 1.0) {
+ switch (pref.editor.downsampling()) {
+ case gen::Downsampling::NEAREST:
+ sampling = os::Sampling(os::Sampling::Filter::Nearest);
+ break;
+ case gen::Downsampling::BILINEAR:
+ sampling = os::Sampling(os::Sampling::Filter::Linear);
+ break;
+ case gen::Downsampling::BILINEAR_MIPMAP:
+ sampling = os::Sampling(os::Sampling::Filter::Linear,
+ os::Sampling::Mipmap::Nearest);
+ break;
+ case gen::Downsampling::TRILINEAR_MIPMAP:
+ sampling = os::Sampling(os::Sampling::Filter::Linear,
+ os::Sampling::Mipmap::Linear);
+ break;
+ }
+ }
+
+ g->drawSurface(tmp.get(),
+ gfx::Rect(0, 0, rc2.w, rc2.h),
+ dest,
+ sampling);
}
else {
g->blit(tmp.get(), 0, 0, dest.x, dest.y, dest.w, dest.h);
@@ -2112,6 +2150,15 @@ void Editor::onActiveToolChange(tools::Tool* tool)
}
}
+void Editor::onSamplingChange()
+{
+ if (m_proj.scaleX() < 1.0 &&
+ m_proj.scaleY() < 1.0 &&
+ isUsingNewRenderEngine()) {
+ invalidate();
+ }
+}
+
void Editor::onFgColorChange()
{
m_brushPreview.redraw();
diff --git a/src/app/ui/editor/editor.h b/src/app/ui/editor/editor.h
index 8b8f0a8bb..c429caa04 100644
--- a/src/app/ui/editor/editor.h
+++ b/src/app/ui/editor/editor.h
@@ -108,6 +108,7 @@ namespace app {
static void destroyEditorSharedInternals();
bool isActive() const;
+ bool isUsingNewRenderEngine() const;
DocView* getDocView() { return m_docView; }
void setDocView(DocView* docView) { m_docView = docView; }
@@ -311,6 +312,7 @@ namespace app {
void onResize(ui::ResizeEvent& ev) override;
void onPaint(ui::PaintEvent& ev) override;
void onInvalidateRegion(const gfx::Region& region) override;
+ void onSamplingChange();
void onFgColorChange();
void onContextBarBrushChange();
void onTiledModeBeforeChange();
@@ -405,6 +407,7 @@ namespace app {
ui::Timer m_antsTimer;
int m_antsOffset;
+ obs::scoped_connection m_samplingChangeConn;
obs::scoped_connection m_fgColorChangeConn;
obs::scoped_connection m_contextBarBrushChangeConn;
obs::scoped_connection m_showExtrasConn;
diff --git a/src/ui/graphics.cpp b/src/ui/graphics.cpp
index 44eb97de7..3efd2deee 100644
--- a/src/ui/graphics.cpp
+++ b/src/ui/graphics.cpp
@@ -1,5 +1,5 @@
// Aseprite UI Library
-// Copyright (C) 2019-2021 Igara Studio S.A.
+// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@@ -21,6 +21,7 @@
#include "gfx/size.h"
#include "os/draw_text.h"
#include "os/font.h"
+#include "os/sampling.h"
#include "os/surface.h"
#include "os/system.h"
#include "os/window.h"
@@ -260,6 +261,25 @@ void Graphics::drawSurface(os::Surface* surface,
gfx::Rect(dstRect).offset(m_dx, m_dy));
}
+void Graphics::drawSurface(os::Surface* surface,
+ const gfx::Rect& srcRect,
+ const gfx::Rect& dstRect,
+ const os::Sampling& sampling,
+ const ui::Paint* paint)
+{
+ dirty(gfx::Rect(m_dx+dstRect.x, m_dy+dstRect.y,
+ dstRect.w, dstRect.h));
+
+ os::SurfaceLock lockSrc(surface);
+ os::SurfaceLock lockDst(m_surface.get());
+ m_surface->drawSurface(
+ surface,
+ srcRect,
+ gfx::Rect(dstRect).offset(m_dx, m_dy),
+ sampling,
+ paint);
+}
+
void Graphics::drawRgbaSurface(os::Surface* surface, int x, int y)
{
dirty(gfx::Rect(m_dx+x, m_dy+y, surface->width(), surface->height()));
diff --git a/src/ui/graphics.h b/src/ui/graphics.h
index d0db15fb9..ebdc90ad4 100644
--- a/src/ui/graphics.h
+++ b/src/ui/graphics.h
@@ -1,5 +1,5 @@
// Aseprite UI Library
-// Copyright (C) 2019-2020 Igara Studio S.A.
+// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@@ -30,6 +30,7 @@ namespace gfx {
namespace os {
class DrawTextDelegate;
+ class Sampling;
}
namespace ui {
@@ -89,6 +90,11 @@ namespace ui {
void drawSurface(os::Surface* surface,
const gfx::Rect& srcRect,
const gfx::Rect& dstRect);
+ void drawSurface(os::Surface* surface,
+ const gfx::Rect& srcRect,
+ const gfx::Rect& dstRect,
+ const os::Sampling& sampling,
+ const ui::Paint* paint = nullptr);
void drawRgbaSurface(os::Surface* surface, int x, int y);
void drawRgbaSurface(os::Surface* surface, int srcx, int srcy, int dstx, int dsty, int w, int h);
void drawRgbaSurface(os::Surface* surface,