Skip to content

Commit a9f18a4

Browse files
authored
Merge pull request #49 from jetspiking/fix/webview2-compositor-rendering-stability
Prevent visual crashes in WebView2 compositor rendering
2 parents 2f111e5 + baa5b1d commit a9f18a4

2 files changed

Lines changed: 112 additions & 83 deletions

File tree

src/Avalonia.Controls.WebView.Core/Win/WebView2/WebView2CompAdapter.cs

Lines changed: 92 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -133,100 +133,119 @@ public async Task UpdateWriteableBitmap(PixelSize currentSize,
133133
{
134134
var target = _controller.GetRootVisualTarget();
135135

136-
if (target is null || currentSize.Height == 0 || currentSize.Width == 0)
136+
if (target is null)
137137
return;
138138

139-
// ReSharper disable once SuspiciousTypeConversion.Global
140-
var compositor = ((ICompositionObject)target).Compositor();
141-
// ReSharper disable once SuspiciousTypeConversion.Global
142-
var compositorCapture = (ICompositionCaptureTest)compositor;
143-
144-
// https://github.com/ocalvo/WorkTests/blob/a2f18151be579addc60d4464b1e2a9f54f8e3314/MediaTestManaged/DCompHelpers.cs#L162
145-
146-
// Render Visual:
147-
// * This function is basically async and returns immediately after putting
148-
// a MILCMD onto the batch for the application channel, and marks the device dirty.
149-
// * It returns two handles by reference.
150-
// * The first handle (hMap) is to a map of bits.
151-
// * The second handle (hEvent) is to an event.
152-
// * The event is signaled when a commit has happened
153-
// and the after actual renderpass has been rendered and presented.
154-
// * Once signaled, the bits are ready for us to grab.
155-
// * Any changes to the tree before the implicit commit sends the batch to the
156-
// Compositor will be reflected in the capture, even if made after the initial
157-
// RenderVisual function call.
158-
// * Any changes to the tree after the implicit commit may safely modify the tree,
159-
// as they will be processed in a separate batch
160-
var hMap = IntPtr.Zero;
161-
var hEvent = IntPtr.Zero;
162-
163-
var hr = compositorCapture.RenderVisual(
164-
target,
165-
0, // offset X
166-
0, // offset y
167-
(uint)currentSize.Width,
168-
(uint)currentSize.Height,
169-
CompositionCaptureTestBitmapPixelFormat.Bgra8,
170-
ref hMap,
171-
ref hEvent,
172-
out var cbMap);
173-
if (hr != 0)
174-
{
175-
throw new COMException("Render Visual Failed", new Win32Exception(hr));
176-
}
177-
178139
try
179140
{
180-
using var wh = new ManualResetEvent(false);
181-
wh.SafeWaitHandle = new SafeWaitHandle(hEvent, ownsHandle: false);
182-
183-
var tcs = new TaskCompletionSource<bool>();
184-
ThreadPool.RegisterWaitForSingleObject(wh, static (state, timedOut) =>
185-
{
186-
var tcs = (TaskCompletionSource<bool>)state!;
187-
tcs.SetResult(!timedOut);
188-
}, tcs, TimeSpan.FromMilliseconds(100), true);
141+
if (currentSize.Height == 0 || currentSize.Width == 0)
142+
return;
189143

190-
if (!await tcs.Task)
144+
// ReSharper disable once SuspiciousTypeConversion.Global
145+
var compositor = ((ICompositionObject)target).Compositor();
146+
// ReSharper disable once SuspiciousTypeConversion.Global
147+
var compositorCapture = (ICompositionCaptureTest)compositor;
148+
149+
// https://github.com/ocalvo/WorkTests/blob/a2f18151be579addc60d4464b1e2a9f54f8e3314/MediaTestManaged/DCompHelpers.cs#L162
150+
151+
// Render Visual:
152+
// * This function is basically async and returns immediately after putting
153+
// a MILCMD onto the batch for the application channel, and marks the device dirty.
154+
// * It returns two handles by reference.
155+
// * The first handle (hMap) is to a map of bits.
156+
// * The second handle (hEvent) is to an event.
157+
// * The event is signaled when a commit has happened
158+
// and the after actual renderpass has been rendered and presented.
159+
// * Once signaled, the bits are ready for us to grab.
160+
// * Any changes to the tree before the implicit commit sends the batch to the
161+
// Compositor will be reflected in the capture, even if made after the initial
162+
// RenderVisual function call.
163+
// * Any changes to the tree after the implicit commit may safely modify the tree,
164+
// as they will be processed in a separate batch
165+
var hMap = IntPtr.Zero;
166+
var hEvent = IntPtr.Zero;
167+
168+
var hr = compositorCapture.RenderVisual(
169+
target,
170+
0, // offset X
171+
0, // offset y
172+
(uint)currentSize.Width,
173+
(uint)currentSize.Height,
174+
CompositionCaptureTestBitmapPixelFormat.Bgra8,
175+
ref hMap,
176+
ref hEvent,
177+
out var cbMap);
178+
if (hr != 0)
191179
{
192-
// Timeout, ignore
193-
return;
180+
throw new COMException("Render Visual Failed", new Win32Exception(hr));
194181
}
195182

196-
using (producer.GetNextFrame(currentSize, out var frame))
183+
try
197184
{
198-
var pbMap = PInvoke.MapViewOfFile(
199-
new HANDLE(hMap), FILE_MAP.FILE_MAP_WRITE, 0, 0, UIntPtr.Zero);
185+
using var wh = new ManualResetEvent(false);
186+
wh.SafeWaitHandle = new SafeWaitHandle(hEvent, ownsHandle: false);
200187

201-
if (pbMap != IntPtr.Zero)
188+
var tcs = new TaskCompletionSource<bool>();
189+
RegisteredWaitHandle? registeredWaitHandle = null;
190+
try
202191
{
203-
try
192+
registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(wh, static (state, timedOut) =>
204193
{
205-
using var buf = frame.Lock();
206-
unsafe
207-
{
208-
Buffer.MemoryCopy(
209-
source: pbMap,
210-
destination: (void*)buf.Address,
211-
destinationSizeInBytes: buf.RowBytes * currentSize.Height,
212-
sourceBytesToCopy: cbMap
213-
);
214-
}
194+
var tcs = (TaskCompletionSource<bool>)state!;
195+
tcs.SetResult(!timedOut);
196+
}, tcs, TimeSpan.FromMilliseconds(100), true);
197+
198+
if (!await tcs.Task)
199+
{
200+
// Timeout, ignore this frame but keep the capture loop alive.
201+
return;
215202
}
216-
finally
203+
}
204+
finally
205+
{
206+
registeredWaitHandle?.Unregister(null);
207+
}
208+
209+
using (producer.GetNextFrame(currentSize, out var frame))
210+
{
211+
var pbMap = PInvoke.MapViewOfFile(
212+
new HANDLE(hMap), FILE_MAP.FILE_MAP_WRITE, 0, 0, UIntPtr.Zero);
213+
214+
if (pbMap != IntPtr.Zero)
217215
{
218-
PInvoke.UnmapViewOfFile(pbMap);
216+
try
217+
{
218+
using var buf = frame.Lock();
219+
unsafe
220+
{
221+
Buffer.MemoryCopy(
222+
source: pbMap,
223+
destination: (void*)buf.Address,
224+
destinationSizeInBytes: buf.RowBytes * currentSize.Height,
225+
sourceBytesToCopy: cbMap
226+
);
227+
}
228+
}
229+
finally
230+
{
231+
PInvoke.UnmapViewOfFile(pbMap);
232+
}
219233
}
220234
}
221235
}
236+
finally
237+
{
238+
PInvoke.CloseHandle(new HANDLE(hMap));
239+
PInvoke.CloseHandle(new HANDLE(hEvent));
240+
}
222241
}
223242
finally
224243
{
225-
PInvoke.CloseHandle(new HANDLE(hMap));
226-
PInvoke.CloseHandle(new HANDLE(hEvent));
244+
if (!Disposed)
245+
{
246+
_commitAsyncLoopHandler.RegisterNext();
247+
}
227248
}
228-
229-
_commitAsyncLoopHandler.RegisterNext();
230249
}
231250

232251
internal EventHandler? GetCursorChanged() => _cursorChangedHandler;

src/Avalonia.Controls.WebView/NativeWebViewCompositorHost.cs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,19 +129,29 @@ private void WebViewAdapterOnInitialized(IWebViewAdapterWithOffscreenBuffer adap
129129

130130
private async void OffscreenAdapter_OnDrawRequested()
131131
{
132-
var adapter = (IWebViewAdapterWithOffscreenBuffer?)TryGetAdapter();
133-
if (adapter is null)
134-
return;
132+
try
133+
{
134+
var adapter = (IWebViewAdapterWithOffscreenBuffer?)TryGetAdapter();
135+
if (adapter is null)
136+
return;
137+
138+
var topLevel = TopLevel.GetTopLevel(this);
139+
if (topLevel is null)
140+
return;
141+
142+
var adapterSize = PixelSize.FromSize(Bounds.Size, topLevel.RenderScaling);
143+
if (_firstDraw)
144+
{
145+
_firstDraw = false;
146+
adapter.SizeChanged(adapterSize);
147+
}
135148

136-
var adapterSize = PixelSize.FromSize(Bounds.Size, TopLevel.GetTopLevel(this)!.RenderScaling);
137-
if (_firstDraw)
149+
await adapter.UpdateWriteableBitmap(adapterSize, _frameChain.Producer);
150+
_customVisual?.SendHandlerMessage(VisualHandler.DrawRequested);
151+
}
152+
catch (Exception)
138153
{
139-
_firstDraw = false;
140-
adapter.SizeChanged(adapterSize);
141154
}
142-
143-
await adapter.UpdateWriteableBitmap(adapterSize, _frameChain.Producer);
144-
_customVisual?.SendHandlerMessage(VisualHandler.DrawRequested);
145155
}
146156

147157
private void CursorAdapter_OnCursorChanged(object? sender, EventArgs e)

0 commit comments

Comments
 (0)