Simulator Crashes Due to setStencilImage

MacOS 12.6, Playdate SDK 1.12.3

I'm having an issue where the playdate simulator crashes at seemingly random points. Here is a gif of one such crash:

crash-video

I have determined that the crashing is due to calling setStencilImage. When I remove the two lines marked "-- PROBLEM!" in the code below, the program runs without crashing. I've stared at the docs and the logic of my code seems airtight. I've checked which key is being set to which stencil a million times and I swear it's always right, but sometimes a stencil is not displayed (instead displaying a solid fill) and sometimes a glitchy-looking partial stencil is displayed. Sometimes the program just crashes without any visual anomaly. When I wrote all this code, about 8 months ago, it all worked perfectly. Then I updated the Playdate SDK (I forget from which version to which), and it stopped working and has remained buggy since. Is this a Playdate problem with stenciling, or is there something about stencils I don't know? Also, any tips or resources on reading apple crash reports for Playdate development?

Here is all my code:

import "CoreLibs/object"
import "CoreLibs/graphics"
import "CoreLibs/sprites"
import "CoreLibs/timer"

local gfx <const> = playdate.graphics
local snd = playdate.sound

-- track the currently selected note
local crankChangePerNote <const> = 25
local currentNoteRaw = 48.5
local currentNote = math.floor(currentNoteRaw)

-- track which notes are active
local activeNotes = {
	true,false,true,false,true, -- C,D,E
	false,false,
	true,false,true,false,false -- G,A
}





--`--`--`--`--`--`--`--`-- HELPER --`--`--`--`--`--`--`--`--

-- helper function: is the given note active?
function isOn(note)
	local noteWithinOctave = note % 12
	return activeNotes[noteWithinOctave+1]
end

-- helper function: return which octave the given note is in (each octave starts with C)
function octaveOf(note)
	return math.floor(note/12)
end





--`--`--`--`--`--`--`--`-- INPUT --`--`--`--`--`--`--`--`--

-- respond to crank input
function playdate.cranked(change, acceleratedChange)
	currentNoteRaw += change/crankChangePerNote
	
	-- bound currentNoteRaw within the min and max MIDI note values
	if currentNoteRaw < 0 then
		currentNoteRaw = 0
	elseif currentNoteRaw > 127 then
		currentNoteRaw = 127
	end
	
	-- if cranking up, look for the nearest active note above; otherwise, look below
	local start, finish, increment, shouldChange
	if change > 0 then
		start = currentNote+1
		finish = currentNote+12
		increment = 1
		shouldChange = (currentNoteRaw > currentNote+1) -- a boolean
	else
		start = currentNote-1
		finish = currentNote-12
		increment = -1
		shouldChange = (currentNoteRaw < currentNote) -- a boolean
	end

	if shouldChange then
		for newNote = start, finish, increment do
			local noteWithinOctave = newNote % 12
			-- when we find the next active note...
			if activeNotes[noteWithinOctave+1] then
				-- set the currentNote to be the new one
				currentNote = newNote
				-- skip over any inactive notes, moving currentNoteRaw to currentNote,
				-- but preserving its decimal part
				local decimalPart = currentNoteRaw - math.floor(currentNoteRaw)
				currentNoteRaw = currentNote + decimalPart
				break
			end
		end
		
		playNote(currentNote)
		showNote(currentNote, true)
	end
	

end

-- when the left button is pressed, move down one semitone
function playdate.leftButtonDown()
	currentNote -= 1
	currentNoteRaw = currentNote
	-- playNote(currentNote)
	showNote(currentNote, isOn(currentNote))
end

-- when the right button is pressed, move up one semitone
function playdate.rightButtonDown()
	currentNote += 1
	currentNoteRaw = currentNote
	-- playNote(currentNote)
	showNote(currentNote, isOn(currentNote))
end

-- when the A button is pressed, toggle the current note
-- display the change
function playdate.AButtonDown()
	local noteWithinOctave = currentNote % 12
	if activeNotes[noteWithinOctave+1] then
		activeNotes[noteWithinOctave+1] = false
		setMarkerForKey(keySprites[noteWithinOctave+1], "current")
	else
		activeNotes[noteWithinOctave+1] = true
		setMarkerForKey(keySprites[noteWithinOctave+1], "currentAndOn")
		playNote(currentNote)
	end
end





--`--`--`--`--`--`--`--`-- SOUND --`--`--`--`--`--`--`--`--

-- set up the synth
local synth = snd.synth.new(snd.kWaveSquare)
synth:setADSR(0,0,1,0)

local synth2 = snd.synth.new(snd.kWaveSine)
synth:setADSR(0,0,1,0)

local filter = snd.twopolefilter.new(snd.kFilterLowPass)
filter:setFrequency(1000)

local lfo = snd.lfo.new(snd.kLFOSine)
lfo:setDepth(0.01)
lfo:setRate(5)

local filterEnv = snd.envelope.new(0.2,0.5,0,0.5)
filterEnv:setScale(0.5)
filterEnv:setOffset(0)
filter:setFrequencyMod(filterEnv)

-- filter:setFrequencyMod(lfo)

local chan = snd.channel.new()
chan:addSource(synth)
chan:addSource(synth2)
chan:addEffect(filter)

-- play the given note
function playNote(note)
	
	synth:playMIDINote(note, 1, 0.8, 0)
	synth2:playMIDINote(note-12, 1, 0.8, 0)
	-- filterEnv:trigger(1)
	
end





--`--`--`--`--`--`--`--`-- GRAPHICS (ONGOING) --`--`--`--`--`--`--`--`--

-- using dithering patterns, display the given key's sprite
-- as turned on, current, or both
function setMarkerForKey(spr, marker)
	if marker == "on" then
		local lightDither = gfx.image.new("images/light-dither.png")
		spr:setStencilImage(lightDither, true) -- PROBLEM!
	elseif marker == "current" then
		local darkDither = gfx.image.new("images/dark-dither.png")
		spr:setStencilImage(darkDither, true) -- PROBLEM!
	elseif marker == "currentAndOn" then
		spr:clearStencil()
	end
end

-- show the given note
function showNote(note, noteIsOn)
	-- find the sprite for the given note
	local noteInOctave = note % 12
	local spr = keySprites[noteInOctave+1]

	-- display the old note we just left properly
	if(isOn(prevNote)) then
		setMarkerForKey(prevSpr, "on")
	else
		prevSpr:remove()
	end

	-- display the new note appropriately
	if noteIsOn then
		setMarkerForKey(spr, "currentAndOn")
	else
		setMarkerForKey(spr, "current")
	end
	spr:add()
	
	-- the new note will be the previous note next time around...
	prevNote = note
	prevSpr = spr
end

function playdate.update()
	
	gfx.sprite.update()
	playdate.timer.updateTimers()
	
end





--`--`--`--`--`--`--`--`-- GRAPHICS (ONCE) --`--`--`--`--`--`--`--`--

-- set the keyboard as the background
local backgroundImage = gfx.image.new( "images/keyboard.png" )
gfx.sprite.setBackgroundDrawingCallback(
	function( x, y, width, height )
		gfx.setClipRect( x, y, width, height ) -- let's only draw the part of the screen that's dirty
		backgroundImage:draw( 0, 0 )
		gfx.clearClipRect() -- clear so we don't interfere with drawing that comes after this
	end
)

-- load images files into the key sprites, which show the user
-- which keys are active and which key is the current one
local cImage = gfx.image.new("images/C.png")
local dbImage = gfx.image.new("images/Db.png")
local dImage = gfx.image.new("images/D.png")
local ebImage = gfx.image.new("images/Eb.png")
local eImage = gfx.image.new("images/E.png")
local fImage = gfx.image.new("images/F.png")
local gbImage = gfx.image.new("images/Gb.png")
local gImage = gfx.image.new("images/G.png")
local abImage = gfx.image.new("images/Ab.png")
local aImage = gfx.image.new("images/A.png")
local bbImage = gfx.image.new("images/Bb.png")
local bImage = gfx.image.new("images/B.png")

keySprites = {
	gfx.sprite.new( cImage ),
	gfx.sprite.new( dbImage ),
	gfx.sprite.new( dImage ),
	gfx.sprite.new( ebImage ),
	gfx.sprite.new( eImage ),
	gfx.sprite.new( fImage ),
	gfx.sprite.new( gbImage ),
	gfx.sprite.new( gImage ),
	gfx.sprite.new( abImage ),
	gfx.sprite.new( aImage ),
	gfx.sprite.new( bbImage ),
	gfx.sprite.new( bImage ),
}

-- the {x,y} screen positions of the upper-left corners of the keys
local keyPositions = {
	{26,47},  -- C
	{50,54},  -- Db
	{65,47},  -- D
	{98,54},  -- Eb
	{103,47}, -- E
	{141,47}, -- F
	{165,54}, -- Gb
	{180,47}, -- G
	{208,54}, -- Ab
	{218,47}, -- A
	{251,54}, -- Bb
	{256,47}, -- B
}

-- move the keys to their positions as listed in keyPositions
-- also, display all the active keys with the "on" marker (dithering pattern)
for i = 1, 12, 1 do
	local spr = keySprites[i]
	local pos = keyPositions[i]
	spr:setCenter(0,0)
	spr:moveTo(pos[1], pos[2])
	
	if(isOn(i-1)) then
		local spr = keySprites[i]
		setMarkerForKey(spr, "on")
		spr:add()
	end
end

-- these will track the previous note/sprite,
-- so we can reset it when we leave it
-- display the current (selected) key as such
prevNote = currentNote
prevSpr = keySprites[(prevNote % 12) + 1]
setMarkerForKey(prevSpr, "currentAndOn")

And here is the crash log for the gif above:

{"app_name":"Playdate Simulator","timestamp":"2022-11-10 15:43:39.00 -0800","app_version":"1.12.3","slice_uuid":"bd5e55f7-1461-349b-adc5-e75dff204a61","build_version":"140884","platform":1,"bundleID":"date.play.simulator","share_with_app_devs":0,"is_first_party":0,"bug_type":"309","os_version":"macOS 12.6 (21G115)","incident_id":"690183E6-102F-4A20-95C5-64C10CB72CC4","name":"Playdate Simulator"}
{
  "uptime" : 93000,
  "procLaunch" : "2022-11-10 15:43:32.5363 -0800",
  "procRole" : "Foreground",
  "version" : 2,
  "userID" : 502,
  "deployVersion" : 210,
  "modelCode" : "MacBookPro16,1",
  "procStartAbsTime" : 93023662035971,
  "coalitionID" : 11777,
  "osVersion" : {
    "train" : "macOS 12.6",
    "build" : "21G115",
    "releaseType" : "User"
  },
  "captureTime" : "2022-11-10 15:43:38.8732 -0800",
  "incident" : "690183E6-102F-4A20-95C5-64C10CB72CC4",
  "bug_type" : "309",
  "pid" : 22180,
  "procExitAbsTime" : 93029996937660,
  "cpuType" : "X86-64",
  "procName" : "Playdate Simulator",
  "procPath" : "\/Users\/USER\/*\/Playdate Simulator.app\/Contents\/MacOS\/Playdate Simulator",
  "bundleInfo" : {"CFBundleShortVersionString":"1.12.3","CFBundleVersion":"140884","CFBundleIdentifier":"date.play.simulator"},
  "storeInfo" : {"deviceIdentifierForVendor":"6389BE61-52BF-55B7-BB4E-6E9BDDD14805","thirdParty":true},
  "parentProc" : "launchd",
  "parentPid" : 1,
  "coalitionName" : "date.play.simulator",
  "crashReporterKey" : "DCEDB760-EA14-5175-DB14-7F32E9431245",
  "wakeTime" : 9788,
  "bridgeVersion" : {"build":"19P6067","train":"6.6"},
  "sleepWakeUUID" : "B7C101AB-A26F-42D8-8B13-6B70E79E0A17",
  "sip" : "enabled",
  "isCorpse" : 1,
  "exception" : {"codes":"0x0000000000000001, 0x0000000000000000","rawCodes":[1,0],"type":"EXC_ARITHMETIC","signal":"SIGFPE"},
  "termination" : {"flags":0,"code":8,"namespace":"SIGNAL","indicator":"Floating point exception: 8","byProc":"exc handler","byPid":22180},
  "extMods" : {"caller":{"thread_create":0,"thread_set_state":0,"task_for_pid":0},"system":{"thread_create":0,"thread_set_state":0,"task_for_pid":0},"targeted":{"thread_create":0,"thread_set_state":0,"task_for_pid":0},"warnings":0},
  "faultingThread" : 0,
  "threads" : [{"triggered":true,"id":1291419,"instructionState":{"instructionStream":{"bytes":[64,7,69,133,192,65,15,73,192,193,248,3,72,99,240,233,232,0,0,0,137,208,153,247,249,76,141,110,16,65,246,198,8,116,4,77,139,109,0,15,183,70,4,76,99,250,76,15,175,248,79,141,20,47,15,183,14,73,137,204,73,193,236,5,69,141,88,31,69,133,192,69,15,73,216,65,193,251,5,65,15,186,230,8,15,130,172,0,0,0,137,206,68,137,200,153,247,249,68,141,74,31,133,210,68,15,73,202,65,193,249,5,131,225,31,102,131,254,32,15,147,194,65,131,248,32,15,157,192,32,208,65,246,198,32,15,133,189,0,0,0,49,210,132,192,116,39,73,99,209,68,137,230,73,99,195,73,141,20,151,73,1,213,49,210,65,139,92,149,0,33,28,151,72,255,194,72,57,242,115,5,72,57,194,124,235,69,57,227,126,33,102],"offset":96}},"threadState":{"r13":{"value":105553146675216},"rax":{"value":208},"rflags":{"value":66050},"cpu":{"value":2},"r14":{"value":0},"rsi":{"value":0},"r8":{"value":32},"cr2":{"value":4431618048},"rdx":{"value":0},"r10":{"value":105553148611522},"r9":{"value":208},"r15":{"value":1936306},"rbx":{"value":140388900023868},"trap":{"value":0},"err":{"value":0},"r11":{"value":1},"rip":{"value":4485961114,"matchesCrashFrame":1},"rbp":{"value":140701863450704},"rsp":{"value":140701863450656},"r12":{"value":0},"rcx":{"value":0},"flavor":"x86_THREAD_STATE","rdi":{"value":140701863450720}},"queue":"com.apple.main-thread","frames":[{"imageOffset":628122,"symbol":"_applyStencil","symbolLocation":145,"imageIndex":0},{"imageOffset":626466,"symbol":"LCDBitmap_drawBitmap","symbolLocation":1175,"imageIndex":0},{"imageOffset":649983,"symbol":"LCD_drawBitmap","symbolLocation":82,"imageIndex":0},{"imageOffset":691795,"symbol":"LCDSprite_draw","symbolLocation":161,"imageIndex":0},{"imageOffset":646016,"symbol":"LCDDisplayList_drawScreenRect","symbolLocation":991,"imageIndex":0},{"imageOffset":644628,"symbol":"LCDDisplayList_draw","symbolLocation":1753,"imageIndex":0},{"imageOffset":518367,"symbol":"_pc_sprite_updateSprites","symbolLocation":30,"imageIndex":0},{"imageOffset":189096,"symbol":"luaD_precall","symbolLocation":487,"imageIndex":0},{"imageOffset":202609,"symbol":"luaV_execute","symbolLocation":1245,"imageIndex":0},{"imageOffset":189459,"symbol":"ccall","symbolLocation":85,"imageIndex":0},{"imageOffset":185867,"symbol":"luaD_rawrunprotected","symbolLocation":87,"imageIndex":0},{"imageOffset":189733,"symbol":"lua_resume","symbolLocation":233,"imageIndex":0},{"imageOffset":538154,"symbol":"lua_execOnThread","symbolLocation":359,"imageIndex":0},{"imageOffset":538812,"symbol":"_callPlaydateFunction","symbolLocation":147,"imageIndex":0},{"imageOffset":562490,"symbol":"updateLua","symbolLocation":177,"imageIndex":0},{"imageOffset":563384,"symbol":"pd_update","symbolLocation":422,"imageIndex":0},{"imageOffset":940404,"symbol":"-[PCPlaydateSimulator update]","symbolLocation":632,"imageIndex":0},{"imageOffset":529703,"symbol":"__NSThreadPerformPerform","symbolLocation":179,"imageIndex":1},{"imageOffset":524667,"symbol":"__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__","symbolLocation":17,"imageIndex":2},{"imageOffset":524515,"symbol":"__CFRunLoopDoSource0","symbolLocation":180,"imageIndex":2},{"imageOffset":523869,"symbol":"__CFRunLoopDoSources0","symbolLocation":242,"imageIndex":2},{"imageOffset":518264,"symbol":"__CFRunLoopRun","symbolLocation":892,"imageIndex":2},{"imageOffset":515644,"symbol":"CFRunLoopRunSpecific","symbolLocation":562,"imageIndex":2},{"imageOffset":189926,"symbol":"RunCurrentEventLoopInMode","symbolLocation":292,"imageIndex":3},{"imageOffset":189258,"symbol":"ReceiveNextEventCommon","symbolLocation":594,"imageIndex":3},{"imageOffset":188645,"symbol":"_BlockUntilNextEventMatchingListInModeWithFilter","symbolLocation":70,"imageIndex":3},{"imageOffset":257965,"symbol":"_DPSNextEvent","symbolLocation":927,"imageIndex":4},{"imageOffset":251498,"symbol":"-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:]","symbolLocation":1394,"imageIndex":4},{"imageOffset":195865,"symbol":"-[NSApplication run]","symbolLocation":586,"imageIndex":4},{"imageOffset":15511,"symbol":"NSApplicationMain","symbolLocation":817,"imageIndex":4},{"imageOffset":21806,"symbol":"start","symbolLocation":462,"imageIndex":5}]},{"id":1291428,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]},{"id":1291430,"queue":"com.apple.root.default-qos","frames":[{"imageOffset":25422,"symbol":"kevent","symbolLocation":10,"imageIndex":7},{"imageOffset":177626,"symbol":"__36-[PDDeviceConnection watchForDevice]_block_invoke","symbolLocation":336,"imageIndex":0},{"imageOffset":8396,"symbol":"_dispatch_call_block_and_release","symbolLocation":12,"imageIndex":8},{"imageOffset":13079,"symbol":"_dispatch_client_callout","symbolLocation":8,"imageIndex":8},{"imageOffset":23036,"symbol":"_dispatch_queue_override_invoke","symbolLocation":787,"imageIndex":8},{"imageOffset":76482,"symbol":"_dispatch_root_queue_drain","symbolLocation":343,"imageIndex":8},{"imageOffset":78428,"symbol":"_dispatch_worker_thread2","symbolLocation":160,"imageIndex":8},{"imageOffset":12170,"symbol":"_pthread_wqthread","symbolLocation":256,"imageIndex":6},{"imageOffset":8023,"symbol":"start_wqthread","symbolLocation":15,"imageIndex":6}]},{"id":1291432,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]},{"id":1291434,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]},{"id":1291449,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]},{"id":1291450,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]},{"id":1291451,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]},{"id":1291452,"name":"com.apple.NSEventThread","frames":[{"imageOffset":6522,"symbol":"mach_msg_trap","symbolLocation":10,"imageIndex":7},{"imageOffset":7400,"symbol":"mach_msg","symbolLocation":56,"imageIndex":7},{"imageOffset":525165,"symbol":"__CFRunLoopServiceMachPort","symbolLocation":319,"imageIndex":2},{"imageOffset":518648,"symbol":"__CFRunLoopRun","symbolLocation":1276,"imageIndex":2},{"imageOffset":515644,"symbol":"CFRunLoopRunSpecific","symbolLocation":562,"imageIndex":2},{"imageOffset":1755598,"symbol":"_NSEventThread","symbolLocation":132,"imageIndex":4},{"imageOffset":25825,"symbol":"_pthread_start","symbolLocation":125,"imageIndex":6},{"imageOffset":8043,"symbol":"thread_start","symbolLocation":15,"imageIndex":6}]},{"id":1291459,"name":"AMCP Logging Spool","frames":[{"imageOffset":6582,"symbol":"semaphore_wait_trap","symbolLocation":10,"imageIndex":7},{"imageOffset":107238,"symbol":"caulk::mach::semaphore::wait_or_error()","symbolLocation":16,"imageIndex":9},{"imageOffset":8520,"symbol":"caulk::concurrent::details::worker_thread::run()","symbolLocation":36,"imageIndex":9},{"imageOffset":7692,"symbol":"void* caulk::thread_proxy<std::__1::tuple<caulk::thread::attributes, void (caulk::concurrent::details::worker_thread::*)(), std::__1::tuple<caulk::concurrent::details::worker_thread*> > >(void*)","symbolLocation":41,"imageIndex":9},{"imageOffset":25825,"symbol":"_pthread_start","symbolLocation":125,"imageIndex":6},{"imageOffset":8043,"symbol":"thread_start","symbolLocation":15,"imageIndex":6}]},{"id":1291502,"name":"com.apple.audio.toolbox.AUScheduledParameterRefresher","frames":[{"imageOffset":6582,"symbol":"semaphore_wait_trap","symbolLocation":10,"imageIndex":7},{"imageOffset":107238,"symbol":"caulk::mach::semaphore::wait_or_error()","symbolLocation":16,"imageIndex":9},{"imageOffset":8520,"symbol":"caulk::concurrent::details::worker_thread::run()","symbolLocation":36,"imageIndex":9},{"imageOffset":7692,"symbol":"void* caulk::thread_proxy<std::__1::tuple<caulk::thread::attributes, void (caulk::concurrent::details::worker_thread::*)(), std::__1::tuple<caulk::concurrent::details::worker_thread*> > >(void*)","symbolLocation":41,"imageIndex":9},{"imageOffset":25825,"symbol":"_pthread_start","symbolLocation":125,"imageIndex":6},{"imageOffset":8043,"symbol":"thread_start","symbolLocation":15,"imageIndex":6}]},{"id":1291532,"frames":[{"imageOffset":6522,"symbol":"mach_msg_trap","symbolLocation":10,"imageIndex":7},{"imageOffset":7400,"symbol":"mach_msg","symbolLocation":56,"imageIndex":7},{"imageOffset":1824314,"symbol":"exception_server_thread","symbolLocation":211,"imageIndex":0},{"imageOffset":25825,"symbol":"_pthread_start","symbolLocation":125,"imageIndex":6},{"imageOffset":8043,"symbol":"thread_start","symbolLocation":15,"imageIndex":6}]},{"id":1291545,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]},{"id":1291546,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]},{"id":1291547,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]},{"id":1291548,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]},{"id":1291549,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]},{"id":1291550,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]},{"id":1291551,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]},{"id":1291559,"name":"com.apple.audio.IOThread.client","frames":[{"imageOffset":6522,"symbol":"mach_msg_trap","symbolLocation":10,"imageIndex":7},{"imageOffset":7400,"symbol":"mach_msg","symbolLocation":56,"imageIndex":7},{"imageOffset":3341319,"symbol":"HALB_MachPort::SendSimpleMessageWithSimpleReply(unsigned int, unsigned int, int, int&, bool, unsigned int)","symbolLocation":111,"imageIndex":10},{"imageOffset":1812265,"symbol":"HALC_ProxyIOContext::IOWorkLoop()","symbolLocation":3931,"imageIndex":10},{"imageOffset":1806853,"symbol":"invocation function for block in HALC_ProxyIOContext::HALC_ProxyIOContext(unsigned int, unsigned int)","symbolLocation":63,"imageIndex":10},{"imageOffset":3692054,"symbol":"HALB_IOThread::Entry(void*)","symbolLocation":72,"imageIndex":10},{"imageOffset":25825,"symbol":"_pthread_start","symbolLocation":125,"imageIndex":6},{"imageOffset":8043,"symbol":"thread_start","symbolLocation":15,"imageIndex":6}]},{"id":1291577,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]},{"id":1291591,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":6}]}],
  "usedImages" : [
  {
    "source" : "P",
    "arch" : "x86_64",
    "base" : 4485332992,
    "CFBundleShortVersionString" : "1.12.3",
    "CFBundleIdentifier" : "date.play.simulator",
    "size" : 2490368,
    "uuid" : "bd5e55f7-1461-349b-adc5-e75dff204a61",
    "path" : "\/Users\/USER\/*\/Playdate Simulator.app\/Contents\/MacOS\/Playdate Simulator",
    "name" : "Playdate Simulator",
    "CFBundleVersion" : "140884"
  },
  {
    "source" : "P",
    "arch" : "x86_64",
    "base" : 140703377891328,
    "CFBundleShortVersionString" : "6.9",
    "CFBundleIdentifier" : "com.apple.Foundation",
    "size" : 3919872,
    "uuid" : "e22e60bb-ab77-3120-862f-92fa74feffcf",
    "path" : "\/System\/Library\/Frameworks\/Foundation.framework\/Versions\/C\/Foundation",
    "name" : "Foundation",
    "CFBundleVersion" : "1866"
  },
  {
    "source" : "P",
    "arch" : "x86_64h",
    "base" : 140703362703360,
    "CFBundleShortVersionString" : "6.9",
    "CFBundleIdentifier" : "com.apple.CoreFoundation",
    "size" : 5255168,
    "uuid" : "93c48919-68af-367e-9a67-db4159bc962c",
    "path" : "\/System\/Library\/Frameworks\/CoreFoundation.framework\/Versions\/A\/CoreFoundation",
    "name" : "CoreFoundation",
    "CFBundleVersion" : "1866"
  },
  {
    "source" : "P",
    "arch" : "x86_64",
    "base" : 140703510548480,
    "CFBundleShortVersionString" : "2.1.1",
    "CFBundleIdentifier" : "com.apple.HIToolbox",
    "size" : 3096576,
    "uuid" : "06fdecd6-9f69-397b-b1e2-a8226c0ba7ed",
    "path" : "\/System\/Library\/Frameworks\/Carbon.framework\/Versions\/A\/Frameworks\/HIToolbox.framework\/Versions\/A\/HIToolbox",
    "name" : "HIToolbox"
  },
  {
    "source" : "P",
    "arch" : "x86_64",
    "base" : 140703407239168,
    "CFBundleShortVersionString" : "6.9",
    "CFBundleIdentifier" : "com.apple.AppKit",
    "size" : 15269888,
    "uuid" : "06015263-62ac-3b08-a298-dc835c18452a",
    "path" : "\/System\/Library\/Frameworks\/AppKit.framework\/Versions\/C\/AppKit",
    "name" : "AppKit",
    "CFBundleVersion" : "2113.60.148"
  },
  {
    "source" : "P",
    "arch" : "x86_64",
    "base" : 4525690880,
    "size" : 442368,
    "uuid" : "71febccd-d9dc-3599-9971-2b3407c588a8",
    "path" : "\/usr\/lib\/dyld",
    "name" : "dyld"
  },
  {
    "source" : "P",
    "arch" : "x86_64",
    "base" : 140703362387968,
    "size" : 49152,
    "uuid" : "b5454e27-e8c7-3fdb-b77f-714f1e82e70b",
    "path" : "\/usr\/lib\/system\/libsystem_pthread.dylib",
    "name" : "libsystem_pthread.dylib"
  },
  {
    "source" : "P",
    "arch" : "x86_64",
    "base" : 140703362158592,
    "size" : 229376,
    "uuid" : "8cc28466-fd2f-3c80-9834-9525b7beac19",
    "path" : "\/usr\/lib\/system\/libsystem_kernel.dylib",
    "name" : "libsystem_kernel.dylib"
  },
  {
    "source" : "P",
    "arch" : "x86_64",
    "base" : 140703360602112,
    "size" : 290816,
    "uuid" : "1a04b380-76e4-3e4b-b0fc-9837533d021d",
    "path" : "\/usr\/lib\/system\/libdispatch.dylib",
    "name" : "libdispatch.dylib"
  },
  {
    "source" : "P",
    "arch" : "x86_64",
    "base" : 140703508058112,
    "CFBundleShortVersionString" : "1.0",
    "CFBundleIdentifier" : "com.apple.audio.caulk",
    "size" : 139264,
    "uuid" : "8e7b3d95-1d47-3f17-9512-c5fcc30792c2",
    "path" : "\/System\/Library\/PrivateFrameworks\/caulk.framework\/Versions\/A\/caulk",
    "name" : "caulk"
  },
  {
    "source" : "P",
    "arch" : "x86_64",
    "base" : 140703388721152,
    "CFBundleShortVersionString" : "5.0",
    "CFBundleIdentifier" : "com.apple.audio.CoreAudio",
    "size" : 7561216,
    "uuid" : "9be547d0-0af1-3470-afde-2ac4cd34441e",
    "path" : "\/System\/Library\/Frameworks\/CoreAudio.framework\/Versions\/A\/CoreAudio",
    "name" : "CoreAudio",
    "CFBundleVersion" : "5.0"
  }
],
  "sharedCache" : {
  "base" : 140703359131648,
  "size" : 19331678208,
  "uuid" : "73669942-bd8a-3e40-951f-7fbe07b51cb8"
},
  "vmSummary" : "ReadOnly portion of Libraries: Total=1.1G resident=0K(0%) swapped_out_or_unallocated=1.1G(100%)\nWritable regions: Total=1.7G written=0K(0%) resident=0K(0%) swapped_out=0K(0%) unallocated=1.7G(100%)\n\n                                VIRTUAL   REGION \nREGION TYPE                        SIZE    COUNT (non-coalesced) \n===========                     =======  ======= \nAccelerate framework               512K        4 \nActivity Tracing                   256K        1 \nCG backing stores                 2688K        4 \nCG image                           200K       11 \nColorSync                          232K       28 \nCoreAnimation                     14.2M       74 \nCoreGraphics                        12K        2 \nCoreUI image data                 3472K       25 \nFoundation                          44K        2 \nKernel Alloc Once                    8K        1 \nMALLOC                           369.5M       99 \nMALLOC guard page                   48K       10 \nMALLOC_MEDIUM (reserved)         960.0M        8         reserved VM address space (unallocated)\nMALLOC_NANO (reserved)           384.0M        1         reserved VM address space (unallocated)\nObjC additional data                15K        1 \nSQLite page cache                   64K        1 \nSTACK GUARD                       56.1M       22 \nStack                             18.7M       22 \nVM_ALLOCATE                        424K       22 \n__CTF                               756        1 \n__DATA                            30.5M      516 \n__DATA_CONST                      30.2M      339 \n__DATA_DIRTY                      1590K      211 \n__FONT_DATA                          4K        1 \n__LINKEDIT                       650.9M       16 \n__TEXT                           506.3M      534 \n__UNICODE                          592K        1 \ndyld private memory               1024K        1 \nmapped file                      208.3M      220 \nshared memory                     1284K       16 \n===========                     =======  ======= \nTOTAL                              3.2G     2194 \nTOTAL, minus reserved VM space     1.9G     2194 \n",
  "legacyInfo" : {
  "threadTriggered" : {
    "queue" : "com.apple.main-thread"
  }
},
  "trialInfo" : {
  "rollouts" : [
    {
      "rolloutId" : "63582c5f8a53461413999550",
      "factorPackIds" : {

      },
      "deploymentId" : 240000002
    },
    {
      "rolloutId" : "60186475825c62000ccf5450",
      "factorPackIds" : {

      },
      "deploymentId" : 240000026
    }
  ],
  "experiments" : [

  ]
}
}

Yep, I see the problem. I have code to add a reference from the sprite to the stencil image so that the image doesn't scope out and get collected while it's in use, but I did a dumb and assumed the image is on the top of the stack. Which it isn't, if you use the extra tile argument. :persevere: But there's an easy fix: you can just move the image loading out of the function to the top level so they don't scope out:

local lightDither = gfx.image.new("images/light-dither.png")
local darkDither = gfx.image.new("images/dark-dither.png")

Multiple sprites can share the same stencil image, so there's really no need to load a unique instance off disk every time anyway

Screen Recording 2022-11-16 at 1.02.16 PM(1)

Thank you! The crashing is gone now, but the problem where a key will display a solid fill (or sometimes a solid fill for only part of the key's area) is still occuring. Weirdly, it is always on the black keys, and which black keys its on depends on whether you're scrolling up or down, and which other keys are highlighted.

Would you mind posting the images, too? Or a compiled pdx? It'll be a lot easier to debug if I can run it myself :slight_smile: