Performance issue when drawing a nine slice for the first time

I’ve noticed a performance problem with nine slices on actual hardware. It happens any time the nine slice is drawn at a particular size for the first time. The results in the sprite’s animation being jerky. After the first hiccup, it’s fine… unless I need to draw the nine slice at a different size. In my game that happens pretty often.

Steps to reproduce:

  • Run the sample project on actual hardware.
  • Press the A button to make a text box appear. Note that the animation is a bit jerky.
  • Press the B button to dismiss the text box.
  • Press the A button again. Note that the animation is smooth now.

The animation will continue to be smooth unless the size of the box is changed.

So far the best workaround I’ve found is to draw the nine slice into an image, then draw that image in the draw() method instead of the slice. You can set the ENABLE_WORKAROUND flag to true to test this workaround. With this fix in place, the animation is always smooth.

One other thing worth noting is that this only seems to be a problem at larger sizes. At 100x100 or even 200x100 I can't notice an issue.

This bug took me a while to track down and I’m a bit surprised by it! I figured drawing a nine slice would perform pretty well. Is there anything else I can do to get this to perform better, or should I stick to my current workaround?

NineSliceDelay.zip (10.3 KB)

Here's the textbox.lua file, where all the relevant code is:

local gfx <const> = playdate.graphics

local ENABLE_WORKAROUND = false

class('TextBox').extends(gfx.sprite)

function TextBox:init()
	TextBox.super.init(self)
	self.backgroundSlice = gfx.nineSlice.new("box.png", 10, 10, 1, 1)
	self.backgroundImage = nil
	self.isPreparing = false
	self.isShowing = false
	self:setVisible(false)
	self:setCenter(0, 0)
	self:setSize(340, 200)
	self:moveTo(30, 240)
end

function TextBox:draw()
	if ENABLE_WORKAROUND then
		if self.backgroundImage then
			self.backgroundImage:draw(0, 0)
		end
	else
		gfx.setImageDrawMode(gfx.kDrawModeCopy)
		self.backgroundSlice:drawInRect(0, 0, self.width, self.height)
	end
end

function TextBox:update()
	local animator = self.animator
	if animator then
		if animator:ended() then
			self.animator = nil
			if self.isShowing == false then
				self:setVisible(false)
			end
		else
			local y = animator:currentValue()
			self:moveTo(self.x, y)
		end
	elseif self.isPreparing then
		self.isPreparing = false
		self.isShowing = true
		self.animator = gfx.animator.new(500, 240, 20,
			playdate.easingFunctions.outBack)
	end
end

function TextBox:processInput()
	if playdate.buttonJustPressed(playdate.kButtonA) or
		playdate.buttonJustPressed(playdate.kButtonB) then
		self:hide(true)
	end
end

function TextBox:show()

	if ENABLE_WORKAROUND then
		local width, height = self:getSize()
		local image = gfx.image.new(width, height)
		gfx.lockFocus(image)
		self.backgroundSlice:drawInRect(0, 0, width, height)
		gfx.unlockFocus(image)
		self.backgroundImage = image
	end

	self.isPreparing = true
	self:moveTo(30, 240)
	self:setVisible(true)

end

function TextBox:hide(animated)
	self.isShowing = false
	if animated then
		self.animator = gfx.animator.new(300, self.y, 360,
			playdate.easingFunctions.inBack)
	else
		self:setVisible(false)
	end
end