Cache mouse cursors to avoid re-generating them on each setCursor()

This should improve the mouse movement, where a new mouse cursor was
created on each mouse movement with black & white pixels. It's a
regression introduced in ef4f691459
(which was originally introduced to improve the mouse movement
perception in a 100Hz monitor).

This might be a possible fix for:
https://github.com/aseprite/aseprite/issues/2713
master
David Capello 2021-07-09 10:21:16 -03:00
parent 8f838ba36d
commit 5a6bd465f5
11 changed files with 270 additions and 170 deletions

2
laf

@ -1 +1 @@
Subproject commit 11ffdbd9cc6232faaff5eecd8cc628bb5a2c706f
Subproject commit 80ec051ecf4b702d769d4b2483e1a34b52368bde

View File

@ -331,7 +331,7 @@ public:
// If the platform supports native cursors...
if ((int(os::instance()->capabilities()) &
int(os::Capabilities::CustomNativeMouseCursor)) != 0) {
int(os::Capabilities::CustomMouseCursor)) != 0) {
if (m_pref.cursor.useNativeCursor())
nativeCursor()->setSelected(true);
nativeCursor()->Click.connect([this]{ onNativeCursorChange(); });
@ -855,9 +855,9 @@ private:
void onNativeCursorChange() {
bool state =
// If the platform supports native cursors...
// If the platform supports custom cursors...
(((int(os::instance()->capabilities()) &
int(os::Capabilities::CustomNativeMouseCursor)) != 0) &&
int(os::Capabilities::CustomMouseCursor)) != 0) &&
// If the native cursor option is not selec
!nativeCursor()->isSelected());

View File

@ -42,10 +42,57 @@
#include "ui/manager.h"
#include "ui/system.h"
#include <array>
namespace app {
using namespace doc;
static int g_crosshair_pattern[7*7] = {
0, 0, 0, 1, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0,
1, 1, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0,
};
// We're going to keep a cache of native mouse cursor for each
// possibility of the following crosshair:
//
// 2
// 2
// 22 3 22
// 2
// 2
//
// Number of crosshair cursors 2^8 * 3 + 2 = 770
// Here the center can be black, white or hidden. When the center is
// black or white, the crosshair can be empty.
//
// The index/key of this array is calculated in
// BrushPreview::createCrosshairCursor().
//
// Win32: This is needed to avoid converting from a os::Surface ->
// HCURSOR calling CreateIconIndirect() as many times as possible for
// each mouse movement (because it's a slow function).
//
static std::array<os::CursorRef, 770> g_bwCursors;
static int g_cacheCursorScale = 0;
// 3 cached cursors when we use a solid cursor (1 dot, crosshair
// without dot at the center, crosshair with dot in the center)
static std::array<os::CursorRef, 3> g_solidCursors;
static gfx::Color g_solidCursorColor = gfx::ColorNone;
// static
void BrushPreview::destroyInternals()
{
g_bwCursors.fill(nullptr);
g_solidCursors.fill(nullptr);
}
BrushPreview::BrushPreview(Editor* editor)
: m_editor(editor)
{
@ -53,7 +100,6 @@ BrushPreview::BrushPreview(Editor* editor)
BrushPreview::~BrushPreview()
{
m_cursor.reset();
}
BrushRef BrushPreview::getCurrentBrush()
@ -310,9 +356,9 @@ void BrushPreview::show(const gfx::Point& screenPos)
ui::SetClip clip(&g);
gfx::Color uiCursorColor = color_utils::color_for_ui(appCursorColor);
createNativeCursor();
if (m_cursor)
forEachLittleCrossPixel(&g, m_screenPosition, uiCursorColor, &BrushPreview::putPixelInCursorDelegate);
if (!(m_type & NATIVE_CROSSHAIR)) {
createCrosshairCursor(&g, uiCursorColor);
}
forEachBrushPixel(&g, spritePos, uiCursorColor, &BrushPreview::savePixelDelegate);
forEachBrushPixel(&g, spritePos, uiCursorColor, &BrushPreview::drawPixelDelegate);
@ -344,7 +390,7 @@ void BrushPreview::hide()
// cursor will be changed anyway after the hide() by the caller.
//
//if (m_cursor)
// m_editor->manager()->getDisplay()->setNativeMouseCursor(os::NativeCursor::kNoCursor);
// m_editor->manager()->getDisplay()->setCursor(os::NativeCursor::Hidden);
// Get drawable region
m_editor->getDrawableRegion(m_clippingRegion, ui::Widget::kCutTopWindows);
@ -443,81 +489,165 @@ void BrushPreview::generateBoundaries()
delete mask;
}
void BrushPreview::createNativeCursor()
void BrushPreview::createCrosshairCursor(ui::Graphics* g,
const gfx::Color cursorColor)
{
ASSERT(!(m_type & NATIVE_CROSSHAIR));
gfx::Rect cursorBounds;
if (m_type & CROSSHAIR) {
cursorBounds |= gfx::Rect(-3, -3, 7, 7);
m_cursorCenter = -cursorBounds.origin();
}
// Special case of a cursor for one pixel
else if (!(m_type & NATIVE_CROSSHAIR) &&
m_editor->zoom().scale() >= 4.0) {
cursorBounds = gfx::Rect(0, 0, 1, 1);
m_cursorCenter = gfx::Point(0, 0);
}
if (m_cursor) {
if (m_cursor->width() != cursorBounds.w ||
m_cursor->height() != cursorBounds.h) {
m_cursor.reset();
}
}
if (cursorBounds.isEmpty()) {
ASSERT(!m_cursor);
if (!(m_type & NATIVE_CROSSHAIR)) {
// TODO should we use ui::set_mouse_cursor()?
ui::set_mouse_cursor_reset_info();
m_editor->manager()->display()->setNativeMouseCursor(os::NativeCursor::Hidden);
}
return;
}
if (!m_cursor) {
m_cursor = os::instance()->makeRgbaSurface(cursorBounds.w, cursorBounds.h);
// Cannot clear the cursor on each iteration because it can
// generate a flicker effect when zooming in the same mouse
// position. That's strange.
m_cursor->clear();
}
}
void BrushPreview::forEachLittleCrossPixel(
ui::Graphics* g,
const gfx::Point& screenPos,
gfx::Color color,
PixelDelegate pixelDelegate)
{
if (m_type & CROSSHAIR)
traceCrossPixels(g, screenPos, color, pixelDelegate);
gfx::Point cursorCenter;
// Depending on the editor zoom, maybe we need subpixel movement (a
// little dot inside the active pixel)
if (!(m_type & NATIVE_CROSSHAIR) &&
m_editor->zoom().scale() >= 4.0) {
(this->*pixelDelegate)(g, screenPos, color);
const bool requireLittleCenterDot = (m_editor->zoom().scale() >= 4.0);
if (m_type & CROSSHAIR) {
// Regular crosshair of 7x7
cursorBounds |= gfx::Rect(-3, -3, 7, 7);
cursorCenter = -cursorBounds.origin();
}
else if (requireLittleCenterDot) {
// Special case of a cursor for one pixel
cursorBounds = gfx::Rect(0, 0, 1, 1);
cursorCenter = gfx::Point(0, 0);
}
else {
// We'll remove the pixel (as we didn't called Surface::clear() to
// avoid a flickering issue when zooming in the same mouse
// position).
base::ScopedValue<bool> restore(m_blackAndWhiteNegative, false,
m_blackAndWhiteNegative);
(this->*pixelDelegate)(g, screenPos, gfx::ColorNone);
}
if (m_cursor) {
ASSERT(m_cursor);
// TODO should we use ui::set_mouse_cursor()?
ui::set_mouse_cursor_reset_info();
m_editor->manager()->display()->setNativeMouseCursor(
m_cursor.get(),
m_cursorCenter,
m_editor->manager()->display()->scale());
m_editor->manager()->display()->setCursor(os::NativeCursor::Hidden);
return;
}
os::Window* window = m_editor->manager()->display();
const int scale = window->scale();
os::CursorRef cursor = nullptr;
// Invalidate the entire cache if the scale has changed
if (g_cacheCursorScale != scale) {
g_cacheCursorScale = scale;
g_bwCursors.fill(nullptr);
g_solidCursors.fill(nullptr);
}
// Cursor with black/white colors (we create a key/index for
// g_cachedCursors depending on the colors on the screen)
if (m_blackAndWhiteNegative) {
int k = 0;
if (m_type & CROSSHAIR) {
int bit = 0;
for (int v=0; v<7; v++) {
for (int u=0; u<7; u++) {
if (g_crosshair_pattern[v*7+u]) {
color_t c = g->getPixel(m_screenPosition.x-3+u,
m_screenPosition.y-3+v);
c = color_utils::blackandwhite_neg(c);
if (rgba_getr(c) == 255) { // White
k |= (1 << bit);
}
++bit;
}
}
}
}
if (requireLittleCenterDot) {
color_t c = g->getPixel(m_screenPosition.x,
m_screenPosition.y);
c = color_utils::blackandwhite_neg(c);
if (rgba_getr(c) == 255) { // White
k |= (m_type & CROSSHAIR ? 0x200: 0x301);
}
else { // Black
k |= (m_type & CROSSHAIR ? 0x100: 0x300);
}
}
ASSERT(k < int(g_bwCursors.size()));
if (k >= int(g_bwCursors.size())) // Unexpected key value in release mode
return;
// Use cached cursor
if (g_bwCursors[k]) {
cursor = g_bwCursors[k];
}
else {
const gfx::Color black = gfx::rgba(0, 0, 0);
const gfx::Color white = gfx::rgba(255, 255, 255);
os::SurfaceRef cursorSurface =
os::instance()->makeRgbaSurface(cursorBounds.w,
cursorBounds.h);
cursorSurface->clear();
int bit = 0;
if (m_type & CROSSHAIR) {
for (int v=0; v<7; v++) {
for (int u=0; u<7; u++) {
if (g_crosshair_pattern[v*7+u]) {
cursorSurface->putPixel(
(k & (1 << bit) ? white: black), u, v);
++bit;
}
}
}
}
if (requireLittleCenterDot) {
cursorSurface->putPixel(
(k == 0x100 || k == 0x300 ? black: white),
cursorBounds.w/2, cursorBounds.h/2);
}
cursor = g_bwCursors[k] =
os::instance()->makeCursor(
cursorSurface.get(),
cursorCenter,
scale);
}
}
// Cursor with solid color (easiest case, we don't have to check the
// colors in the screen to create the crosshair)
else {
// We have to recreate all cursors if the color has changed.
if (g_solidCursorColor != cursorColor) {
g_solidCursors.fill(nullptr);
g_solidCursorColor = cursorColor;
}
int k = 0;
if (m_type & CROSSHAIR) {
if (requireLittleCenterDot)
k = 2;
else
k = 1;
}
// Use cached cursor
if (g_solidCursors[k]) {
cursor = g_solidCursors[k];
}
else {
os::SurfaceRef cursorSurface =
os::instance()->makeRgbaSurface(cursorBounds.w,
cursorBounds.h);
cursorSurface->clear();
if (m_type & CROSSHAIR) {
for (int v=0; v<7; v++)
for (int u=0; u<7; u++)
if (g_crosshair_pattern[v*7+u])
cursorSurface->putPixel(cursorColor, u, v);
}
if (requireLittleCenterDot)
cursorSurface->putPixel(cursorColor, cursorBounds.w/2, cursorBounds.h/2);
cursor = g_solidCursors[k] =
os::instance()->makeCursor(
cursorSurface.get(),
cursorCenter,
scale);
}
}
if (cursor) {
// TODO should we use ui::set_mouse_cursor()?
ui::set_mouse_cursor_reset_info();
window->setCursor(cursor);
}
}
@ -538,34 +668,6 @@ void BrushPreview::forEachBrushPixel(
m_savedPixelsLimit = m_savedPixelsIterator;
}
void BrushPreview::traceCrossPixels(
ui::Graphics* g,
const gfx::Point& pt, gfx::Color color,
PixelDelegate pixelDelegate)
{
static int cross[7*7] = {
0, 0, 0, 1, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0,
1, 1, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0,
0, 0, 0, 1, 0, 0, 0,
};
gfx::Point out;
int u, v;
for (v=0; v<7; v++) {
for (u=0; u<7; u++) {
if (cross[v*7+u]) {
out.x = pt.x-3+u;
out.y = pt.y-3+v;
(this->*pixelDelegate)(g, out, color);
}
}
}
}
// Old thick cross (used for selection tools)
void BrushPreview::traceSelectionCrossPixels(
ui::Graphics* g,
@ -634,30 +736,6 @@ void BrushPreview::traceBrushBoundaries(ui::Graphics* g,
//////////////////////////////////////////////////////////////////////
// Pixel delegates
void BrushPreview::putPixelInCursorDelegate(ui::Graphics* g, const gfx::Point& pt, gfx::Color color)
{
ASSERT(m_cursor);
if (!m_clippingRegion.contains(pt))
return;
if (m_blackAndWhiteNegative) {
color_t c = g->getPixel(pt.x, pt.y);
int r = gfx::getr(c);
int g = gfx::getg(c);
int b = gfx::getb(c);
m_cursor->putPixel(color_utils::blackandwhite_neg(gfx::rgba(r, g, b)),
pt.x - m_screenPosition.x + m_cursorCenter.x,
pt.y - m_screenPosition.y + m_cursorCenter.y);
}
else {
m_cursor->putPixel(color,
pt.x - m_screenPosition.x + m_cursorCenter.x,
pt.y - m_screenPosition.y + m_cursorCenter.y);
}
}
void BrushPreview::savePixelDelegate(ui::Graphics* g, const gfx::Point& pt, gfx::Color color)
{
if (m_clippingRegion.contains(pt)) {

View File

@ -19,6 +19,7 @@
#include "gfx/rect.h"
#include "gfx/region.h"
#include "os/surface.h"
#include "ui/cursor.h"
#include <vector>
@ -72,6 +73,8 @@ namespace app {
NATIVE_CROSSHAIR = 8,
};
static void destroyInternals();
BrushPreview(Editor* editor);
~BrushPreview();
@ -94,23 +97,17 @@ namespace app {
void generateBoundaries();
// Creates a little native cursor to draw the CROSSHAIR
void createNativeCursor();
void forEachLittleCrossPixel(
ui::Graphics* g,
const gfx::Point& screenPos,
gfx::Color color,
PixelDelegate pixelDelegate);
void createCrosshairCursor(ui::Graphics* g, const gfx::Color cursorColor);
void forEachBrushPixel(
ui::Graphics* g,
const gfx::Point& spritePos,
gfx::Color color,
PixelDelegate pixelDelegate);
void traceCrossPixels(ui::Graphics* g, const gfx::Point& pt, gfx::Color color, PixelDelegate pixel);
void traceSelectionCrossPixels(ui::Graphics* g, const gfx::Point& pt, gfx::Color color, int thickness, PixelDelegate pixel);
void traceBrushBoundaries(ui::Graphics* g, gfx::Point pos, gfx::Color color, PixelDelegate pixel);
void putPixelInCursorDelegate(ui::Graphics* g, const gfx::Point& pt, gfx::Color color);
void savePixelDelegate(ui::Graphics* g, const gfx::Point& pt, gfx::Color color);
void drawPixelDelegate(ui::Graphics* g, const gfx::Point& pt, gfx::Color color);
void clearPixelDelegate(ui::Graphics* g, const gfx::Point& pt, gfx::Color color);
@ -129,10 +126,6 @@ namespace app {
gfx::Point m_screenPosition; // Position in the screen (view)
gfx::Point m_editorPosition; // Position in the editor (model)
// Native mouse cursor to draw crosshair
os::SurfaceRef m_cursor;
gfx::Point m_cursorCenter;
// Information about current brush
doc::MaskBoundaries m_brushBoundaries;
int m_brushGen;

View File

@ -227,6 +227,7 @@ Editor::~Editor()
void Editor::destroyEditorSharedInternals()
{
BrushPreview::destroyInternals();
if (m_renderEngine) {
delete m_renderEngine;
m_renderEngine = nullptr;

View File

@ -222,12 +222,12 @@ SkinTheme* SkinTheme::instance()
SkinTheme::SkinTheme()
: m_sheet(nullptr)
, m_standardCursors(ui::kCursorTypes, nullptr)
, m_defaultFont(nullptr)
, m_miniFont(nullptr)
, m_preferredScreenScaling(-1)
, m_preferredUIScaling(-1)
{
m_standardCursors.fill(nullptr);
}
SkinTheme::~SkinTheme()

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2020-2021 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -12,15 +12,17 @@
#include "app/ui/skin/skin_part.h"
#include "gfx/color.h"
#include "gfx/fwd.h"
#include "ui/cursor.h"
#include "ui/cursor_type.h"
#include "ui/manager.h"
#include "ui/scale.h"
#include "ui/theme.h"
#include "theme.xml.h"
#include <array>
#include <map>
#include <string>
#include <vector>
namespace ui {
class Entry;
@ -155,7 +157,7 @@ namespace app {
std::map<std::string, gfx::Color> m_colors_by_id;
std::map<std::string, int> m_dimensions_by_id;
std::map<std::string, ui::Cursor*> m_cursors;
std::vector<ui::Cursor*> m_standardCursors;
std::array<ui::Cursor*, ui::kCursorTypes> m_standardCursors;
std::map<std::string, ui::Style*> m_styles;
std::map<std::string, FontData*> m_fonts;
std::map<std::string, os::FontRef> m_themeFonts;

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2020-2021 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -13,14 +13,36 @@
#include "base/debug.h"
#include "os/surface.h"
#include "os/system.h"
namespace ui {
Cursor::Cursor(const os::SurfaceRef& surface, const gfx::Point& focus)
Cursor::Cursor(const os::SurfaceRef& surface,
const gfx::Point& focus)
: m_surface(surface)
, m_focus(focus)
, m_scale(0)
{
ASSERT(m_surface != nullptr);
}
void Cursor::reset()
{
m_surface.reset();
m_cursor.reset();
m_focus = gfx::Point(0, 0);
m_scale = 0;
}
os::CursorRef Cursor::nativeCursor(const int scale) const
{
if (m_cursor && m_scale == scale)
return m_cursor;
m_cursor = os::instance()->makeCursor(
m_surface.get(),
m_focus,
m_scale = scale);
return m_cursor;
}
} // namespace ui

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2020-2021 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -10,22 +10,27 @@
#pragma once
#include "gfx/point.h"
#include "os/cursor.h"
#include "os/surface.h"
namespace os { class Surface; }
namespace ui {
class Cursor {
public:
Cursor(const os::SurfaceRef& surface, const gfx::Point& focus);
Cursor(const os::SurfaceRef& surface = nullptr,
const gfx::Point& focus = gfx::Point(0, 0));
const os::SurfaceRef& getSurface() const { return m_surface; }
const gfx::Point& getFocus() const { return m_focus; }
const os::SurfaceRef& surface() const { return m_surface; }
const gfx::Point& focus() const { return m_focus; }
os::CursorRef nativeCursor(const int scale) const;
void reset();
private:
os::SurfaceRef m_surface;
gfx::Point m_focus;
mutable os::CursorRef m_cursor;
mutable int m_scale;
};
} // namespace ui

View File

@ -59,14 +59,14 @@ static void update_mouse_overlay(const Cursor* cursor)
if (mouse_cursor && mouse_scares == 0) {
if (!mouse_cursor_overlay) {
mouse_cursor_overlay = base::make_ref<Overlay>(
mouse_cursor->getSurface(),
mouse_cursor->surface(),
get_mouse_position(),
Overlay::MouseZOrder);
OverlayManager::instance()->addOverlay(mouse_cursor_overlay);
}
else {
mouse_cursor_overlay->setSurface(mouse_cursor->getSurface());
mouse_cursor_overlay->setSurface(mouse_cursor->surface());
update_cursor_overlay();
}
}
@ -84,19 +84,18 @@ static bool update_custom_native_cursor(const Cursor* cursor)
// Check if we can use a custom native mouse in this platform
if (support_native_custom_cursor &&
mouse_display) {
if (cursor) {
result = mouse_display->setNativeMouseCursor(
// The surface is already scaled by guiscale()
cursor->getSurface().get(),
cursor->getFocus(),
// We scale the cursor by the os::Display scale
mouse_display->scale() * mouse_cursor_scale);
if (cursor && cursor->surface()) {
// The cursor surface is already scaled by guiscale(), we scale
// the cursor by the os::Display scale and mouse scale.
const int scale = mouse_display->scale() * mouse_cursor_scale;
if (auto osCursor = cursor->nativeCursor(scale))
result = mouse_display->setCursor(osCursor);
}
else if (mouse_cursor_type == kOutsideDisplay) {
result = mouse_display->setNativeMouseCursor(os::NativeCursor::Arrow);
result = mouse_display->setCursor(os::NativeCursor::Arrow);
}
else {
result = mouse_display->setNativeMouseCursor(os::NativeCursor::Hidden);
result = mouse_display->setCursor(os::NativeCursor::Hidden);
}
}
@ -147,7 +146,7 @@ static void update_mouse_cursor()
// Set native cursor
if (mouse_display) {
bool ok = mouse_display->setNativeMouseCursor(nativeCursor);
bool ok = mouse_display->setCursor(nativeCursor);
// It looks like the specific native cursor is not supported,
// so we can should use the internal overlay (even when we
@ -192,7 +191,7 @@ UISystem::UISystem()
support_native_custom_cursor =
((os::instance() &&
(int(os::instance()->capabilities()) &
int(os::Capabilities::CustomNativeMouseCursor))) ?
int(os::Capabilities::CustomMouseCursor))) ?
true: false);
details::initWidgets();
@ -200,8 +199,6 @@ UISystem::UISystem()
UISystem::~UISystem()
{
OverlayManager::destroyInstance();
// finish theme
set_theme(nullptr, guiscale());
@ -211,6 +208,8 @@ UISystem::~UISystem()
if (!update_custom_native_cursor(nullptr))
update_mouse_overlay(nullptr);
OverlayManager::destroyInstance();
ASSERT(g_instance == this);
g_instance = nullptr;
}
@ -262,7 +261,7 @@ void update_cursor_overlay()
{
if (mouse_cursor_overlay != nullptr && mouse_scares == 0) {
gfx::Point newPos =
get_mouse_position() - mouse_cursor->getFocus();
get_mouse_position() - mouse_cursor->focus();
if (newPos != mouse_cursor_overlay->position()) {
mouse_cursor_overlay->moveOverlay(newPos);

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2020-2021 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -59,7 +59,7 @@ namespace ui {
virtual os::Font* getDefaultFont() const = 0;
virtual os::Font* getWidgetFont(const Widget* widget) const = 0;
virtual Cursor* getStandardCursor(CursorType type) = 0;
virtual ui::Cursor* getStandardCursor(CursorType type) = 0;
virtual void initWidget(Widget* widget) = 0;
virtual void getWindowMask(Widget* widget, gfx::Region& region) = 0;
virtual void setDecorativeWidgetBounds(Widget* widget);