如何消除WriteableBitmap快速更新时的画面伪影
问题描述
我正在尝试使用WriteableBitmap实现一个视频渲染器。当前实现对全屏60fps内容运行正常,但拖动视频进度条切换到新时间点时,渲染器会在第二个渲染周期开始后更新WriteableBitmap,导致画面出现伪影(帧渲染引擎采用并行机制)。
附画面伪影示意图:
![画面伪影]
我尝试实现双缓冲来解决问题,虽有一定效果,但伪影仍然存在。
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) { //等待CurrentTime变化后重新渲染 _event.WaitOne(); //TEST: 加锁无明显效果 lock (Lock) { var currentTimestamp = CurrentRenderTime; //绘制背景和所有轨道 DrawBackground(_backBufferAddress, _backStride, _backPixelWidth, _backPixelHeight); foreach (var track in Tracks) track.RenderAt(_backBufferAddress, _backStride, CurrentRenderTime); //交换前后缓冲区 (_frontBuffer, _backBuffer) = (_backBuffer, _frontBuffer); (_frontBufferAddress, _backBufferAddress) = (_backBufferAddress, _frontBufferAddress); (_frontStride, _backStride) = (_backStride, _frontStride); (_frontPixelWidth, _backPixelWidth) = (_backPixelWidth, _frontPixelWidth); (_frontPixelHeight, _backPixelHeight) = (_backPixelHeight, _frontPixelHeight); //TEST: 此检查无效果 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: 等待无效果 _eventRenderer.WaitOne(); } } }
标记有TEST的代码是临时修改内容,但未产生明显效果。
实现细节补充
- 渲染必须在单独线程中进行,通过
AutoResetEvent监听CurrentRenderTime的变化触发渲染。 - 伪代码:
计算时序(可变或固定帧率) 遍历时序 调用渲染 等待(帧间隔时长 - 渲染耗时)
WriteableBitmap由WPF的Image元素显示。- 播放机制无问题,已通过PInvoke调用
timeBeginPeriod实现高精度定时器。
简化示例代码:
//此方法在独立线程中运行 private void RenderFrame() { while (!_renderCancellationSource.IsCancellationRequested) { //等待CurrentTime变化后重新渲染 _event.WaitOne(); lock (Lock) { //绘制背景和所有轨道 DrawBackground(_backBufferAddress, _backStride, _backPixelWidth, _backPixelHeight); foreach (var track in Tracks) track.RenderAt(_backBufferAddress, _backStride, CurrentRenderTime); //交换前后缓冲区 (_frontBuffer, _backBuffer) = (_backBuffer, _frontBuffer); (_frontBufferAddress, _backBufferAddress) = (_backBufferAddress, _frontBufferAddress); (_frontStride, _backStride) = (_backStride, _frontStride); (_frontPixelWidth, _backPixelWidth) = (_backPixelWidth, _frontPixelWidth); (_frontPixelHeight, _backPixelHeight) = (_backPixelHeight, _frontPixelHeight); //WriteableBitmap的Lock/Unlock必须在UI线程执行 Application.Current.Dispatcher.Invoke(() => { RenderedImage.Lock(); RenderedImage.AddDirtyRect(new Int32Rect(0, 0, RenderedImage.PixelWidth, RenderedImage.PixelHeight)); RenderedImage.Unlock(); //由于交换了缓冲区,需要通知WPF更新 OnPropertyChanged(nameof(RenderedImage)); }, DispatcherPriority.Render); } } }
可行解决方案
一种性能稍差的解决方案是在缓冲区之间复制数据,而非直接交换缓冲区:
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); } } }
内容的提问来源于stack exchange,提问作者Nicke Manarin
相关产品推荐
相关产品推荐

