toptensoftware / RichTextKit

Rich text rendering for SkiaSharp
Other
367 stars 73 forks source link

overdraw #54

Closed mgood7123 closed 2 years ago

mgood7123 commented 2 years ago

is it normal for RTK to do partial drawing?

this is my shader

var sksl = SKRuntimeEffect.Create(
    "uniform shader input;\n" +
    "uniform shader inputAlpha;\n" +
    "uniform shader inputGradient;\n" +
    "\n" +
    "half4 main() {\n" +
    "    half4 color = sample(input);\n" +
    // return zero if alpha is zero
    "    if (color.a == 0) return vec4(0,0,0,0);\n" +
    "    int alpha = 255.0 * sample(inputAlpha).a;\n" +
    // return color if input alpha is 0, this means we only drawn this pixel once
    // Skia's overdraw canvas increases the alpha of a pixel each time it drawn touched
    // R G B A
    "    if (alpha == 0) {\n" +
    // apply greyscale to the overdraw canvas in order to isolate the overdraw colors
    "       return half4(vec3((color.r + color.g + color.b) / 3), 1);\n" +
    "    }\n" +
    "    return half4(1,0,0,1);\n" +
    //// gradient heatmap
    //"    return sample(inputGradient, float2(0, alpha));\n" +
    "}\n",
    out string err
);

and if use this

void drawText(SKCanvas canvas, int n, int x, int y)
{
    Topten.RichTextKit.TextBlock block = new();
    Topten.RichTextKit.Style style = new();
    style.TextColor = SKColors.Silver;
    style.FontSize = 20;
    var t = new Topten.RichTextKit.TextPaintOptions();
    t.Edging = SKFontEdging.SubpixelAntialias;
    for (int i = 0; i < n; i++)
    {
        string text = "drawn " + n + " time";
        if (i != 0) text += "s";
        block.Clear();
        block.AddText(text, style);
        block.Paint(canvas, new SKPoint(x, y));
        canvas.Flush();
    }
}

then i get this

image

and if i use this

void drawText(SKCanvas canvas, int n, int x, int y)
{
    using var paint = new SKPaint();
    paint.Color = SKColors.Silver;
    for (int i = 0; i < n; i++)
    {
        string text = "drawn " + n + " time";
        if (i != 0) text += "s";
        canvas.DrawText(text, x, y, paint);
    }
}

then i get this (expected result)

image

my full code is this

SKShader createAlphaGradientShader()
{
    return SKShader.CreateLinearGradient(
        // start
        new SKPoint(0, 0),
        // end
        new SKPoint(0, 255),
        // colors
        new SKColor[]
        {
        // light blue
        //new SKColorF(0, 0.5f, 0.75f).ToSKColor(),
        // blueish-whitish
        //new SKColorF(0.37f, 0.5f, 0.75f).ToSKColor(),
        // lighter orange
        new SKColor(249, 205, 172),
        // light orange
        //new SKColor(243, 158, 95),
        // orange-redish
        //new SKColorF(1f, 0.28f, 0).ToSKColor(),
        // red
        new SKColorF(1f, 0, 0).ToSKColor(),
        },
        // distribution (color pos from 0 to 1)
        new float[] { 0, 0.05f },
        SKShaderTileMode.Clamp
    );
}

using var gradient = createAlphaGradientShader();

// to improve overdraw quality we only apply overdraw to non transparent final output pixels
// this means we need to draw twice, once in full color, another in alpha
// if the full color pixel has an alpha of zero we discard the result
var sksl = SKRuntimeEffect.Create(
    "uniform shader input;\n" +
    "uniform shader inputAlpha;\n" +
    "uniform shader inputGradient;\n" +
    "\n" +
    "half4 main() {\n" +
    "    half4 color = sample(input);\n" +
    // return zero if alpha is zero
    "    if (color.a == 0) return vec4(0,0,0,0);\n" +
    "    int alpha = 255.0 * sample(inputAlpha).a;\n" +
    // return color if input alpha is 0, this means we only drawn this pixel once
    // Skia's overdraw canvas increases the alpha of a pixel each time it drawn touched
    // R G B A
    "    if (alpha == 0) {\n" +
    // apply greyscale to the overdraw canvas in order to isolate the overdraw colors
    "       return half4(vec3((color.r + color.g + color.b) / 3), 1);\n" +
    "    }\n" +
    "    return half4(1,0,0,1);\n" +
    //// gradient heatmap
    //"    return sample(inputGradient, float2(0, alpha));\n" +
    "}\n",
    out string err
);

if (err != null)
{
    Log.d("SHADER", "runtime effect compiled with errors: " + err);
    return;
}

int w = canvas.BaseLayerSize.Width;
int h = canvas.BaseLayerSize.Height;
SKImageInfo offscreenInfo = new(w, h);
SKImageInfo offscreenAlphaInfo = new(w, h, SKColorType.Alpha8);
using var offscreenSurface = SKSurface.Create(offscreenInfo);
using var offscreenAlphaSurface = SKSurface.Create(offscreenAlphaInfo);
using SKCanvas imageCanvas = offscreenSurface.Canvas;
using SKOverdrawCanvas overdrawCanvas = new(offscreenAlphaSurface.Canvas);
using SKNWayCanvas nWayCanvas = new(w, h);
nWayCanvas.AddCanvas(overdrawCanvas);
nWayCanvas.AddCanvas(imageCanvas);

using SKPaint colorPaint = new();

void drawText_(SKCanvas canvas, int n, int x, int y)
{
    Topten.RichTextKit.TextBlock block = new();
    Topten.RichTextKit.Style style = new();
    style.TextColor = SKColors.Silver;
    style.FontSize = 20;
    var t = new Topten.RichTextKit.TextPaintOptions();
    t.Edging = SKFontEdging.SubpixelAntialias;
    for (int i = 0; i < n; i++)
    {
        string text = "drawn " + n + " time";
        if (i != 0) text += "s";
        block.Clear();
        block.AddText(text, style);
        block.Paint(canvas, new SKPoint(x, y));
        canvas.Flush();
    }
}

void drawText(SKCanvas canvas, int n, int x, int y)
{
    using var paint = new SKPaint();
    paint.Color = SKColors.Silver;
    for (int i = 0; i < n; i++)
    {
        string text = "drawn " + n + " time";
        if (i != 0) text += "s";
        canvas.DrawText(text, x, y, paint);
    }
}

void drawMatrix(SKCanvas canvas, int count, int max_lines, int spacing)
{
    max_lines++;
    int n = 0;
    int column = 0;
    int line = 1;
    for (int i = 0; i < count; i++)
    {
        n = i + 1;
        if (line == max_lines)
        {
            line = 1;
            column += spacing;
        }
        //int s = canvas.Save();
        drawText(canvas, n, column, 20 * line);
        //canvas.RestoreToCount(s);
        line++;
    }
}

drawMatrix(nWayCanvas, 20, 20, 50);

nWayCanvas.Flush();

using var imageAlpha = offscreenAlphaSurface.Snapshot();
using var image = offscreenSurface.Snapshot();
var imageAlphaShader = imageAlpha.ToShader();
var imageShader = image.ToShader();

SKRuntimeEffectChildren children = new(sksl) {
    { "input", imageShader },
    { "inputAlpha", imageAlphaShader },
    { "inputGradient", gradient },
};

var ourShader = sksl.ToShader(false, new(sksl), children);

sksl.Dispose();
imageAlphaShader.Dispose();
imageShader.Dispose();

// we only want to write our paint shader's output pixel to the canvas
// this is the same as if the canvas was cleared before painting the shader
colorPaint.BlendMode = SKBlendMode.Src;
colorPaint.Shader = ourShader;
canvas.DrawPaint(colorPaint);
ourShader.Dispose();
toptensoftware commented 2 years ago

I can't think of any reason why this would be happening. Normally each call to TextBlock.Paint should paint each run of text just once.

Out of curiosity and as an experiment, what happens if you create a new TextBlock for each paint call (instead of .Clear() and re-using the previous one).

Failing you might need to debug through it to figure out what's going on. Happy to make fixes if you can point out the issue.

mgood7123 commented 2 years ago

I can't think of any reason why this would be happening. Normally each call to TextBlock.Paint should paint each run of text just once.

Out of curiosity and as an experiment, what happens if you create a new TextBlock for each paint call (instead of .Clear() and re-using the previous one).

Failing you might need to debug through it to figure out what's going on. Happy to make fixes if you can point out the issue.

the intent is to get RTK to draw the same block of text multiple times the exact same way each time

eg if it draws once, then all pixels it draws are drawn once

if it draws twice then all pixels that where drawn once, should be drawn twice

if you create a new TextBlock for each paint call

i get the same result

toptensoftware commented 2 years ago

Thinking about this more... the only thing I can think of is if the actual text rasterization is reading back from the target surface and not updating some pixels if not required. And the only case I can think of when this happens is with SubpixelAntialiasing.

What happens if you remove this:

 t.Edging = SKFontEdging.SubpixelAntialias;

and/or try with other values.

mgood7123 commented 2 years ago

i tried Alias, AntiAlias and SubpixelAntialiasing and i get the same results for all

var t = new Topten.RichTextKit.TextPaintOptions();
t.Edging = SKFontEdging.Alias;
block.Paint(canvas, new SKPoint(x, y), t);

image

var t = new Topten.RichTextKit.TextPaintOptions();
t.Edging = SKFontEdging.Antialias;
block.Paint(canvas, new SKPoint(x, y), t);

image

var t = new Topten.RichTextKit.TextPaintOptions();
t.Edging = SKFontEdging.SubpixelAntialias;
block.Paint(canvas, new SKPoint(x, y), t);

image

mgood7123 commented 2 years ago

if i use this

void drawText(SKCanvas canvas, int n, int x, int y)
{
    SKTypeface t = SKTypeface.FromFamilyName("Arial");
    SKFont f = t.ToFont();

    using var paint = new SKPaint(f);
    paint.Color = SKColors.Silver;
    paint.TextSize = 20;

    for (int i = 0; i < n; i++)
    {
        string text = "drawn " + n + " time";
        if (i != 0) text += "s";
        canvas.DrawText(text, x, y, paint);
    }
}

i get this which is what i expect

image

mgood7123 commented 2 years ago

if i use this

void drawText(SKCanvas canvas, int n, int x, int y)
{
    for (int i = 0; i < n; i++)
    {
        string text = "drawn " + n + " time";
        if (i != 0) text += "s";
        Topten.RichTextKit.TextBlock block = new();
        Topten.RichTextKit.Style style = new();
        style.TextColor = SKColors.Silver;
        style.FontFamily = "Arial";
        style.FontSize = 20;
        block.AddText(text, style);
        block.Paint(canvas, new SKPoint(x, y));
    }
}

i get this

image

mgood7123 commented 2 years ago

Failing you might need to debug through it to figure out what's going on. Happy to make fixes if you can point out the issue.

i would have no idea where to start debugging text rendering issues

toptensoftware commented 2 years ago

i would have no idea where to start debugging text rendering issues

If you step into the TextBlock.Paint call you'll see it just draws a bunch of font runs. You just need to check the SKPaint options in there match what you're using in the case where it works.

Failing that, if you can send me a complete, ready to run (on Windows) VS project (preferably as a git repo) I'll take a look when I get a chance.

mgood7123 commented 2 years ago

Skia TestBed.zip

mgood7123 commented 2 years ago

https://github.com/mgood7123/SKRichTextKit-Bug-Incorrect-Overdraw

toptensoftware commented 2 years ago

I think the problem is external to RichTextKit and related to using geometry transforms.

The same problem can be demonstrated by changing your non-RichTextKit drawText function so that it uses Translate to position the text (similarly to what RichTextKit does):

                   void drawText(SKCanvas canvas, int n, int x, int y)
                    {
                        SKTypeface t = SKTypeface.FromFamilyName("Arial");
                        SKFont f = t.ToFont();

                        using var paint = new SKPaint(f);
                        paint.Color = SKColors.Silver;
                        paint.TextSize = 20;

                        for (int i = 0; i < n; i++)
                        {
                            string text = "drawn " + n + " time";
                            if (i != 0) text += "s";
                            canvas.Save();
                            canvas.Translate(new SKPoint(x, y));
                            canvas.DrawText(text, 0, 0, paint);
                            canvas.Restore();
                        }
                    }
mgood7123 commented 2 years ago

hmmm

toptensoftware commented 2 years ago

Nice. Will you report this to Skia devs? If so, please post a link to issue here.

mgood7123 commented 2 years ago

successfully replicated on skia fiddle

https://fiddle.skia.org/c/6ad1c5508fa12c0afc63f4a7e80c6c75

https://groups.google.com/g/skia-discuss/c/q91GYonjd98

mgood7123 commented 2 years ago

could be a problem with how the overdraw canvas is computing alpha increments

mgood7123 commented 2 years ago

if i manually increment the alpha i get a slight improvement but still not 100% geometry replicated drawing

image

Topten.RichTextKit.TextBlock block = new();
Topten.RichTextKit.Style style = new();
block.AddText(text, null);
style.TextColor = new SKColor(0, 0, 0, (byte)(i + 1));
style.FontFamily = "Arial";
style.FontSize = 20;
block.ApplyStyle(0, text.Length, style);

block.Paint(offscreenAlphaSurface.Canvas, new SKPoint(x, y));

style.TextColor = SKColors.Silver;
block.ApplyStyle(0, text.Length, style);
block.Paint(offscreenSurface.Canvas, new SKPoint(x, y));
mgood7123 commented 2 years ago

Not sure if this issue should be kept open or not

mgood7123 commented 2 years ago

if use a manual overdraw

    public class SKOverdrawCanvas4 : SKCanvasForwarder
    {
        SKCanvas baseCanvas;

        public SKOverdrawCanvas4(SKCanvas canvas)
        {
            baseCanvas = canvas;
            SetNativeObject(canvas);
        }

        public override void DrawRect(SKRect rect, SKPaint paint)
        {
            using SKPaint p = new();
            p.Color = new(0, 0, 0, 1);
            p.BlendMode = SKBlendMode.Plus;
            base.DrawRect(rect, p);
        }

        public override void DrawRect(float x, float y, float w, float h, SKPaint paint)
        {
            using SKPaint p = new();
            p.Color = new(0, 0, 0, 1);
            p.BlendMode = SKBlendMode.Plus;
            base.DrawRect(x, y, w, h, p);
        }

        public override void DrawText(SKTextBlob text, float x, float y, SKPaint paint)
        {
            using SKPaint p = new();
            p.Color = new(0, 0, 0, 1);
            p.BlendMode = SKBlendMode.Plus;
            baseCanvas.DrawText(text, x, y, p);
        }
    }

then i get this

image image

Topten.RichTextKit.TextBlock block = new();
Topten.RichTextKit.Style style = new();
style.TextColor = new SKColor(0, 255, 0, 255);
style.FontFamily = "Arial";
style.FontSize = textSize + n;
block.AddText(text, style);
block.Paint(canvas, new SKPoint(x, prevY));

and for normal text i get this

image image

SKTypeface t = SKTypeface.FromFamilyName("Arial");
SKFont f = t.ToFont();

using var paint = new SKPaint(f);
paint.Color = new SKColor(0, 255, 0, 255);
paint.TextSize = textSize + n;
canvas.Save();
canvas.Translate(new SKPoint(x, paint.TextSize + prevY));
canvas.DrawText(text, 0, 0, paint);
canvas.Restore();
mgood7123 commented 2 years ago

it is substantially better tho not quite perfect

mgood7123 commented 2 years ago

if we convert the translate + drawText into a single drawText we then get this

image

mgood7123 commented 2 years ago

also if we use a CPU surface (with translate + draw) instead of a GPU surface we get a much higher quality (at the cost of performance)

image

mgood7123 commented 2 years ago

if we convert the translate + drawText into a single drawText we then get this

image

could you modify RIchTexkKit to use this?

toptensoftware commented 2 years ago

Given this is reproducible without invoking RichTextKit, this should be logged as an issue against SkiaSharp (or perhaps Skia). This is not a RichTextKit bug.

mgood7123 commented 2 years ago

could you add a toggle to work around this until it gets fixed in skia?

toptensoftware commented 2 years ago

That would be a non-trivial change for what appears to be a very edge case issue. If you really need this, best bet would be to fork the repo and make the change yourself (until fixed in skia).

mgood7123 commented 2 years ago

alright