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

Windows下多上下文场景中SkiaSharp触发访问违规问题排查

问题:Windows下Mupen64Plus前端OpenGL+Skia渲染触发段错误

我正在为Mupen64Plus模拟器开发前端,支持通过自定义脚本在视频输出上绘图。但该实现仅在Windows系统下出现段错误(Linux系统无此问题)。

以下最小可复现示例(MVE)模拟了我的渲染系统,编译需从NuGet安装Silk.NET.OpenGL、Silk.NET.SDL和SkiaSharp:

  • 视频插件控制大部分渲染循环,其操作由Render()调用表示
  • 后续代码模拟视频插件请求交换缓冲区时的流程:
    • 刷新OpenGL输出
    • 切换到独立上下文渲染Skia输出
    • 刷新Skia输出
    • 渲染OpenGL

代码会在Windows系统的第72行触发访问违规。

using System.Diagnostics;
using Silk.NET.OpenGL;
using Silk.NET.SDL;
using SkiaSharp;

Sdl sdl = Sdl.GetApi();

unsafe
{
    sdl.SetHint(Sdl.HintVideodriver, "x11");
    if (sdl.Init(Sdl.InitVideo | Sdl.InitEvents) != 0)
        SdlAbort();

    sdl.GLSetAttribute(GLattr.ContextMajorVersion, 4);
    sdl.GLSetAttribute(GLattr.ContextMinorVersion, 1);
    sdl.GLSetAttribute(GLattr.ContextProfileMask, (int) GLprofile.Compatibility);

    sdl.GLSetAttribute(GLattr.RedSize, 8);
    sdl.GLSetAttribute(GLattr.GreenSize, 8);
    sdl.GLSetAttribute(GLattr.BlueSize, 8);
    sdl.GLSetAttribute(GLattr.AlphaSize, 8);
    sdl.GLSetAttribute(GLattr.Doublebuffer, 1);
    sdl.GLSetAttribute(GLattr.StencilSize, 8);

    Window* window = sdl.CreateWindow("Test", Sdl.WindowposCentered, Sdl.WindowposCentered, 800, 600, (uint) WindowFlags.Opengl);
    void* mainCtx = sdl.GLCreateContext(window);
    GL mainGL = GL.GetApi(x => (IntPtr) sdl.GLGetProcAddress(x));

    void* skiaCtx = sdl.GLCreateContext(window);
    if (sdl.GLMakeCurrent(window, skiaCtx) != 0)
        SdlAbort();

    using var grContext = GRContext.CreateGl();
    Debug.Assert(grContext != null);
    using var surface =
        SKSurface.Create(grContext,
            new GRBackendRenderTarget(800, 600, 0, 8, new GRGlFramebufferInfo(0, (uint) GLEnum.Rgba8)),
            SKColorType.Rgba8888);
    Debug.Assert(surface != null);

    if (sdl.GLMakeCurrent(window, mainCtx) != 0)
        SdlAbort();

    Init(mainGL);

    bool shouldClose = false;
    while (true)
    {
        Event evt = new();
        while (sdl.PollEvent(ref evt) != 0)
        {
            switch ((EventType) evt.Type)
            {
                case EventType.Windowevent:
                    switch ((WindowEventID) evt.Window.Event)
                    {
                        case WindowEventID.Close:
                            shouldClose = true;
                            break;
                    }

                    break;
            }
        }

        Render(mainGL);
        mainGL.Flush();
        if (sdl.GLMakeCurrent(window, skiaCtx) != 0)
            SdlAbort();
        Render2(surface.Canvas);
        surface.Flush();
        if (sdl.GLMakeCurrent(window, mainCtx) != 0)
            SdlAbort();
        sdl.GLSwapWindow(window);

        if (shouldClose)
            break;
    }
    sdl.GLDeleteContext(mainCtx);
    sdl.DestroyWindow(window);

    sdl.QuitSubSystem(Sdl.InitVideo | Sdl.InitEvents);
}

void SdlAbort()
{
    throw new Exception($"SDL: {sdl.GetErrorS()}");
}

void Init(GL gl)
{

}

void Render(GL gl)
{
    gl.ClearColor(0.0f, 0.0f, 1.0f, 1.0f);
    gl.Clear(ClearBufferMask.ColorBufferBit);
}

void Render2(SKCanvas canvas)
{
    using var lime = new SKPaint
    {
        Color = SKColors.Lime
    };
    canvas.DrawCircle(300, 300, 100, lime);
}

请问这类段错误的成因是什么?该如何避免?


成因分析

  1. Windows OpenGL上下文资源隔离限制
    Windows下的OpenGL上下文对跨上下文资源访问的限制远严于Linux。你创建了两个独立的OpenGL上下文(mainCtx和skiaCtx)但未启用资源共享,Skia绑定到skiaCtx后尝试访问主上下文的默认帧缓冲区时,会因资源不可见触发访问违规。Linux的OpenGL实现对这类操作的限制更宽松,因此无报错。

  2. 帧缓冲区归属不匹配
    创建Skia表面时使用的GRGlFramebufferInfo指定了默认帧缓冲区(ID 0),但该帧缓冲区属于mainCtx。在skiaCtx中直接访问不属于它的帧缓冲区,Windows驱动会直接拒绝这种越权操作,导致段错误。

  3. 无效的SDL视频驱动设置
    代码中硬编码sdl.SetHint(Sdl.HintVideodriver, "x11"),Windows系统无X11驱动,SDL会自动回退到原生驱动,但这个强制设置会引发上下文初始化的隐性错误,加剧资源访问冲突。

解决方法

1. 启用OpenGL上下文共享

创建skiaCtx时将mainCtx指定为共享上下文,让两个上下文可以安全共享纹理、帧缓冲区等资源:

// 替换原skiaCtx创建代码
void* skiaCtx = sdl.GLCreateContext(window, mainCtx); // 第二个参数为共享上下文

注意:不同SDL绑定的GLCreateContext重载参数顺序可能不同,需确保传递正确的共享上下文指针。

2. 为Skia创建独立离屏渲染目标

不要直接使用主上下文的默认帧缓冲区,而是为Skia创建独立的离屏帧缓冲区,渲染完成后再将内容绘制到主上下文:

// 在skiaCtx激活时创建离屏帧缓冲区
if (sdl.GLMakeCurrent(window, skiaCtx) != 0)
    SdlAbort();

// 创建纹理作为离屏渲染目标
mainGL.GenTextures(1, out uint skiaTexture);
mainGL.BindTexture(TextureTarget.Texture2D, skiaTexture);
mainGL.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba, 800, 600, 0, PixelFormat.Rgba, PixelType.UnsignedByte, null);
mainGL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
mainGL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);

// 创建帧缓冲区并绑定纹理
mainGL.GenFramebuffers(1, out uint skiaFbo);
mainGL.BindFramebuffer(FramebufferTarget.Framebuffer, skiaFbo);
mainGL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, skiaTexture, 0);

// 使用该FBO创建Skia表面
using var surface = SKSurface.Create(grContext,
    new GRBackendRenderTarget(800, 600, 0, 8, new GRGlFramebufferInfo(skiaFbo, (uint)GLEnum.Rgba8)),
    SKColorType.Rgba8888);

渲染循环中切换回主上下文后,将Skia的纹理绘制到默认帧缓冲区即可。

3. 移除无效的SDL视频驱动设置

删除sdl.SetHint(Sdl.HintVideodriver, "x11")一行,让SDL自动选择当前系统的正确视频驱动,避免上下文初始化的隐性错误。

4. 确保上下文切换后的状态正确性

每次切换上下文后,重新绑定必要的OpenGL资源(如帧缓冲区、纹理),避免因上下文切换导致的状态丢失或错误引用。


内容的提问来源于stack exchange,提问作者itzjackyscode

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 15:12:07