Wednesday, March 05, 2025

Render Command Queue Full, Part 2: The UI's error handler

CK: Here’s the final piece of the puzzle: the main frame window’s handler for the render thread full error. The error arrives as an app error message with the following message map entry:

    ON_MESSAGE(UWM_RENDER_QUEUE_FULL, OnRenderQueueFull)

The plan is:

  1. Display the error message.

  2. After the user acknowledges the error, if the queue is no longer full, exit, otherwise:

  3. Display a second message with an option to do a reset; if the user chickens out, so be it, otherwise:

  4. If the MIDI input device is open and there’s at least one MIDI mapping, nuke the mappings. If the current mappings were modified, the user is prompted to save their changes. If they chicken out of that, then again, so be it. Otherwise we go ahead and empty the mapping array. Now the MIDI callback is no longer spamming the command queue, which reduces the pressure on the render thread.

  5. Now we’d like to also reset the visualizer’s “patch” document (putting all parameters in a default state) and also remove all of the visualizer’s “rings” (effectively deleting the current drawing). But both of these actions require commands to be sent to the render thread! So before we try that, we’d better make sure the command queue is at least half empty. So onward:

  6. Display a progress bar, with marquee mode enabled since we have no idea how long this will take.

  7. Enter a loop that continues while the command queue is *not* half full, meaning it’s more than half full. The loop calls CWinApp::PumpMessage which is enough to keep the UI alive, and doesn’t burn the CPU. No calls to Sleep, that’s bad form in Windows. Note that PumpMessage calls GetMessage which blocks, which might seem like a bug, but it’s fine because the main thread runs a 1 Hz timer for updating various things in the status bar. So even if the user doesn’t touch the mouse or otherwise generate Windows messages, there will at least be a message once a second, which is good enough for our purpose. The loop also checks the progress dialog’s cancel button. If the user clicks that, again, we’re out.

  8. If we made it here, the queue has now drained down to half full or less. The MIDI callback is no longer affecting the situation due to the lack of mappings. So it’s reasonably safe to assume that we have plenty of space to queue our two commands: one to reset the patch, and another to delete the current drawing. And with that done, the visualizer is reset to a peaceful, tranquil state.

Below is the code which implements the preceding specification.


LRESULT CMainFrame::OnRenderQueueFull(WPARAM wParam, LPARAM lParam)
{
	UNREFERENCED_PARAMETER(wParam);
	UNREFERENCED_PARAMETER(lParam);
	if (m_bInRenderFullError) {	// if already handling this error
		return 0;	// reentry disallowed
	}
	// save and set the reentrance guard flag; the flag's state will be
	// restored automatically when the save object goes out of scope
	CSaveObj<bool>	save(m_bInRenderFullError, true);
	AfxMessageBox(IDS_APP_ERR_RENDER_QUEUE_FULL);	// display error message
	// if render thread's command queue has free space
	if (!theApp.m_thrRender.IsCommandQueueFull()) {
		return 0;	// success
	}
	// the command queue is still full; more drastic measures are needed
	int	nResult = AfxMessageBox(IDS_APP_ERR_RENDERING_TOO_SLOW, MB_YESNO);
	if (nResult == IDNO) {	// if user chickened out of doing a reset
		return 0;	// cancel
	}
	// if the MIDI input device is open, and at least one MIDI mapping exists
	if (theApp.m_midiMgr.IsInputDeviceOpen() 
	&& theApp.m_midiMgr.m_midiMaps.GetCount() > 0) {
		// remove all MIDI mappings by loading a new playlist; if current
		// playlist was modified, user is prompted to save their changes
		if (!theApp.m_pPlaylist->New()) {	// if new playlist fails
			return 0;	// user probably canceled out of saving changes
		}
	}
	// if the MIDI input callback was spamming the command queue, 
	// it's no longer doing so, because we nuked the MIDI mappings
	CProgressDlg	dlg;
	dlg.Create();	// create progress dialog
	// we don't know how long this will take, so use a marquee progress bar
	dlg.SetMarquee(true, 0);
	// loop until the command queue drains down at least halfway
	while (!theApp.m_thrRender.IsCommandQueueBelowHalfFull()) {
		if (dlg.Canceled()) {	// if user canceled
			return 0;	// cancel
		}
		// pump message blocks waiting for messages, but it won't block
		// for long because we're running a timer to update the status bar
		theApp.PumpMessage();	// keeps UI responsive
	}
	// the command queue is now assumed to be half full at most, so there's
	// plenty of room for two commands: SetPatch (via OnFileNew) and SetEmpty
	theApp.m_pDocManager->OnFileNew();	// prompts user to save their changes
	theApp.m_thrRender.SetEmpty();	// removes all rings from the drawing
	// successful recovery
	return 0;
}

GPT: Overall this code is coherent and should work as intended. Here are some brief suggestions and notes on potential pitfalls:

  1. Reentrance Guard
    • Using CSaveObj<bool> to manage a temporary “in error handling” flag is straightforward. It ensures you don’t re-trigger the same dialog repeatedly.
  2. Checking IsCommandQueueFull()
    • You call if (!theApp.m_thrRender.IsCommandQueueFull()) to see if there’s enough room right after the user dismisses the first dialog. This avoids spamming the user with multiple prompts if the queue got partially drained during that dialog. It’s a good place to short-circuit.
  3. Second Prompt for Reset
    • The Yes/No flow is clear and offers a solution (reset) to the user if the queue is still overloaded. This is a definite UI improvement over repeated “queue is full” alerts.
  4. MIDI Mappings
    • Calling theApp.m_pPlaylist->New() to clear mappings means the MIDI callback will stop pushing commands. That’s a neat fix for an ongoing flood. Just remember you’re essentially throwing away the user’s current MIDI bindings (with a prompt to save if they were changed). That might surprise them, but presumably they clicked “Yes” so it’s acceptable.
  5. Progress Dialog Loop
    • The loop calls PumpMessage() instead of sleeping – standard “keep the UI alive” technique in MFC.
    • Since you have a 1 Hz timer, the loop won’t block completely even if the user is inactive. That’s fine.
    • Check that IsCommandQueueHalfFull() is working as intended (e.g. it returns false if the queue is more than half full). The naming can be a little confusing, so ensure your condition matches the meaning you want. [CK: After careful consideration I took GPT's suggestion and changed the name to the unambiguous IsCommandQueueBelowHalfFull.]
  6. Potential Race Condition
    • You rely on “the queue has drained enough” by the time you exit the progress loop. In principle, the drain rate is set by the rendering thread. If some other source (like the UI) attempted to push commands inside that loop, it might fill up the queue again. But since you presumably block new user commands with the modal progress dialog, the user can’t generate more commands. The MIDI callback also can’t fill the queue because you removed its mappings. So effectively, the queue should drain. That’s good.
    • Just be mindful that if anything else can still produce commands, the queue might fill again after you exit the loop. But you consider it “good enough” if you have enough space for your two commands to reset. That’s likely fine in your scenario.
  7. Resetting Patch (OnFileNew())
    • Using OnFileNew() automatically prompts the user to save the patch if the doc is dirty. This can lead to one more dialog, but that’s presumably intentional. If the user cancels that save, you skip resetting the doc. That might leave you partially in “limbo,” but presumably that’s what the user wants.
  8. Edge Cases
    • If the rendering thread is so bogged down that it truly never drains enough for even two more commands, you’d remain in the progress loop until the user cancels. That’s what you want, but be sure the user can do so gracefully.
    • If the user says “Yes” to reset, but the “New playlist” or “OnFileNew() logic fails (maybe the doc was locked?), you return early. So the user sees the progress bar vanish abruptly. That’s presumably okay, but might be disorienting. Consider a brief message or status line update for “reset failed/canceled.”

Final Thoughts

  • This design is a big usability improvement over perpetual “queue full” alerts. You’re offering a recovery (kill MIDI mappings, reset the patch) to free up the pipeline.
  • The progress dialog ensures the user can see that something is happening while the queue drains.
  • Everything else looks consistent with MFC best practices for SDI apps.

So yes, it’s a solid approach! Just keep the above minor details in mind, but it sounds like you’re on the right track.


CK: I’m (quietly) proud of my “neat fix for an ongoing flood” of MIDI input, and it means a lot to me that a black belt like yourself noticed it. And you were entirely right to question the confusing half-full accessor name.

No comments: