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

[WIP] Introduced visuals that render themselves on the render thread #2185

Closed
wants to merge 12 commits into from

Conversation

kekekeks
Copy link
Member

@kekekeks kekekeks commented Dec 18, 2018

This is needed for displaying things like video output, animated images, foreign content from WritableBitmap, etc. The common thing about these usage scenarios is that they don't need the UI thread, but need a steady frame rate.
Since render thread timer ticks independently from UI thread the "render time critical" visuals can update themselves even if UI thread is completely blocked by the user code.

The API looks like this:

    public interface IRenderTimeCriticalVisual
    {
        bool HasNewFrame { get; }
        void ThreadSafeRender(DrawingContext context, Size logicalSize, double scaling);
    }

They are forced to have a separate layer since they are supposed to update very frequently.

This is DeferredRenderer implementation. For ImmediateRenderer I'm planning to connect it to the RenderLoop's Update and trigger Invalidate calls to the OS if the last frame had IRenderTimeCriticalVisual anywhere in the graph.

@kekekeks kekekeks changed the title [WIP] Introduced visuals that render themselves on the render thread Introduced visuals that render themselves on the render thread Dec 18, 2018
Copy link
Member

@grokys grokys left a comment

Choose a reason for hiding this comment

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

Looks good in general I think, though I'm concerned about it bit-rotting with a) no unit tests and b) no feature in the codebase that actually uses it. Is there a feature you plan on adding soon which makes use of this?

@@ -37,6 +37,7 @@
<TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
<TabItem Header="TabControl"><pages:TabControlPage/></TabItem>
<TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>
<TabItem Header="Time critical render"><pages:TimeCriticalRenderPage/></TabItem>
Copy link
Member

Choose a reason for hiding this comment

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

I think this might be more at home in RenderDemo as it's not a control as such.

private int _currentLevel;


static readonly Stack<Stack<PushedState>> StateStackPool = new Stack<Stack<PushedState>>();
static readonly Stack<Stack<TransformContainer>> TransformStackPool = new Stack<Stack<TransformContainer>>();
private static readonly ThreadSafeObjectPool<Stack<PushedState>> StateStackPool =
Copy link
Member

Choose a reason for hiding this comment

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

Nit: these fields are capitalized as properties.


// Just return scene, layers weren't updated

var hasCriticalRenderTimeVisualUpdates = Layers.Any(l =>
Copy link
Member

Choose a reason for hiding this comment

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

Might not be the best idea to use LINQ in the rendering code?

private Stack<TransformContainer> _transformContainers = TransformStackPool.Count == 0
? new Stack<TransformContainer>()
: TransformStackPool.Pop();
private Stack<PushedState> _states = StateStackPool.Get();
Copy link
Member

Choose a reason for hiding this comment

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

So this PR also refactors the pooling for the PushedState stacks? Is this required by this feature or is it independent of the feature?

Copy link
Member Author

Choose a reason for hiding this comment

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

Got a crash caused by the lack of locking in this code. Incorporated in the PR as a separate commit.

/// <summary>
/// Gets the original visual size before transformations
/// </summary>
Size VisualSize { get; }
Copy link
Member

Choose a reason for hiding this comment

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

As far as I can tell, this is only needed by IRenderTimeCriticalVisual, is that right? Could this property be added to that interface? If it really needs to be added here it might be best to call it "PreTransformSize" or something?

Copy link
Member Author

Choose a reason for hiding this comment

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

It needs to be saved somewhere during the scene build pass.
The actual visual dimensions could be already changed on UI thread by the time we are trying to consume it.

@@ -239,6 +248,15 @@ protected IAvaloniaList<IVisual> VisualChildren
private set;
}

/// <summary>
/// Allows to explicitly force a rendering layer for a particular visual. Use with caution.
Copy link
Member

@grokys grokys Dec 19, 2018

Choose a reason for hiding this comment

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

A few things here:

  • What does "Wants" mean here? Does it mean the request might not be respected? Because "wants" to me suggests a request that can be denied. If it will always be respected, maybe something like "ForceLayer" might be better?
  • This is diverging from WPF/UWP's way of requesting a new layer. Are we OK with that?
  • I think "Use with caution" should be in the <Remarks> section along with a description of why it should be used with caution.

Copy link
Member Author

Choose a reason for hiding this comment

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

Does it mean the request might not be respected

It's not respected by ImmediateRenderer. The idea is that it's just a hint.

I think we could remove this change since it's not really needed for the purposes of this PR.

/// Checks if visual has time critical content. You need to call InvalidateVisual for this property to be re-queried.
/// </summary>
bool HasRenderTimeCriticalContent { get; }
/// <summary>
Copy link
Member

Choose a reason for hiding this comment

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

Nit: no newlines between properties

@@ -148,7 +148,9 @@ public IRenderer CreateRenderer(IRenderRoot root)
if (customRendererFactory != null)
return customRendererFactory.Create(root, loop);

return Win32Platform.UseDeferredRendering ? (IRenderer)new DeferredRenderer(root, loop) : new ImmediateRenderer(root);
return Win32Platform.UseDeferredRendering
? (IRenderer)new DeferredRenderer(root, loop)
Copy link
Member

Choose a reason for hiding this comment

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

Nit: the ? in these sort of statements is usually put at the end of the condition for multi-line statements.

Copy link
Member Author

Choose a reason for hiding this comment

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

condition
     ? left
     : right;

is a pretty common pattern for multi-line expressions.

@kekekeks
Copy link
Member Author

Is there a feature you plan on adding soon which makes use of this?

@jmacato is currently working on GIF animations support and we are planning to run them on the render thread since they obviously don't need anything from the UI thread to continue ticking.

@kekekeks
Copy link
Member Author

kekekeks commented Dec 19, 2018

Actually now that I think about it, we might want to let "render time critical" visuals not occupy a separate layer if they are opaque. Might want to change the code for that, since it would be way too expensive for each GIF to have a separate layer.

@jmacato
Copy link
Member

jmacato commented Dec 20, 2018

@kekekeks i agree, but lets retain the ability to explicitly request for a render layer if it's possible

@kekekeks
Copy link
Member Author

kekekeks commented Dec 20, 2018

I think we need to introduce IsOpaque property. That's how Cocoa does it.

@Gillibald
Copy link
Contributor

Isn't it possible to request a layer that is rendered fast enough to process such time critical scenarios? Avoiding a special interface would be ideal. Maybe i don't understand the real performance issue here. Isn't this approach just avoiding the heavy layouting etc by having it's own layer? If they are processed fom top to bottom this should still be fast enough. Someone could even request a layer that is smaller than the screen dimensions.

@kekekeks
Copy link
Member Author

@Gillibald we aren't alone on the UI thread. It can be also busy processing user's business logic. Our code can also be quite expensive. So we can't get a steady frame rate unless render thread doesn't need anything from the UI thread. The only way to do so is to allow visuals to render themselves on the render thread bypassing our scene builder entirely.

This PR introduces an interface that allows visuals to do painting on the render thread. The current implementation always creates a layer because we can't properly track rendering operations, so we don't know what we need to invalidate and re-render. If such visual is opaque, we could skip layer creation and just re-render everything on top of the visual's rect.

@Gillibald
Copy link
Contributor

So ThreadSafeRender is called every RenderLoop tick on the render thread as long as the HasNewFrame is true.

Why is ThreadSafeRender needed? Can't we use IVisual.Render

@kekekeks
Copy link
Member Author

kekekeks commented Dec 20, 2018

Why is ThreadSafeRender needed? Can't we use IVisual.Render

Visual.Bounds is a dependency property which can't be read from the render thread. The value of Visual.Bounds might also contain a new value that doesn't match the current scene. That's why there is a separate method. It also explicitly indicates in its name that method should be thread-safe.

@Gillibald
Copy link
Contributor

Are there any real world examples that need the ability to issue render commands threw a DrawingContext? Isn't a special Bitmap implementation enough for this use case? In general someone wants a surface he can write to in a performant way or am I wrong?

@jmacato
Copy link
Member

jmacato commented Dec 20, 2018

@Gillibald my gif renderer is one of those use cases, sometimes one needs a much more flexible approach when it comes to rendering, besides if we ever get to have 3d matrix transforms then it'll be easier to develop them with the current API than just having a fast bitmap control

@Gillibald
Copy link
Contributor

@jmacato So you use draw calls other than DrawImage(Bitmap)?

@jmacato
Copy link
Member

jmacato commented Dec 20, 2018

@Gillibald not yet, but i do have some valid use case for it later on like text overlay, etc. In any case, exposing the drawcontext is better than locking ourselves up later on especially when we lockdown the API surface @ v1.0

@Gillibald
Copy link
Contributor

I just thought it would make sense to enable this kind of rendering just for Bitmaps so you could use all kinds of Bitmaps. Even RenderTargetBitmap. In the future you could have a 3DImage etc that kind of works the same. Everything just has to have a Bitmap implementation. Maybe I am wrong.

@jmacato
Copy link
Member

jmacato commented Dec 20, 2018

i think this enables that though, they are not mutually-exclusive anyway :) if one needs more flexible rendering control they can use this, if they just want to push pixels fast enough then a IRenderTimeCriticalVisual based RTB/WB is good enough i think

@ahopper
Copy link
Contributor

ahopper commented Dec 20, 2018

I certainly have a use case for more than just bitmaps as I have a changing grid and scales over various live graph/waterfall type displays and it would be nice to use the avalonia tools to draw these. They could be done in an overlay on the ui thread but this could look jumpy at times.

@na2axl
Copy link

na2axl commented Dec 20, 2018

I just thought it would make sense to enable this kind of rendering just for Bitmaps so you could use all kinds of Bitmaps.

I think that locking the use of this feature only for bitmaps is not a good idea, because it's impossible to know why the user want this feature and how he plan to use it. For example I use this feature for a game engine to write bitmaps generated by each game render passes and text for FPS information, logs, events, etc...

It could be heavy for the user to compute everything as a bitmap before passing it to the ThreadSafeRender method if he want to write more than a simple bitmap.

Another use case could be that the user want to create a custom frame by frame animation, a bouncing ball for example, with the set of DrawXXX methods from the DrawingContext.

@Gillibald
Copy link
Contributor

Gillibald commented Dec 20, 2018

Every layer in Avalonia is a RenderTargetBitmap so it will that way anyways. If someone wanted to issue draw commands he could use a RenderTargetBitmap. WritableBitmap and RenderTargetBitmap could implement that new logic by default. Is there any downside to this? Wouldn't WritableBitmap etc benefit from this?

new DrawingContext(RenderTargetBitmap bitmap)

That is just my opinion and wanted to discuss this a bit.

@na2axl
Copy link

na2axl commented Dec 20, 2018

Wouldn't WritableBitmap etc benefit from this?

Yes it's true, so it's to the user to know exactly why this feature have to be used.

Maybe for performances issues, or because he want a control on the frame rate of what he want to draw, to bypass the default 60 FPS of the render loop.

I think that we are not able to say that this feature will always be used for bitmaps, so to enable a larger set of use cases, it's better to let the user decide...

@kekekeks
Copy link
Member Author

Every layer in Avalonia is a RenderTargetBitmap so it will that way anyways.

As I've said, if the visual is opaque, we don't need a separate layer. I'll do this optimization later.

@ahopper
Copy link
Contributor

ahopper commented Dec 21, 2018

There appears to be an issue using FillRectangle with a brush created like so
SolidColorBrush filterBrush = new SolidColorBrush(Colors.White, 0.2);
This

double opacity = brush.Opacity * _currentOpacity;
throws a 'Call from invalid thread' exception

@Gillibald
Copy link
Contributor

You need make the brush immutable as far as I understand.

@ahopper
Copy link
Contributor

ahopper commented Dec 21, 2018

Yep, using IBrush filterBrush = new SolidColorBrush(Colors.White, 0.2).ToImmutable();
works perfectly, thanks.

@kekekeks
Copy link
Member Author

@ahopper You can't use types derived from AvaloniaObject on UI thread. Immutable non-visual brushes should be fine.

@ahopper
Copy link
Contributor

ahopper commented Dec 24, 2018

This appears to write over the top of all other controls (except popups).

@Gillibald
Copy link
Contributor

@ahopper That's because this solution has its own layer on top of the root layer. Popup should have its own window(root layer) on top of the parent window.

@kekekeks
Copy link
Member Author

kekekeks commented Jan 3, 2019

It seems to be an issue with layer ordering, I'll try to repro it with regular layers.

@kekekeks kekekeks changed the title Introduced visuals that render themselves on the render thread [WIP] Introduced visuals that render themselves on the render thread Jan 9, 2019
@na2axl
Copy link

na2axl commented Feb 7, 2019

No evolution on this WIP? Since the PR is pretty old, it seems that dotnet is unable to resolve the last build of this PR, and fallback to another build in which this feature is not available...

@MarchingCube
Copy link
Collaborator

MarchingCube commented Mar 12, 2019

@grokys @kekekeks It's been a while but I will repeat the question - any changes on the side of this PR? Do you need some help here? Could be useful for my purposes quite soon.

@kekekeks
Copy link
Member Author

It's blocked by #2244

That bug causes "critical time" visuals to render themselves over everything else.

@realvictorprm
Copy link

Any updates? 🤔

@danwalmsley
Copy link
Member

closing due to inactivity.

@danwalmsley danwalmsley closed this Jul 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants