A note on hooks and event polling

Started by Adam Novagen, July 27, 2023, 00:01:38

Previous topic - Next topic

Adam Novagen

So after four hours of coding myself completely in a circle today, I'm leaving this here for future generations that don't know this about BMax (legacy anyway, not sure if this still applies to NG but I'd assume so).

I wanted a panic system in my game engine, i.e. a command that, while in developer mode, would let me quit the program on the spot and show a debug report. The idea was to have something that could, for example, allow me to get out of an infinite loop and maybe figure out why I was stuck there in the first place.

There are of course a few ways to do this, but I didn't want to do something clunky like calling panic() in every single FOR, REPEAT and WHILE loop in the entire program. So I turned to hooks, which on the surface to me looked like a promising answer; after all, I was already using them as a way to keep a GUI canvas running smoothly while resizing the program window, so my surface understanding of hooks was "this is something that always runs in the background, and does a thing as soon as a triggering event happens." Technically true, but the veteran coders here can probably already spot my mistake.

So, I go about setting up a hook system, get everything working correctly, and at first it looks perfect; seemingly regardless of what it's doing, my program quits and makes its log report the moment I press the Escape key. Okay, cool, so far so good, let me just try a quick infinite REPEAT...FOREVER loop and... Nothing. I spend the next few hours in a trimmed-down proof-of-concept program, scratching my head over why the hook function will trigger normally at any time except when I enter the forced infinite loop. At first I'm thinking maybe it's something to do with having graphics active with Flip and Cls - it wasn't - but after stripping stuff away bit by bit, I finally figure out that using KeyHit(KEY_SPACE) to trigger the infinite loop is the only thing left in the program that's allowing the hook to work properly; as soon as I remove that, the hook stops being called altogether. I look at the inside of KeyHit(), and that's when I finally spot the PollSystem command which I had no idea was in there.

Now things are starting to make sense, and sure enough, adding PollSystem to the infinite loop finally allows the hook to react appropriately, and the panic system now works; but, this puts me right back at square one where I have to add this command to every loop in the program to make the panic system work. Worse still, benchmarking an otherwise empty loop for a ten-run average at 10,000,000 iterations each makes the loop time blow up from 108ms to over 3000ms; almost 30x slower with PollSystem in there. (That was in Debug mode; with debug disabled, the loops averaged 2ms each without PollSystem and 91ms with it, much faster in absolute terms but an even bigger relative gap of 45x slower. It should also be noted that this was on a Ryzen 9 5900X, still a very fast, high-end CPU as of 2023.) Not really surprising, which is why I benchmarked it in the first place, but definitely not feasible for release mode, and to avoid a huge performance hit I would also have to add an IF statement to every loop to check whether the program was in developer mode or not before polling, which would also mean even more overhead, so... Yeah, not doing that :P


In the end, I spent half the day going in a huge circle, but it did at least teach me a good bit about event and hook handling, which is great because the documentation for hooks is almost completely nonexistent. Hopefully my struggles today will help save some poor soul from mucking about with this all day in the future :))

Here's a brief program to demonstrate, if it helps:

Code: BASIC
SuperStrict

SeedRnd MilliSecs()


Global panicEnabled:Int = False ' Set to True to enable developer panic, and make it possible to escape the infinite loop


Global quitGame:Int = False
Global eventTimer:tTimer

Global ballX:Int,ballY:Int ' Just a visual to indicate program activity
Global ballXSpeed:Int = 2,ballYSpeed:Int = 2


' ---------- Setup ---------

DebugLog "Initializing game..."

Graphics 640,480,0,60

SetClsColor 15,47,127

ballX = Rand(7,GraphicsWidth() - 7)
ballY = Rand(7,GraphicsHeight() - 7)

eventTimer = CreateTimer(4) ' Handle the event queue this many times per second
AddHook EmitEventHook,handleEvents ' Create a hook to run the event handler each time eventTimer ticks

DebugLog "Game initialized"


Repeat
	' ---------- Logic ----------
	local playLoopStart:int = milliSecs()
	
	ballX = ballX + ballXSpeed
		If ballX < 9 Or ballX > GraphicsWidth() - 10
			ballXSpeed = -ballXSpeed
		EndIf
	ballY = ballY + ballYSpeed
		If ballY < 9 Or ballY > GraphicsHeight() - 10
			ballYSpeed = -ballYSpeed
		EndIf
	
	If KeyHit(KEY_SPACE) ' Trigger an infinite loop if the spacebar is pressed
		DebugLog "Starting infinite loop..."
		Repeat
			' Without PollSystem, events will not move at all, the hook function will not be called and the panic system will not work
			If panicEnabled
				PollSystem
			EndIf
		Forever
	EndIf
	
	If KeyHit(KEY_RETURN) ' Quit normally if Enter is pressed - will not work once the infinite loop starts
		quitGame = True
	EndIf
	
	
	' ---------- Rendering ----------
	
	DrawOval ballX - 7,ballY - 7,15,15
	
	SetColor 0,0,0
	DrawText "Press Enter to quit the game normally.",1,0
	DrawText "Press spacebar to trigger infinite loop.",1,13
	If panicEnabled
		DrawText "Press Esc to trigger developer panic and quit.",1,25
	Else
		DrawText "Press Esc to trigger developer panic and quit (will not work from infinite loop).",1,25
	EndIf
	SetColor 255,255,255
	DrawText "Press Enter to quit the game normally.",1,0
	DrawText "Press spacebar to trigger infinite loop.",0,12
	If panicEnabled
		DrawText "Press Esc to trigger developer panic and quit.",0,24
	Else
		DrawText "Press Esc to trigger developer panic and quit (will not work from infinite loop)",0,24
	EndIf
	
	Flip ; Cls ' Display and then clear the back buffer
	
	' In case of higher refresh rates than 60fps, let the CPU breathe and keep the framerate at roughly 60
	Delay 16 - (MilliSecs() - playLoopStart)
Until quitGame = True

Notify "Game quit normally."

End


' The hook function
Function handleEvents:Object(id:Int,data:Object,context:Object)
	Local triggeringEvent:TEvent = TEvent(data)
	
	' Panic code - does not care why or when this function was called
	If KeyHit(KEY_ESCAPE)
		Notify "Game quit through developer-initiated panic."
		End
	EndIf
	
	' If the triggering event was a tick from the event handling timer, run through the event queue
	If triggeringEvent.id = EVENT_TIMERTICK And triggeringEvent.source = eventTimer
		
		' A timestamp to show activity
		DebugLog "Handling event queue at " + CurrentTime()
		
		' All other event handling
		While PollEvent() <> 0
			' All other event handling
		Wend
	EndIf
	
	Return data
EndFunction
We all know the main problem with dictionaries is that they contain too many words, and not enough butterscotch sauce!

Midimaster

#1
I would say, that there is a more comfortable way to stop apps, which are in a infinite loop.

In your model you need to add something into every loop you would expect a katastrophe. But you never know when or where it happens. It could hang while waiting for the file system or loosing connection to the internet, etc...

Why not working with a second thread?  This example creates a second thread and via the variable PanicTime it auto-detects a hanging main-loop and reacts:


Code: BASIC
SuperStrict
Graphics 600,400
Global PanicThread:TThread = CreateThread(PanicLoop, "")
Global PanicTime:Int=MilliSecs()+1000

Repeat
    Cls
    DrawText "Running:" + MilliSecs(),100,100
    DrawText "Press P to simulate a infinite loop",100,130
    DrawText "Press ESC to end the app at any time",100,160
    InfiniteLoop()
    PanicTime=MilliSecs()+1000
    Flip
Until AppTerminate()

Function InfiniteLoop()
    Global LOOP_ON:Int
    If KeyHit (KEY_P) LOOP_ON=True 
    If LOOP_ON = False Return 
    Repeat
    Forever
End Function 

Function PanicLoop:Object( data:Object )
    Repeat
        If PanicTime<MilliSecs() 
        Print "Hello Panic Loop " + (MilliSecs() - PanicTime)
        If PanicTime<MilliSecs()-5000 End  
        Delay 10
    Until KeyHit(KEY_ESCAPE)
End Function


And the advantage is that this does not slow down the app.
...on the way to China.

Derron

PollSystem is "expensive" as it checks the state of all keys on your keyboard (or more specific - 255 values).

Instead of polling the system "this way" you could also register for the key events and handle the input things on your own.

Anyways: as soon as you are in a loop, this won't help you here.

I guess a solution could be to have:
- input/events (collecting) running in a thread
- running your logic in "another thread"
- having the main thread looking for "panic ??" (aka events -> input -> panic mode desired?)
- having the main thread collecting the debug information from the other threads, spitting out the debug parts, exiting the threads


bye
Ron

col

A system that I use for my work is to have the main thread expect a message from all threads within a specified time interval. If no message is received from any thread within the time internal then I break into the debugger which also stops all threads and I can find the offending issue (vstudio pro with cpp).

Unfortunately in bmax an offending thread (except the thread that kicks the debugger) thats stuck in a loop doesn't stop when the debugger kicks in so if someone wants to invest time to fix that issue then my idea will work in bmax.
https://github.com/davecamp

"When you observe the world through social media, you lose your faith in it."

Adam Novagen

Man, threads never occurred to me; they're almost as poorly documented as hooks, so I've never experimented with them. Also I think I'd been assuming that hooks were somehow threaded themselves. I might just plug away at implementing this soon, thanks for that! ;D
We all know the main problem with dictionaries is that they contain too many words, and not enough butterscotch sauce!