Below is a demo/test for VSYNC loss and tearing that can be seen even with some double buffered screens. This demo/test might be handy if you suspect a VSYNC/tearing problem despite using a double buffered screen. Sweeping Vertical bars make any tearing rather obvious. Total frame times shown should be multiples of 16.6 ms, if not then VSYNC has been lost.
VSYNC loss and tearing show up on my platform (2400SK 075.05E00430A) as soon as the animation time exceeds 16mS (use left/right buttons to adjust the animation time). On "giga" platforms this VSYNC loss is apparantly due to a hack to fix something on the home screen.
The demo code includes a workaround (which complicates it a bit): Drawing is done on a pair of off-screen buffers so that the "front" buffer can be pushed to the screen every 16ms even if the frame animation is still in progress on the "back" buffer. This keeps the VSYNC "alive". This workaround can be enabled by the "OK/Select" button.
'
' Demonstration of loss of VSYNC if animation takes longer than 16 mS.
' ...and tears - was not expecting that as well.
'
' Affects Giga platforms
'
' - left/right buttons adjust update time +/-1 ms
' - select button toggles swap on every field
'
Library "v30/bslCore.brs"
sub main ()
' Initialise the usual resources
'
ecodes = bslUniversalControlEventCodes()
clk = CreateObject("roTimespan")
port = CreateObject("roMessagePort")
screen = CreateObject("roScreen", TRUE) ' dbl buffered
screen.SetMessagePort(port)
' Two full screen bitmaps so we can push the front one
' as many times as we need while preparing the back one
'
' This is needed to prevent the loss of sync on giga platforms
' that happens if we fail to swap every 16mS!
'
bm_props = {width:screen.getWidth(),height:screen.getHeight()}
bms = [
createobject("roBitmap", bm_props),
createobject("roBitmap", bm_props)
]
back = bms[0]
front = bms[1]
' Let's have a font to display the timings on screen
'
font = CreateObject("roFontRegistry").GetDefaultFont()
txt_height = font.getOneLineHeight()
txt_color = &h60ff60ff
txt_margin = 100
' Animated stripes to show tearing or jumps.
'
stripes = []
for x = 0 to screen.getWidth()-1 step screen.getWidth()/8
stripes.push(VerticalStripe(screen.getWidth(), screen.getHeight(), x))
end for
' Artificial animation time.
' This is adjusted using LEFT/RIGHT buttons to explore the
' frame sync problem.
'
animation_ms = 14
swap_on_every_field = FALSE ' force swap after 15mS to fix VSYNC problem
' Frame timing data.
'
' The filter is just a luxury - but its reassuring to see
' it settle close to 16,666 uSecs in the 60FPS regime!
'
last_frame_start = INVALID ' monitor inter-frame period
frame_avg_filter = jcEWMAFilter(0.995, 1000/60)
' Clear working buffers and push to prime the pump.
'
back.clear(0)
front.clear(0)
screen.drawObject(0,0, front)
screen.SwapBuffers()
screen.drawObject(0,0, front)
' Spin around SwappingBuffers and monitoring
' how long each cycle takes.
'
while TRUE
' Flip display buffers
'
screen.SwapBuffers()
frame_start = clk.TotalMilliseconds()
' Handle async events without blocking:
' - left/right buttons adjust update time +/-1 ms
' - option & select button toggles swap on every field
'
msg = port.GetMessage()
if type(msg)="roUniversalControlEvent" then
ecode = msg.GetInt()
if ecode = ecodes.BUTTON_RIGHT_PRESSED then
animation_ms = animation_ms + 1
else if ecode = ecodes.BUTTON_LEFT_PRESSED then
animation_ms = animation_ms - 1
else if ecode = ecodes.BUTTON_INFO_PRESSED or ecode = ecodes.BUTTON_SELECT_PRESSED then
swap_on_every_field = not swap_on_every_field
end if
end if
' Paint to back buffer
'
back.clear(0)
' Animate & paint the vertical stripes.
'
for each stripe in stripes
stripe.animate(back)
end for
' Compute & paint frame timings.
'
' The 1ms resolution measurements _should_ dither around
' the vsync time (16.6mSecs) or a multiple thereof.
' I.e. 16-17, 33-34 ...
'
if last_frame_start <> INVALID then
' Calc
last_frame_ms = frame_start - last_frame_start
avg_ms = frame_avg_filter.update(last_frame_ms)
avg_us = int(1000 * avg_ms)
if swap_on_every_field then
vsync_fix = "ON"
else
vsync_fix = "OFF"
endif
' Draw
x = txt_margin
y = txt_margin
back.drawText("Vertical Synchronisation Loss Demo", x, y, txt_color, font)
y = y + txt_height
back.drawText("Animation time: " + animation_ms.toStr() + " ms (Left = -1, Right = +1)", x, y, txt_color, font)
y = y + txt_height
back.drawText("VSYNC fix: " + vsync_fix + " (OK = Toggle)", x, y, txt_color, font)
y = y + txt_height
back.drawText("Frame time: " + last_frame_ms.toStr() + " ms (Average: " + avg_us.toStr() + " us)", x, y, txt_color, font)
endif
last_frame_start = frame_start
' Simulate lengthy animation.
'
' Optionally push front to screen every time VSYNC aproaches
' to keep VSYNC enabled. This may be only needed on Giga platforms?
'
field_start = frame_start
while TRUE
now = clk.TotalMilliseconds()
if now - frame_start >= animation_ms then
exit while
end if
if swap_on_every_field and now - field_start >= 16 then
screen.drawObject(0,0, front)
screen.SwapBuffers()
field_start = clk.TotalMilliseconds()
end if
end while
' Flip working buffers and push new front to screen
'
tmp = front
front = back
back = tmp
screen.drawObject(0,0, front)
end while
end sub
' Vertical White Stripe that sweeps the canvas
' to show up tearing and animation jumps.
'
function VerticalStripe (cw as Integer, ch as Integer, x0 as Integer) as Object
this = {}
this.cw = cw
this.x = x0
this.w = 40
this.h = ch
this.dx = 4
this.clr = &h808080ff
this.animate = function (canvas as Object) as Void
' paint & animate in one call - this is demo only
this = m
this.x = this.x + this.dx
if this.x > this.cw then
this.x = this.x - (this.cw + this.w)
end if
canvas.drawRect(this.x, 0, this.w, this.h, this.clr)
end function
return this
end function
' Exponentially Weighted Moving Average (EWMA) filter
' Yt = Xt + α ( Yt-1 - Xt)
'
function jcEWMAFilter(alpha as Float, avg0 as Float) as Object
this = {}
this.alpha = alpha
this.avg = avg0
' Insert new sample, returns updated average.
'
this.update = function (x as Float) as Float
this = m
this.avg = x + (this.alpha * (this.avg - x))
return this.avg
end function
return this
end function