I'm trying to implement a video renderer with a WriteableBitmap
.
My current implementation works fine for full screen 60fps content, but if I scrub the video (drag the playback back/forth to a new time) the renderer starts updating the WriteableBitmap
after the second render cycle started, causing it to appear with artifacting (the frame rendering engine uses parallelism).
I tried implementing a double buffer to see if it could help, and it did to a certain point, but the artifacting continues.
public WriteableBitmap RenderedImage
{
get => _frontBuffer;
set => SetProperty(ref _frontBuffer, value);
}
public long CurrentRenderTime
{
get => _currentRenderTime;
set
{
if (SetProperty(ref _currentRenderTime, value))
if (RenderedImage != null)
_event.Set();
}
}
private void RenderFrame()
{
while (!_renderCancellationSource.IsCancellationRequested)
{
//Wait until the CurrentTime changes to render again.
_event.WaitOne();
//TEST: No noticeable difference with this lock.
lock (Lock)
{
var currentTimestamp = CurrentRenderTime;
//Draw the background and all tracks.
DrawBackground(_backBufferAddress, _backStride, _backPixelWidth, _backPixelHeight);
foreach (var track in Tracks)
track.RenderAt(_backBufferAddress, _backStride, CurrentRenderTime);
//Swap the back buffer with the front buffer.
(_frontBuffer, _backBuffer) = (_backBuffer, _frontBuffer);
(_frontBufferAddress, _backBufferAddress) = (_backBufferAddress, _frontBufferAddress);
(_frontStride, _backStride) = (_backStride, _frontStride);
(_frontPixelWidth, _backPixelWidth) = (_backPixelWidth, _frontPixelWidth);
(_frontPixelHeight, _backPixelHeight) = (_backPixelHeight, _frontPixelHeight);
//TEST: No difference with this check.
if (currentTimestamp != CurrentRenderTime)
continue;
Application.Current.Dispatcher.Invoke(() =>
{
RenderedImage.Lock();
RenderedImage.AddDirtyRect(new Int32Rect(0, 0, RenderedImage.PixelWidth, RenderedImage.PixelHeight));
RenderedImage.Unlock();
OnPropertyChanged(nameof(RenderedImage));
_eventRenderer.Set();
}, DispatcherPriority.Render);
//TEST: No difference with this wait.
_eventRenderer.WaitOne();
}
}
}
Lines marked with TEST
are last-minute changes that provided no noticeable results.
More details about the implementation
The rendering must occur on another thread, and it's triggered by changes in the CurrentRenderTime
by using a AutoResetEvent
.
Pseudo-code:
Calculate timings, variable or fixed framerate.
Iterate through the timings.
Call render.
Wait (between-frame timing - time it took to render).
The WriteableBitmap
is being displayed by a WPF Image
element.
The playback mechanism is not the issue here, and anyway I use a fine-tuned timer resolution (PInvoking timeBeginPeriod).
This is the minimal example:
//This method runs in a separated thread.
private void RenderFrame()
{
while (!_renderCancellationSource.IsCancellationRequested)
{
//Wait until the CurrentTime changes to render again.
_event.WaitOne();
lock (Lock)
{
//Draw the background and all tracks.
DrawBackground(_backBufferAddress, _backStride, _backPixelWidth, _backPixelHeight);
foreach (var track in Tracks)
track.RenderAt(_backBufferAddress, _backStride, CurrentRenderTime);
//Swap the back buffer with the front buffer.
(_frontBuffer, _backBuffer) = (_backBuffer, _frontBuffer);
(_frontBufferAddress, _backBufferAddress) = (_backBufferAddress, _frontBufferAddress);
(_frontStride, _backStride) = (_backStride, _frontStride);
(_frontPixelWidth, _backPixelWidth) = (_backPixelWidth, _frontPixelWidth);
(_frontPixelHeight, _backPixelHeight) = (_backPixelHeight, _frontPixelHeight);
//WriteableBitmap Lock/Unlock needs to run on the same thread as the UI.
Application.Current.Dispatcher.Invoke(() =>
{
RenderedImage.Lock();
RenderedImage.AddDirtyRect(new Int32Rect(0, 0, RenderedImage.PixelWidth, RenderedImage.PixelHeight));
RenderedImage.Unlock();
//Since I swapped the buffers, I need to tell WPF about that.
OnPropertyChanged(nameof(RenderedImage));
}, DispatcherPriority.Render);
}
}
}
Possible solution
One solution, but less performant would be to copy data between buffers, instead of switching them.
private void RenderFrame()
{
while (!_renderCancellationSource.IsCancellationRequested)
{
_event.WaitOne();
lock (Lock)
{
DrawBackground(_backBufferAddress, _backStride, _backPixelWidth, _backPixelHeight);
foreach (var track in Tracks)
track.RenderAt(_backBufferAddress, _backStride, Project.Width, Project.Height, CurrentRenderTime);
Kernel32.CopyMemory(_frontBufferAddress, _backBufferAddress, (uint)(_backPixelHeight * _backStride));
Application.Current.Dispatcher.Invoke(() =>
{
RenderedImage.Lock();
RenderedImage.AddDirtyRect(new Int32Rect(0, 0, RenderedImage.PixelWidth, RenderedImage.PixelHeight));
RenderedImage.Unlock();
}, DispatcherPriority.Render);
}
}
}