Use shaders for ColorTintShadeTone/ColorSpectrum selectors (#960)

This is the first step to optimize the painting code of ColorSelectors
which were using a background thread to paint their
surface. ColorWheel is still using the old method (without shaders).

This impl is already a lot faster on CPU, and it's ready for
GPU-acceleration in a future.
This commit is contained in:
David Capello 2022-05-17 13:24:53 -03:00
parent 4b99d3022a
commit aca96c6d36
10 changed files with 373 additions and 31 deletions

2
laf

@ -1 +1 @@
Subproject commit 34c67cd6f2be9dbb14f559b7fb16160318b07086
Subproject commit 8b0422877a39d655bf6a405eb2e2985763b150da

View File

@ -40,22 +40,6 @@ using namespace gfx;
namespace {
gfx::Color gridColor1()
{
if (ui::is_ui_thread() && current_editor)
return color_utils::color_for_ui(current_editor->docPref().bg.color1());
else
return gfx::rgba(128, 128, 128);
}
gfx::Color gridColor2()
{
if (ui::is_ui_thread() && current_editor)
return color_utils::color_for_ui(current_editor->docPref().bg.color2());
else
return gfx::rgba(192, 192, 192);
}
void draw_checked_grid(ui::Graphics* g,
const gfx::Rect& rc,
const gfx::Size& tile,
@ -90,11 +74,27 @@ void draw_checked_grid(ui::Graphics* g,
} // anonymous namespace
gfx::Color grid_color1()
{
if (ui::is_ui_thread() && current_editor)
return color_utils::color_for_ui(current_editor->docPref().bg.color1());
else
return gfx::rgba(128, 128, 128);
}
gfx::Color grid_color2()
{
if (ui::is_ui_thread() && current_editor)
return color_utils::color_for_ui(current_editor->docPref().bg.color2());
else
return gfx::rgba(192, 192, 192);
}
void draw_checked_grid(ui::Graphics* g,
const gfx::Rect& rc,
const gfx::Size& tile)
{
draw_checked_grid(g, rc, tile, gridColor1(), gridColor2());
draw_checked_grid(g, rc, tile, grid_color1(), grid_color2());
}
void draw_checked_grid(ui::Graphics* g,
@ -102,7 +102,7 @@ void draw_checked_grid(ui::Graphics* g,
const gfx::Size& tile,
DocumentPreferences& docPref)
{
draw_checked_grid(g, rc, tile, gridColor1(), gridColor2());
draw_checked_grid(g, rc, tile, grid_color1(), grid_color2());
}
void draw_color(ui::Graphics* g,
@ -208,8 +208,8 @@ void draw_alpha_slider(ui::Graphics* g,
for (int x=0; x<rc.w; ++x) {
const int a = (255 * x / xmax);
const doc::color_t c1 = doc::rgba_blender_normal(gridColor1(), c, a);
const doc::color_t c2 = doc::rgba_blender_normal(gridColor2(), c, a);
const doc::color_t c1 = doc::rgba_blender_normal(grid_color1(), c, a);
const doc::color_t c2 = doc::rgba_blender_normal(grid_color2(), c, a);
const int mid = rc.h/2;
const int odd = (x / rc.h) & 1;
g->drawVLine(
@ -236,8 +236,8 @@ void draw_alpha_slider(os::Surface* s,
os::Paint paint;
for (int x=0; x<rc.w; ++x) {
const int a = (255 * x / xmax);
const doc::color_t c1 = doc::rgba_blender_normal(gridColor1(), c, a);
const doc::color_t c2 = doc::rgba_blender_normal(gridColor2(), c, a);
const doc::color_t c1 = doc::rgba_blender_normal(grid_color1(), c, a);
const doc::color_t c2 = doc::rgba_blender_normal(grid_color2(), c, a);
const int mid = rc.h/2;
const int odd = (x / rc.h) & 1;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -27,6 +27,9 @@ namespace ui {
namespace app {
gfx::Color grid_color1();
gfx::Color grid_color2();
void draw_checked_grid(ui::Graphics* g,
const gfx::Rect& rc,
const gfx::Size& tile);

View File

@ -19,6 +19,7 @@
#include "app/modules/gfx.h"
#include "app/ui/skin/skin_theme.h"
#include "app/ui/status_bar.h"
#include "app/util/shader_helpers.h"
#include "base/clamp.h"
#include "base/concurrent_queue.h"
#include "base/scoped_value.h"
@ -36,8 +37,16 @@
#include <algorithm>
#include <cmath>
#include <condition_variable>
#include <cstdio>
#include <thread>
#if SK_ENABLE_SKSL
#include "os/skia/skia_surface.h"
#include "include/core/SkCanvas.h"
#include "include/effects/SkRuntimeEffect.h"
#endif
namespace app {
using namespace app::skin;
@ -224,6 +233,11 @@ private:
static ColorSelector::Painter painter;
#if SK_ENABLE_SKSL
// static
sk_sp<SkRuntimeEffect> ColorSelector::m_alphaEffect;
#endif
ColorSelector::ColorSelector()
: Widget(kGenericWidget)
, m_paintFlags(AllAreasFlag)
@ -421,12 +435,88 @@ void ColorSelector::onPaint(ui::PaintEvent& ev)
if (rc.isEmpty())
return;
g->drawSurface(
painter.getCanvas(rc.w, rc.h, theme->colors.workspace()),
rc.x, rc.y);
gfx::Rect bottomBarBounds = this->bottomBarBounds();
gfx::Rect alphaBarBounds = this->alphaBarBounds();
os::Surface* painterSurface = nullptr;
#if SK_ENABLE_SKSL // Paint with shaders
buildEffects();
if (m_mainEffect && m_bottomEffect && m_alphaEffect) {
SkCanvas* canvas;
bool isSRGB;
// TODO compare both color spaces
if (get_current_color_space()->isSRGB() &&
g->getInternalSurface()->colorSpace()->isSRGB()) {
// We can render directly in the ui::Graphics surface
canvas = &static_cast<os::SkiaSurface*>(g->getInternalSurface())->canvas();
isSRGB = true;
}
else {
// We'll paint in the ColorSelector::Painter canvas, and so we
// can convert color spaces.
painterSurface = painter.getCanvas(rc.w, rc.h, theme->colors.workspace());
canvas = &static_cast<os::SkiaSurface*>(painterSurface)->canvas();
isSRGB = false;
}
canvas->save();
{
SkPaint p;
p.setStyle(SkPaint::kFill_Style);
// Main area
gfx::Rect rc2(0, 0, rc.w, std::max(1, rc.h-bottomBarBounds.h-alphaBarBounds.h));
SkRuntimeShaderBuilder builder1(m_mainEffect);
builder1.uniform("iRes") = SkV3{float(rc2.w), float(rc2.h), 0.0f};
builder1.uniform("iColor") = appColor_to_SkV4(m_color);
p.setShader(builder1.makeShader());
if (isSRGB)
canvas->translate(rc.x+g->getInternalDeltaX(),
rc.y+g->getInternalDeltaY());
canvas->drawRect(SkRect::MakeXYWH(0, 0, rc2.w, rc2.h), p);
// Bottom bar
canvas->translate(0.0, rc2.h);
rc2.h = bottomBarBounds.h;
SkRuntimeShaderBuilder builder2(m_bottomEffect);
builder2.uniform("iRes") = SkV3{float(rc2.w), float(rc2.h), 0.0f};
builder2.uniform("iColor") = appColor_to_SkV4(m_color);
p.setShader(builder2.makeShader());
canvas->drawRect(SkRect::MakeXYWH(0, 0, rc2.w, rc2.h), p);
// Alpha bar
canvas->translate(0.0, rc2.h);
rc2.h = alphaBarBounds.h;
SkRuntimeShaderBuilder builder3(m_alphaEffect);
builder3.uniform("iRes") = SkV3{float(rc2.w), float(rc2.h), 0.0f};
builder3.uniform("iColor") = appColor_to_SkV4(m_color);
builder3.uniform("iBg1") = gfxColor_to_SkV4(grid_color1());
builder3.uniform("iBg2") = gfxColor_to_SkV4(grid_color2());
p.setShader(builder3.makeShader());
canvas->drawRect(SkRect::MakeXYWH(0, 0, rc2.w, rc2.h), p);
}
canvas->restore();
// We already painted all areas
m_paintFlags = 0;
}
else
#endif // SK_ENABLE_SKSL
{
painterSurface = painter.getCanvas(rc.w, rc.h, theme->colors.workspace());
}
if (painterSurface)
g->drawSurface(painterSurface, rc.x, rc.y);
rc.h -= bottomBarBounds.h + alphaBarBounds.h;
onPaintMainArea(g, rc);
@ -537,4 +627,56 @@ void ColorSelector::updateColorSpace()
invalidate();
}
#if SK_ENABLE_SKSL
// static
const char* ColorSelector::getAlphaBarShader()
{
return R"(
uniform half3 iRes;
uniform half4 iColor;
uniform half4 iBg1;
uniform half4 iBg2;
half4 main(vec2 fragcoord) {
vec2 d = (fragcoord.xy / iRes.xy);
half4 p = (mod((fragcoord.x / iRes.y) + floor(d.y+0.5), 2.0) > 1.0) ? iBg2: iBg1;
half4 q = iColor.rgb1;
float a = d.x;
return (1.0-a)*p + a*q;
}
)";
}
void ColorSelector::buildEffects()
{
if (!m_mainEffect) {
if (const char* code = getMainAreaShader())
m_mainEffect = buildEffect(code);
}
if (!m_bottomEffect) {
if (const char* code = getBottomBarShader())
m_bottomEffect = buildEffect(code);
}
if (!m_alphaEffect) {
if (const char* code = getAlphaBarShader())
m_alphaEffect = buildEffect(code);
}
}
sk_sp<SkRuntimeEffect> ColorSelector::buildEffect(const char* code)
{
auto result = SkRuntimeEffect::MakeForShader(SkString(code));
if (!result.errorText.isEmpty()) {
LOG(ERROR, "Shader error: %s\n", result.errorText.c_str());
std::printf("Shader error: %s\n", result.errorText.c_str());
return nullptr;
}
else {
return result.effect;
}
}
#endif
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2021 Igara Studio S.A.
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2016-2018 David Capello
//
// This program is distributed under the terms of
@ -21,6 +21,13 @@
#include <atomic>
#include <cmath>
// TODO We should wrap the SkRuntimeEffect in laf-os, SkRuntimeEffect
// and SkRuntimeShaderBuilder might change in future Skia
// versions.
#if SK_ENABLE_SKSL
#include "include/effects/SkRuntimeEffect.h"
#endif
// TODO move this to laf::base
inline bool cs_double_diff(double a, double b) {
return std::fabs((a)-(b)) > 0.001;
@ -61,6 +68,8 @@ namespace app {
void onResize(ui::ResizeEvent& ev) override;
void onPaint(ui::PaintEvent& ev) override;
virtual const char* getMainAreaShader() { return nullptr; }
virtual const char* getBottomBarShader() { return nullptr; }
virtual app::Color getMainAreaColor(const int u, const int umax,
const int v, const int vmax) = 0;
virtual app::Color getBottomBarColor(const int u, const int umax) = 0;
@ -101,6 +110,12 @@ namespace app {
void updateColorSpace();
#if SK_ENABLE_SKSL
static const char* getAlphaBarShader();
void buildEffects();
sk_sp<SkRuntimeEffect> buildEffect(const char* code);
#endif
// Internal flag used to lock the modification of m_color.
// E.g. When the user picks a color harmony, we don't want to
// change the main color.
@ -117,6 +132,13 @@ namespace app {
ui::Timer m_timer;
obs::scoped_connection m_appConn;
#if SK_ENABLE_SKSL
// Shaders
sk_sp<SkRuntimeEffect> m_mainEffect;
sk_sp<SkRuntimeEffect> m_bottomEffect;
static sk_sp<SkRuntimeEffect> m_alphaEffect;
#endif
};
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2020-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -14,6 +14,7 @@
#include "app/color_utils.h"
#include "app/ui/skin/skin_theme.h"
#include "app/ui/status_bar.h"
#include "app/util/shader_helpers.h"
#include "base/clamp.h"
#include "os/surface.h"
#include "ui/graphics.h"
@ -35,6 +36,52 @@ ColorSpectrum::ColorSpectrum()
{
}
const char* ColorSpectrum::getMainAreaShader()
{
#if SK_ENABLE_SKSL
if (m_mainShader.empty()) {
m_mainShader += "uniform half3 iRes;"
"uniform half4 iColor;";
m_mainShader += kRGB_to_HSL_sksl;
m_mainShader += kHSL_to_RGB_sksl;
m_mainShader += R"(
half4 main(vec2 fragcoord) {
vec2 d = fragcoord.xy / iRes.xy;
half hue = d.x;
half sat = rgb_to_hsl(iColor.rgb).y;
half lit = 1.0 - d.y;
return hsl_to_rgb(half3(hue, sat, lit)).rgb1;
}
)";
}
return m_mainShader.c_str();
#else
return nullptr;
#endif
}
const char* ColorSpectrum::getBottomBarShader()
{
#if SK_ENABLE_SKSL
if (m_bottomShader.empty()) {
m_bottomShader += "uniform half3 iRes;"
"uniform half4 iColor;";
m_bottomShader += kRGB_to_HSL_sksl;
m_bottomShader += kHSL_to_RGB_sksl;
m_bottomShader += R"(
half4 main(vec2 fragcoord) {
half s = (fragcoord.x / iRes.x);
half3 hsl = rgb_to_hsl(iColor.rgb);
return hsl_to_rgb(half3(hsl.x, s, hsl.z)).rgb1;
}
)";
}
return m_bottomShader.c_str();
#else
return nullptr;
#endif
}
app::Color ColorSpectrum::getMainAreaColor(const int u, const int umax,
const int v, const int vmax)
{

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -17,6 +18,8 @@ namespace app {
ColorSpectrum();
protected:
const char* getMainAreaShader() override;
const char* getBottomBarShader() override;
app::Color getMainAreaColor(const int u, const int umax,
const int v, const int vmax) override;
app::Color getBottomBarColor(const int u, const int umax) override;
@ -28,6 +31,10 @@ namespace app {
const gfx::Rect& alpha,
bool& stop) override;
int onNeedsSurfaceRepaint(const app::Color& newColor) override;
private:
std::string m_mainShader;
std::string m_bottomShader;
};
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2020 Igara Studio S.A.
// Copyright (C) 2020-2022 Igara Studio S.A.
// Copyright (C) 2016-2018 David Capello
//
// This program is distributed under the terms of
@ -13,6 +13,7 @@
#include "app/color_utils.h"
#include "app/ui/skin/skin_theme.h"
#include "app/util/shader_helpers.h"
#include "base/clamp.h"
#include "ui/graphics.h"
@ -28,6 +29,52 @@ ColorTintShadeTone::ColorTintShadeTone()
{
}
const char* ColorTintShadeTone::getMainAreaShader()
{
#if SK_ENABLE_SKSL
if (m_mainShader.empty()) {
m_mainShader += "uniform half3 iRes;"
"uniform half4 iColor;";
m_mainShader += kRGB_to_HSV_sksl;
m_mainShader += kHSV_to_RGB_sksl;
m_mainShader += R"(
half4 main(vec2 fragcoord) {
vec2 d = fragcoord.xy / iRes.xy;
half hue = rgb_to_hsv(iColor.rgb).x;
half sat = d.x;
half val = 1.0 - d.y;
return hsv_to_rgb(vec3(hue, sat, val)).rgb1;
}
)";
}
return m_mainShader.c_str();
#else
return nullptr;
#endif
}
const char* ColorTintShadeTone::getBottomBarShader()
{
#if SK_ENABLE_SKSL
if (m_bottomShader.empty()) {
m_bottomShader += "uniform half3 iRes;"
"uniform half4 iColor;";
m_bottomShader += kRGB_to_HSV_sksl;
m_bottomShader += kHSV_to_RGB_sksl;
m_bottomShader += R"(
half4 main(vec2 fragcoord) {
half h = (fragcoord.x / iRes.x);
half3 hsv = rgb_to_hsv(iColor.rgb);
return hsv_to_rgb(half3(h, hsv.y, hsv.z)).rgb1;
}
)";
}
return m_bottomShader.c_str();
#else
return nullptr;
#endif
}
app::Color ColorTintShadeTone::getMainAreaColor(const int u, const int umax,
const int v, const int vmax)
{

View File

@ -1,4 +1,5 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2016-2018 David Capello
//
// This program is distributed under the terms of
@ -18,6 +19,8 @@ namespace app {
ColorTintShadeTone();
protected:
const char* getMainAreaShader() override;
const char* getBottomBarShader() override;
app::Color getMainAreaColor(const int u, const int umax,
const int v, const int vmax) override;
app::Color getBottomBarColor(const int u, const int umax) override;
@ -29,6 +32,10 @@ namespace app {
const gfx::Rect& alpha,
bool& stop) override;
int onNeedsSurfaceRepaint(const app::Color& newColor) override;
private:
std::string m_mainShader;
std::string m_bottomShader;
};
} // namespace app

View File

@ -0,0 +1,67 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_UTIL_SHADER_HELPERS_H_INCLUDED
#define APP_UTIL_SHADER_HELPERS_H_INCLUDED
#pragma once
#if SK_ENABLE_SKSL
#include "app/color.h"
#include "gfx/color.h"
#include "include/core/SkM44.h"
// To include kRGB_to_HSL_sksl and kHSL_to_RGB_sksl
#include "src/core/SkRuntimeEffectPriv.h"
namespace app {
// rgb_to_hsl() and hsv_to_hsl() functions by Sam Hocevar licensed
// under WTFPL (https://en.wikipedia.org/wiki/WTFPL)
// Source:
// http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl
// https://stackoverflow.com/a/17897228/408239
inline constexpr char kRGB_to_HSV_sksl[] = R"(
half3 rgb_to_hsv(half3 c) {
half4 K = half4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
half4 p = mix(half4(c.bg, K.wz), half4(c.gb, K.xy), step(c.b, c.g));
half4 q = mix(half4(p.xyw, c.r), half4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return half3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
)";
inline constexpr char kHSV_to_RGB_sksl[] = R"(
half3 hsv_to_rgb(half3 c) {
half4 K = half4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
half3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
)";
inline SkV4 gfxColor_to_SkV4(gfx::Color color) {
return SkV4{float(gfx::getr(color) / 255.0),
float(gfx::getg(color) / 255.0),
float(gfx::getb(color) / 255.0),
float(gfx::geta(color) / 255.0)};
}
inline SkV4 appColor_to_SkV4(const app::Color& color) {
return SkV4{float(color.getRed() / 255.0),
float(color.getGreen() / 255.0),
float(color.getBlue() / 255.0),
float(color.getAlpha() / 255.0)};
}
} // namespace app
#endif
#endif