You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何消除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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 16:24:54