mono / SkiaSharp

SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library. It provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.
MIT License
4.14k stars 522 forks source link

[BUG] Drawing is warped during window resize on NVIDIA graphics card #2832

Open bmitc opened 1 month ago

bmitc commented 1 month ago

Description

I have a project where I am using SkiaSharp to draw to a window provided by GLFW. In the code below, I have reproduced the issue with just Silk.NET's GLFW bindings (whereas I am normally using my own bindings) and SkiaSharp. The code is not doing anything fancy. It simply draws a circle while the window is open, and during a resize, the entire surface is recreated and redrawn.

The issue is that when the window is resized, the drawing is stretched or compressed during the resize. It isn't clear what is causing this, as the entire surface is recreated during the resize callback and the drawing redrawn, and the callback is blocking. In this case, only a constant sized circle in a constant location is drawn, so it should be impossible for the drawing to contain anything other than a circle. However, the circle is often stretched into an ellipse in either or both directions during a window resize.

What's curious is that this does not occur when integrated Intel graphics are used. It only seems to occur when NVIDIA graphics are used. Thus, it is unclear whether this is a SkiaSharp problem, a Skia problem, whatever backend graphics API is being chosen by Skia or SkiaSharp, or an NVIDIA driver problem. I have searched, and it seems pathological resize behavior has occurred in various forms in all three contexts.

This issue prevents any proper resize functionality in a window.

Code

Attached is a .zip of the source file and project. Below is a copy of the source file. This issue occurs on any version of SkiaSharp I have tried, including the latest stable 2.88.8.

SilkNETGLFWWindowTest.zip

open FSharp.NativeInterop
open Silk.NET.GLFW
open SkiaSharp

#nowarn "9"

let initialWidth, initialHeight = 500, 500
let mutable framebufferWidth, framebufferHeight = initialWidth, initialHeight

let glfw = Glfw.GetApi()
glfw.Init() |> printfn "Initialized?: %A"

// Uncomment these window hints if on macOS
//glfw.WindowHint(WindowHintInt.ContextVersionMajor, 3)
//glfw.WindowHint(WindowHintInt.ContextVersionMinor, 3)
//glfw.WindowHint(WindowHintBool.OpenGLForwardCompat, true)
//glfw.WindowHint(WindowHintOpenGlProfile.OpenGlProfile, OpenGlProfile.Core)

glfw.WindowHint(WindowHintInt.Samples, 0)
glfw.WindowHint(WindowHintInt.StencilBits, 1)
glfw.WindowHint(WindowHintBool.DoubleBuffer, true)
glfw.WindowHint(WindowHintBool.Focused, false)
glfw.WindowHint(WindowHintBool.Maximized, false)
glfw.WindowHint(WindowHintBool.Visible, true)

let window = glfw.CreateWindow(initialWidth, initialHeight, "Test Window", NativePtr.ofNativeInt 0n, NativePtr.ofNativeInt 0n)
printfn "Window: %A" window
glfw.MakeContextCurrent(window)
let mutable error = nativeint<byte> 1uy |> NativePtr.ofNativeInt
glfw.GetError(&error) |> printfn "Error: %A"

let grGlInterface = GRGlInterface.Create(fun name -> glfw.GetProcAddress name)

if not (grGlInterface.Validate()) then
    raise (System.Exception("Invalid GRGlInterface"))

let grContext = GRContext.CreateGl(grGlInterface)
let grGlFramebufferInfo = new GRGlFramebufferInfo(0u, SKColorType.Rgba8888.ToGlSizedFormat()) // 0x8058

let draw(window, width, height) =
    glfw.PollEvents()
    grContext.ResetContext()

    let grBackendRenderTarget = new GRBackendRenderTarget(width, height, 1, 0, grGlFramebufferInfo)
    let surface = SKSurface.Create(grContext, grBackendRenderTarget, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888)
    let canvas = surface.Canvas

    canvas.Clear(SKColors.LightBlue)
    let red = new SKPaint(Color = SKColors.Black)
    canvas.DrawCircle(float32(300)/2.0f, float32(300)/2.0f, 100.0f, red)
    canvas.Flush()

    glfw.SwapBuffers(window)

    red.Dispose()
    surface.Dispose()
    grBackendRenderTarget.Dispose()

let resizeCallbackFun window width height = draw(window, width, height)
glfw.SetFramebufferSizeCallback(window, resizeCallbackFun) |> ignore

while glfw.WindowShouldClose window <> true do
    glfw.GetFramebufferSize(window, &framebufferWidth, &framebufferHeight)
    draw(window, framebufferWidth, framebufferHeight)

glfw.DestroyWindow window
glfw.Terminate()

Expected Behavior

I expected that the resize happens as it does in the integrated Intel graphics situation, where the image drawn inside the main loop and resize callback is always as specified and not warped.

Actual Behavior

Resizing with integrated Intel graphics

https://github.com/mono/SkiaSharp/assets/65685447/10333812-40fc-4aed-8910-f31afe935d0f

Resizing with NVIDIA graphics

https://github.com/mono/SkiaSharp/assets/65685447/bf94c867-fba3-4669-a6f8-f59405e9e69f

Version of SkiaSharp

3.x (Alpha)

Last Known Good Version of SkiaSharp

Other (Please indicate in the description)

IDE / Editor

Visual Studio (Windows)

Platform / Operating System

Windows

Platform / Operating System Version

I am running Windows 11. I currently can't run SkiaSharp on Linux, and I don't have a macOS machine to test on.

winver returns Version 23H2 (OS Build 22631.3447).

Devices

Relevant Screenshots

You can see in this screenshots how the circle is stretched. The issue seems to be related to a race condition somewhere. In the underlying stack, as the only time a circle is drawn in the top-level code is inside the draw function, and it's a constant circle in the same location every time, so the stretching is being performed by something else.

canvas.DrawCircle(float32(300)/2.0f, float32(300)/2.0f, 100.0f, red)

image

image

Relevant Log Output

No response

Code of Conduct