feat(skia): GPU-accelerated RenderTargetBitmap#23637
Conversation
RenderTargetBitmap now records the visual tree into an SKPicture on the UI thread (mirroring the regular rendering pipeline) and replays it into a GRContext-backed surface during the CompositionTarget's next render pass, reading the pixels back into the caller's buffer. The GRContext is resolved from the draw canvas (SKCanvas.Context), so the mechanism is platform-neutral and applies to every Skia backend; when the target renders in software (raster canvas), pending render jobs fail fast and the previous raster path is used as fallback. Opting out is possible via the new FeatureConfiguration.Rendering.UseGpuRenderTargetBitmap flag, since GPU rasterization can produce slightly different anti-aliasing than the software rasterizer. RenderAsync now completes asynchronously on Skia (one render pass later) instead of synchronously inside the call. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds an orientation test guarding against vertically-flipped output on GPU backends with bottom-left-origin surfaces, and pins When_CornerRadius_AntiAliasing to software RenderTargetBitmap since its reference asset was rendered by the software rasterizer and GPU anti-aliasing differs slightly along the rounded edge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds GPU-accelerated RenderTargetBitmap rendering for Skia targets by replaying a recorded SKPicture into a GRContext-backed surface on the next render pass, with a software fallback and a FeatureConfiguration opt-out.
Changes:
- Implement GPU-backed
RenderTargetBitmaprendering on Skia (record on UI thread, replay/readback on render thread) with software fallback. - Add
CompositionTarget.TryExecuteOnNextRenderAsync(Action<GRContext>)to run work insideDrawwhile the native GPU context is current. - Add runtime tests for vertical-flip regressions and pin an existing anti-aliasing test to software rendering for byte-exact comparisons.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
GPU rendering of RenderTargetBitmap is now always on when the target renders with a GRContext. When_CornerRadius_AntiAliasing consequently can no longer pin itself to the software rasterizer, and no RMSE threshold separates valid GPU-vs-CPU anti-aliasing variance (0.0325) from a simulated non-anti-aliased render (0.0349) on this image, so the test now asserts the invariant directly: a meaningful fraction of the pixels in a ring around the circle's edge must blend between the two colors (~34% for the CPU rasterizer, 0% for a jagged render). Its software-rendered reference asset is removed with it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both overloads carried identical copy-pasted bodies differing only in scaledSize and, through historical drift, in the fallback used when element is null (PublicRootVisual vs RootElement). They now share one core, unified on RootElement: the window's root visual, i.e. what is presented on screen including popups. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hardware and software paths shared no code despite doing the same thing: both now record the SKPicture through one method (owning the layout-clip removal and the IsSoftwareRenderer record-time flag) and replay it through one method (surface, optional resampling, pixel read-back). The only difference between the two paths is the surface factory: GRContext-backed for hardware, raster for the fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-23637/wasm-skia-net9/index.html |
|
The build 220660 found UI Test snapshots differences: Details
|
|
|
The drag visual was captured with RenderAsync, which now completes during a later render pass; the resulting dispatcher yield broke the synchronous DragStarting -> DragStarted -> DragEnter/DragOver sequence (matching WinUI) that the drag-and-drop runtime tests assert, failing them on all Skia CI targets. The capture now uses a synchronous software render (its behavior before the GPU path was introduced), while the public RenderAsync stays asynchronous. Also hardens RenderAsync against overlapping calls on the same instance: calls are serialized with an async gate and the pending GPU replay roots the pixel buffer it writes to, so a concurrent EnsureBuffer can neither resize nor free it mid-write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RunRenderJobs took the canvas only to reach its GRContext; callers now resolve the context themselves. Also asserts UI-thread access in TryExecuteOnNextRenderAsync, matching the rest of the scheduling surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-23637/wasm-skia-net9/index.html |

GitHub Issue: closes #23636, https://github.com/unoplatform/kahua-private/issues/505
PR Type:
✨ Feature
What changed? 🚀
RenderTargetBitmapon Skia targets previously always rasterized in software (SKSurface.Create(info)withCompositor.IsSoftwareRenderer = true), even when the window renders through a hardwareGRContext.With this PR,
RenderTargetBitmaprenders through the GPU when available, in a fully platform-neutral way:SKPictureon the UI thread (mirroring the regular rendering pipeline), then replayed into aGRContext-backedSKSurfaceduring theCompositionTarget's next render pass, and the pixels are read back into the caller's buffer.CompositionTargetgains an internalTryExecuteOnNextRenderAsync(Action<GRContext>): it queues the action, invalidates the host so a render pass happens promptly, and runs the action insideDraw— i.e. on the rendering thread while the native GPU context is current. Since every Skia backend funnels throughOnNativePlatformFrameRequested→Draw, no per-platform code is needed.GRContextis resolved from the draw canvas itself (SKCanvas.Context). A raster canvas (software rendering) yields no context, in which case pending jobs fail fast andRenderTargetBitmapfalls back to the previous software path. Pending jobs are also failed when the window is unregistered, so awaiters never hang on a closing window.RenderAsyncnow completes asynchronously on Skia (one render pass later) instead of synchronously inside the call; on the browser this also avoids blocking the single thread.Note that GPU rasterization can produce slightly different (equally valid) anti-aliasing than the software rasterizer, so
RenderTargetBitmapoutput is no longer bit-identical to the raster path on GPU-rendered windows — it instead matches what is actually presented on screen.Tests:
When_CornerRadius_AntiAliasingno longer compares against a software-rendered reference asset (no RMSE threshold separates valid GPU-vs-CPU anti-aliasing variance, measured 0.0325, from a simulated non-anti-aliased render, 0.0349). It now asserts the invariant directly: a meaningful fraction of the pixels in a ring around the circle's edge must blend between the two colors (~34% for the CPU rasterizer, 0% for a jagged render), making it rasterizer-independent.Validation:
Given_RenderTargetBitmapandGiven_Borderruntime suites were run on Skia Desktop (Win32) against both the Vulkan renderer (via SwiftShader, all renders through the GPU path) and the software renderer (clean fallbacks) — all green. CI agents without GPUs take the software fallback, so screenshot-comparison runs should be unaffected there.PR Checklist ✅
Screenshots Compare Test Runresults.🤖 Generated with Claude Code