-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
Conversation
…hat don't need the time feature all the time.
There was a problem hiding this 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> |
There was a problem hiding this comment.
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 = |
There was a problem hiding this comment.
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 => |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@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. |
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. |
@kekekeks i agree, but lets retain the ability to explicitly request for a render layer if it's possible |
I think we need to introduce |
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. |
@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. |
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 |
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. |
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? |
@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 |
@jmacato So you use draw calls other than DrawImage(Bitmap)? |
@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 |
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. |
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 |
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. |
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. |
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?
That is just my opinion and wanted to discuss this a bit. |
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... |
As I've said, if the visual is opaque, we don't need a separate layer. I'll do this optimization later. |
There appears to be an issue using FillRectangle with a brush created like so
|
You need make the brush immutable as far as I understand. |
Yep, using IBrush filterBrush = new SolidColorBrush(Colors.White, 0.2).ToImmutable(); |
@ahopper You can't use types derived from AvaloniaObject on UI thread. Immutable non-visual brushes should be fine. |
This appears to write over the top of all other controls (except popups). |
@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. |
It seems to be an issue with layer ordering, I'll try to repro it with regular layers. |
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... |
It's blocked by #2244 That bug causes "critical time" visuals to render themselves over everything else. |
Any updates? 🤔 |
closing due to inactivity. |
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:
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.