Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Add SKSLEffect. #17981

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft

Draft: Add SKSLEffect. #17981

wants to merge 2 commits into from

Conversation

kkwpsv
Copy link
Contributor

@kkwpsv kkwpsv commented Jan 16, 2025

What does the pull request do?

Add fragment shader effect support using SKSL.

What is the current behavior?

Not support now.

What is the updated/expected behavior with this PR?

Fragment shader effects and animations work as expected.

How was the solution implemented (if it's not obvious)?

Create an SKImageFilter using SKImageFilter.CreateRuntimeShader, and works like other effect.

Checklist

Breaking changes

A SkiaSharp change is required, which is base on SkiaSharp3.

Obsoletions / Deprecations

None.

Fixed issues

#17980

@kkwpsv
Copy link
Contributor Author

kkwpsv commented Jan 16, 2025

The required SkiaSharp nuget package is here. Currently only Windows version is available.


namespace Avalonia.Media
{
public interface IShaderEffect : IEffect
Copy link
Member

@kekekeks kekekeks Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom effects need to report their expected padding, i. e. the area outside of the affected drawing bounds they will alter (e. g. drop-shadow(5px 5px) would add (0,0,5,5) padding for renderer invalidation).

Note that this can depend on uniform values.

Copy link
Contributor Author

@kkwpsv kkwpsv Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think shader effect shouldn't has padding?
The coord is sksl input, and the color is output. Coord cannot be changed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SKImageFilter can have padding, that's how our blur and drop shadow effects work. They report padding to the compositor and it extends the intermediate texture bounds.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, right now the intermediate texture size always matches the render target size. That's something we've planned to change but never actually implemented.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SKImageFilter can have padding, that's how our blur and drop shadow effects work. They report padding to the compositor and it extends the intermediate texture bounds.

But runtimeshader seems to be not?
I'll read skia source code to confirm it.

Copy link
Member

@kekekeks kekekeks Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "padding" is just shader sampling the source at a different coordinate than it's currently producing output for. E. g. a simple implementation of drop-shadow(5,5) shader would just sample src at (-5,-5) from coord and use float4(1-alpha, 1-alpha, 1-alpha, alpha) for output.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "padding" is just shader sampling the source at a different coordinate than it's currently producing output for. E. g. a simple implementation of drop-shadow(5,5) shader would just sample src at (-5,-5) from coord and use float4(1-alpha, 1-alpha, 1-alpha, alpha) for output.

I have read skia source and usage of GetEffectOutputPadding.

GetEffectOutputPadding affect dirty rect and bounds in Avalonia compositor.
The drop shadow implementation is here.
It has blur and MatrixTransform. And they can affect rendering bounds. So we need padding for compositor.
The implementation seems to be not same as you said?

And i think runtimeshader cannot affect. It's just a fragment shader and a subset of SKimageFilter.
Or can you give me a runtimeshader example where padding is needed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In WPF it's a normal thing for ShaderEffect to extend the affected Visual's render bounds. We should do the same. If a custom SKImageFilter somehow doesn't support that, we need to manually manage the intermediate texture and use SKShader instead of SKImageFilter.

@@ -4,6 +4,7 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
<AvsIncludeSkiaSharp3>true</AvsIncludeSkiaSharp3>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avalonia 11.x will stay compatible with SkiaSharp 2.88, so such change cant' be merged. We need to have code paths for both 2.88 and 3.x.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose some of user-facing version-specific APIs can live in separate Avalonia.Skia.VersionSpecific.2.88 and Avalonia.Skia.VersionSpecific.3.0 packages.
@maxkatz6

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How feasible would it be to avoid SkiaSharp types in the new public API? This way it would be easier for us to maintain and abstract different versions internally.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maxkatz6 This pr is add the sksl effect, which means that it depends on sksl. Or do you want to design a new shader language, alsl (avalonia shader language) ?

Copy link
Member

@maxkatz6 maxkatz6 Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. I mean SKIA shaders, but not SkiaSharp API (not public API at least, internally we can bridge different versions/implementations of SkiaSharp).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How feasible would it be to avoid SkiaSharp types in the new public API? This way it would be easier for us to maintain and abstract different versions internally.

Only SkiaSharp with my pr can works now.
This requires RuntimeShader, and SkiaSharp hasn't exposed this api yet.
And i think it's hard to avoid SkiaSharp types. The SKImageFilter can be many types of things, like images, shaders which is not exists in avalonia.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically we don't need SKRuntimeFilter and can work with SKShader. The way it would work is by manually creating an intermediate texture and SKCanvas for subsequent drawing calls and using it as an input for SKShader.

That way it can be compatible with 2.88.
So the API could accept SKSL source + Avalonia.Media.Bitmap for inputs.

ShaderBuilder = builder;
}

public void RegisterUniform(string name, AvaloniaProperty<int> property) => _uniformProperties.Add(name, property);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How uniform updates are supposed to be propagated to the composition thread?

I think we need to make effects to use the same infrastructure as brushes do (ICompositionRenderResource/ICompositorSerializable)

Copy link
Contributor Author

@kkwpsv kkwpsv Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be stored in the copided SkRuntimeShaderBuilder in ToImuatable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would invalidation work after the initial layout/render pass?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess AffectsRender<T> is still public in Effect.cs. It's something we are planning to remove though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm using AffectsRender currently.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When calling RegisterUniform, we can call AffectsRender internally?

Copy link
Member

@kekekeks kekekeks Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would still prefer switching to the compositor-aware infra, so we could make SKSLShader disposable and implement lifetime management for inputs/builder as well.


public string[] ChildShaderNames { get; }

public SKImageFilter?[] Inputs { get; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with having ShaderBuilder and Inputs as such properties is lifetime management. There is no clear ownership of ImmutableSKSLEffect once it's passed to the compositor, so we are not allowed to manually call Dispose on ShaderBuilder and SKImageFilters, yet they might hold a huge chunk of unmanaged memory by referencing bitmaps. So the API is prone to "soft" unmanaged memory leaks when GC isn't aware that it's supposed to collect collect gen2 to free a few GB of native bitmaps.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kekekeks Do you means using GC.AddMemoryPressure to add the memory pressure to GC? Or the GC untimely collect the object to release the skia resource?

Copy link
Contributor

@Gillibald Gillibald Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ownership of allocated objects needs to be transferred to the compositor and only the compositor knows when to free those objects. By implementing ICompositionRenderResource etc. you make sure these objects are properly ref counted and will be cleaned up at the right time.

ICompositorSerializable allows you to sync mutable states with the compositor, which will then hold an immutable copy of that resource. Using that mechanism makes AffectsRender redundant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which will then hold an immutable copy of that resource.

Creating a copy of the SKImageFilter seems to be not possible. It's immutable.
Users may want to reuse SKImageFilter. So the ownership shouldn't be transferred to the compositor?
But maybe we can use ICompositionRenderResource to let users know when the compositor is no longer using the object.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as objects are properly ref counted this is fine

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users may want to reuse SKImageFilter

Compositor shouldn't be calling Dispose directly either, it should be calling a user-specified callback as a means to report that it's not using the resource anymore.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e. g. void AddInput(SKImageFilter filter, Action? release = null);

@kekekeks
Copy link
Member

kekekeks commented Jan 16, 2025

We probably want to expose a similar API for SKColorFilter like we currently do with XPF, but it should be way more simple to implement

@lindexi
Copy link
Contributor

lindexi commented Jan 16, 2025

@kekekeks What is the API similar XPF means? Can it use the sksl?

@kekekeks
Copy link
Member

What is the API similar XPF means? Can it use the sksl?

With XPF we have an extension API that allows setting SKColorFilter for a visual. That was required for some apps that utilized WPF's ShaderEffect to implement features like colormap. For Avalonia I'd like to have that as a proper IEffect.

@kkwpsv
Copy link
Contributor Author

kkwpsv commented Jan 16, 2025

The RegisterUniform may be not good enough.
There's too many type of uniforms. We need many overloads. And some type is missing in Avalonia now like vector3 vector4.

@dme-compunet
Copy link
Contributor

dme-compunet commented Jan 17, 2025

According to C# naming convention, the SKSLEffect** should be renamed to SkslEffect**

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants