I looked at the original case...
Clicking close (or any other close action) will set a flag. The message loop will then
- finish all events
- call any pending QueueAsyncCalls (and that may be important, as they may do clean up work, and avoid error on exit)
- check the flag and exit the app
Only, you keep adding QueueAsyncCalls.
I don't know if that should be considered a bug in the LCL... since after all there is good reason to finish any pending work.
Despite my clear objection to running a 100% CPU load loop like your code did, you can fix your original code
At the end of paint (or for any QueueAsyncCall)
if not Application.Terminated then
Application.QueueAsyncCall(@DoRepaint, 0);
Then you will no longer tell the app control that you have more work before the app can exit.
You can also remove any remaining "Application.ProcessMessages" => not needed.