Repository navigation
Refactor per-draw render data allocations with a binary opcode stream - #21366
Conversation
|
You can test this PR using the following package version. |
|
You can test this PR using the following package version. |
|
I have addressed the review comments, let me know if there is anything else you would like to be changed. |
|
You can test this PR using the following package version. |
|
You can test this PR using the following package version. |
|
You can test this PR using the following package version. |
0b20c25 to
7fb34d8
Compare
|
I've rebased the PR and added serialization for the effect support. It would be nice to know if anything else needs to be changed, I don't want to keep up with rebasing and plucking in things that are being added in the master branch. |
|
You can test this PR using the following package version. |
| if (_buffer is null) | ||
|
_buffer = ArrayPool |
||
| else if (_buffer.Length < required) | ||
| { | ||
|
var grown = ArrayPool |
||
| Array.Copy(_buffer, grown, _length); | ||
|
ArrayPool |
||
| _buffer = grown; | ||
| } |
There was a problem hiding this comment.
I don't have a preference here, just a question. Should we use shared pool here or create a dedicated one for rendering? Dedicated one can be enough, if it's only reused between frames, and not globally in the app
There was a problem hiding this comment.
The benefit of shared is that it can re-use memory and keep the memory footprint small across the app for when things just need a scratch buffer, but I also don't exactly have a strong preference here assuming this question was directed at me?
There was a problem hiding this comment.
Note that a shared pool has an app-wide cap of memory that could be returned into it without being GC-freed. This might have unexpected effects in actual apps where some other library uses the same pool, we'll essentially fall back to always allocate.
This is probably out of scope of this PR, but we could use the same approach as with our batch streams, where we track sustained usage counters over time and free memory after some time passes.
Another thing we could explore here is ref-counted native memory, but, again, out of scope.
There was a problem hiding this comment.
Makes sense, I can follow up on this if this if/when PR lands, for now I somewhat doubt someone will ever run into the issue of pooled memory exhaustion, if this happens then quite frankly it's more of a problem that it's being used wrong.
|
You can test this PR using the following package version. |
|
You can test this PR using the following package version. |
MrJul
left a comment
There was a problem hiding this comment.
Substantial gains, comprehensive tests, clean code, this is a great PR. I've reviewed it and this looks very good to me. I've left a couple of very minor comments.
First step of the Drawing/Nodes binary stream refactor.
Encodes the opcode stream field-by-field via BinaryPrimitives to avoid unsafe and blittability assumptions.
b97057b to
953f823
Compare
|
You can test this PR using the following package version. |
This work stems from the comment #20885 (comment) , this is not exactly how WPF is doing it, the unsafe keyword for example has been avoided, also I wasn't a huge fan of where #20885 was going.
What does the pull request do?
Replaces the render data representation. Until now every
DrawingContextdraw or push call recorded a heap-allocated node object (RenderDataLineNode,RenderDataRectangleNode, the push nodes, …) into aPooledInlineList.This PR replaces those objects with a flat binary opcode stream plus a resource table. This is the approach WPF uses for its MILCMD render data.
It is an internal change: no public API, rendering output, or behaviour changes.
What is the current behavior?
Every recorded draw/push allocates a node object. For a visual whose content changes each frame (animations, custom-drawn controls) that is one GC-tracked allocation per draw call, every frame - gen0 churn proportional to the draw-call count.
What is the updated/expected behavior with this PR?
Render data is recorded as a
byte[]opcode stream plus a resource table. Recording, server-side replay, hit-testing and bounds calculation all walk the byte stream directly, with no per-draw object allocation.Rendering, hit-testing and visual bounds are unchanged, covered by the render-data contract tests merged in #21341 and the existing render tests, plus new unit tests added here, resource table, the three walkers, serialization round-trip, deep-nesting fallback.
The changes were measured locally with a stress test of ~9k draw calls per frame using mixed primitives.
master:
PR:
The code for the test: https://gist.github.com/ZehMatt/6d033ecd016335b8b02f649a6666286c , its quite evident that this saves quite a bit of memory, in my testing there was no GC pressure anymore in the rendering path, now the major contributor to GC cycles is elsewhere.
How was the solution implemented (if it's not obvious)?
New types under
Rendering/Composition/Drawing/:RenderDataOpcode- one value per draw/push operation, plusPop.RenderDataWriter/RenderDataReader- the byte codec. Blittable payload structs (Point,Rect,RoundedRect,Matrix,BoxShadow,RenderOptions, …) are bulk-copied through awhere T : unmanagedgeneric helper; the constraint is a compile-time guard against a payload type silently becoming non-blittable.RenderDataResources- interns the non-blittable operands (brushes, pens, geometries, bitmaps, custom ops) tointhandles referenced from the payloads.RenderDataStream- owns the opcode stream and resource table, with the recording API and the replay / hit-test / bounds walkers. Push/Pop are inline opcodes; the walkersstackalloctheir scope stack, sized from a max-push-depth tracked whilerecording.
The four consumers were switched over:
RenderDataDrawingContext(recording),CompositionRenderData(client + hit-testing + serialization),ServerCompositionRenderData(server replay + bounds),ImmediateRenderDataSceneBrushContentand the old node classes andIRenderDataItemdeleted.The branch is structured as small standalone commits, so it should be easy to review the changes commit by commit.
Checklist
Fixed issues
Addresses some points of #19363 in regards to rendering.
Final Note
I'm glad that #20885 wasn't merged, this is definitely the cleaner solution to the problem and its backed up by the data.