Windows下多上下文场景中SkiaSharp触发访问违规问题排查
我正在为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); }
请问这类段错误的成因是什么?该如何避免?
成因分析
Windows OpenGL上下文资源隔离限制
Windows下的OpenGL上下文对跨上下文资源访问的限制远严于Linux。你创建了两个独立的OpenGL上下文(mainCtx和skiaCtx)但未启用资源共享,Skia绑定到skiaCtx后尝试访问主上下文的默认帧缓冲区时,会因资源不可见触发访问违规。Linux的OpenGL实现对这类操作的限制更宽松,因此无报错。帧缓冲区归属不匹配
创建Skia表面时使用的GRGlFramebufferInfo指定了默认帧缓冲区(ID 0),但该帧缓冲区属于mainCtx。在skiaCtx中直接访问不属于它的帧缓冲区,Windows驱动会直接拒绝这种越权操作,导致段错误。无效的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

