I have an F# windowing system built on top of GLFW that allows easy creation and management of multiple windows. The rendering is done via Skia (specifically SkiaSharp). Recently, I moved the rendering for each window from being synchronous (i.e., occurring on the main/UI thread along with the window management) to a unique thread for each window. With this setup, each window gets a unique, standalone thread that completely handles the rendering.
However, moving the rendering to a thread has introduced some jitter in the shapes drawn and also some flickering of the canvas (sometimes going black). Below are some videos of the behavior.
(Note: the graininess of the GIFs is due to the video to GIF conversion process to make everything small enough to upload.)
Single threaded window management and rendering, all on the same thread:
Multithreaded environment, with window management on the main/UI thread and rendering in a single, unique thread:
The code is effectively identical between the two situations outside of the threaded behavior.
Even though the rendering is occurring on another thread, when the refresh window or framebuffer resize callbacks are called, I make synchronous calls to regenerate the canvas (recreate the Skia surface with the new framebuffer size, redraw the canvas, and then swap buffers) before allowing the callbacks to finish. Even with that, this jitter behavior still persists.
Curiously enough, if I add a sleep of like 1s inside the render loop after recreating the surface and swapping buffers, this seems to get rid of this. It almost seems like there’s some multithreaded issue with the context/rendering functions that allows a resize to occur while a callback is being executed and/or before a buffer swap begins or completes. Or it seems possible that the swap buffers call is returning too early, before it has finished with the buffer swap.
Here’s my callback setup:
member private this.DefaultWindowRefreshCallback (_window: GLFWWindow) =
if _debugFlag then printfn "%s refreshed" _title
let width, height = getFrameBufferSize _window
_framebufferWidth <- width
_framebufferHeight <- height
this.RegenerateCanvas()
member private this.DefaultFramebufferSizeCallback (GLFWWindow(_window), width: int, height: int) =
if _debugFlag then printfn "%s framebuffer resized: (width, height) = (%d, %d)" _title width height
_framebufferWidth <- uint16 width
_framebufferHeight <- uint16 height
this.RegenerateCanvas()
The RegenerateCanvas
call makes a synchronous call in both situations, whether the actual rendering occurs on another thread or not. In the threaded cases, a message is sent and a reply is awaited, where the reply is sent after the buffers are swapped. Here’s the code that does the framebuffer resize and drawing in both situations. GenerateCanvas
is called first and then Draw
.
member this.GenerateCanvas bundle =
if _debugFlag then printfn "Generating new canvas..."
//printfn "generating canvas %A" renderState.Canvases
this._bundle <- bundle
// Dispose of existing canvas
if this._surface <> null then this._surface.Dispose()
if this._backendRenderTarget <> null then this._backendRenderTarget.Dispose()
// Create new canvas
this._backendRenderTarget <- new GRBackendRenderTarget(int this._bundle.FramebufferWidth,
int this._bundle.FramebufferHeight,
this._bundle.Samples,
this._bundle.StencilBits,
this._framebufferInfo)
this._surface <- SKSurface.Create(this._grContext,
this._backendRenderTarget,
GRSurfaceOrigin.BottomLeft,
SKColorType.Rgba8888)
this._canvas <- this._surface.Canvas
member this.Draw () =
_drawFunction this._canvas
this._canvas.Flush()
swapBuffers _window
The bundle
contains the framebuffer width and height. The _drawFunction
draws the shape at static positions (i.e., does not change between render frames).
Polling versus waiting for events doesn’t seem to affect this. Changing the swap interval between 1 and 0 seems to have some effect, the latter reducing the frequency of the jitter but not removing it.
I am thinking that there may be a possible issue with the timing relationship between when resizes on the framebuffer are allowed, the relevant callbacks, and the swap buffer function.
Thank you for any help on this! I have read many posts here in the GLFW forum and also posts in Stack Overflow, but I have not been able to figure this out.