Wednesday, March 05, 2025

PushCommand: Handling render command queue full

CK: Based on all we’ve discussed previously, let’s review my PushCommand method. Thank you for your suggestions, several of which are incorporated. In particular, exponential backoff is implemented. Bear in mind that only three threads are involved: the main thread, the render thread, and the MIDI input callback. Note also that the render thread never calls PushCommand; it role is strictly to drain the command queue, once per frame. The only callers of PushCommand are the main thread (in response to user activity) and the MIDI input callback (in response to MIDI messages from a control surface).

The handling for a failed push—due to the command queue being full—is as follows:

  • For the main thread: Simply display a retry/cancel message box.

  • For the MIDI input thread: Retry the push, with an exponentially increasing timeout (Sleep) between each retry. The first Sleep is zero (relinquishing the remainder of the time slice), after that it’s 1, 2, 4, 8, 16 etc. milliseconds. If the total timeout reaches the limit (250 ms), enter an error state.

This is the “all retries failed” error state. In this state, no retries are attempted for the next 2500 ms. This is essential, because otherwise PushCommand could block for every MIDI message. Instead, for the next 2500 ms, the MIDI input callback continues to map messages to commands, but discards the commands, and is thus unimpeded.

PushCommand posts a notification message to the main thread when it enters the error state. This main thread displays a dialog informing the user an error has occurred. It uses reentrance guards to ensure that only one dialog can be displayed at a time.

The user of the MIDI input device will be unable to control the rendering during these 2500 ms of error state. This is by design, and it is no great disaster.

Let’s assume MIDI input continues to arrive and gets mapped to commands. One of two things will happen:

  1. The render thread will catch up and empty the queue. In this case, the next PushCommand will succeed, exiting the error state.

  2. Though the queue is still full, the 2500 ms break will expire. In this case, PushCommand will begin the retry loop again.

Some other things to note about this code:

  • Under no circumstance will PushCommand notify the main thread more often that once every 2500 ms.

  • The most likely execution path (the enqueue succeeds on the first try) is full optimal; the error handling imposes no additional overhead of any kind in that case.

  • This code is not merely hypothetical; it has been tested extensively using a so-called “torture test” consisting of a separate application that fires MIDI messages at the visualizer app, along with a temporary kludge in the visualizer app that causes it to render unusually slowly. The visualizer was reduced to frame rates below 1 Hz for extended periods, but no harmful effects were noticed: the MIDI input continued to arrive normally, the OS did not complain, and no anomalous thread interactions (such as priority inversion) occurred.

And without any further introduction, here is the code of PushCommand:

bool CWhorldThread::PushCommand(const CRenderCmd& cmd)
{
	while (!CRenderThread::PushCommand(cmd)) {	// try to enqueue command
		// enqueue failed because render command queue was full
		if (CWhorldApp::IsMainThread()) {	// if we're the user-interface thread
			// if not already handling render queue full error
			if (!theApp.GetMainFrame()->InRenderFullError()) {
				// give the user a chance to retry enqueuing the command
				if (AfxMessageBox(IDS_APP_ERR_RENDER_QUEUE_FULL, MB_RETRYCANCEL) != IDRETRY) {
					return false;	// user canceled, so stop retrying
				}
			}
		} else {	// we're a worker thread
			// all times are in milliseconds
			const UINT	nMaxTotalTimeout = 256;	// maximum total duration of retry loop
			const UINT	nRetryBreakTimeout = 2500;	// duration of break from retries
			LONGLONG	nTimeNow = static_cast<LONGLONG>(GetTickCount64());
			// if we're in an error state, taking a break from doing retries
			if (m_nLastPushErrorTime + nRetryBreakTimeout > nTimeNow) {
				return false;	// push command fails immediately, no retries
			}
			// do a limited number of retries, separated by increasing timeouts
			UINT	nTotalTimeout = 0;
			UINT	nTimeoutLen = 0;	// zero means relinquish remainder of time slice
			// while total time spent sleeping remains within limit
			while (nTotalTimeout + nTimeoutLen < nMaxTotalTimeout) {
				Sleep(nTimeoutLen);	// do a timeout of the specified length
				if (CRenderThread::PushCommand(cmd)) {	// retry enqueuing command
					// success: clear error state by zeroing time of last error
					InterlockedExchange64(&m_nLastPushErrorTime, 0);
					return true;	// retry succeeded
				}
				// retry failed: increase timeout and try again if permitted
				nTotalTimeout += nTimeoutLen;	// add timeout to total time slept
				if (nTimeoutLen) {	// if non-zero timeout
					nTimeoutLen <<= 1;	// double timeout (exponential backoff)
				} else {	// zero timeout
					nTimeoutLen = 1;	// start doubling from one
				}
			}
			// all retries have failed, so take a break from doing retries,
			// to avoid blocking the worker thread on every attempted push
			InterlockedExchange64(&m_nLastPushErrorTime, nTimeNow);
			// notify main thread that an unrecoverable error occurred
			PostMsgToMainWnd(UWM_RENDER_QUEUE_FULL);
			return false;	// we are in the retries failed error state
		}
	}
	return true;
}  

GPT: Overall, this code cleanly implements your stated goals and should be stable under the conditions you’ve described. Here are a few concise observations and potential refinements:

  1. Exponential Backoff Logic
    • You start with Sleep(0) (yield) and then escalate to 1 ms, 2 ms, 4 ms, etc., up to a total of 256 ms. This is a good balance between not hogging the CPU and quickly recovering when the queue frees up.
    • If you need finer control, you could add a limit to the backoff (e.g., never exceed 16 ms) so you don’t wait too long in one step. But that’s optional.
  2. Avoiding Frequent Error Dialogs
    • You’ve addressed the “perpetual message box” problem by allowing only one dialog at a time, and by blocking further retries for 2500 ms after failure. This ensures you won’t spam the user, and that the MIDI callback is never permanently tied up once it enters the error state.
    • If you need even fewer dialogs (because 2.5 seconds might still produce multiple popups in rare edge cases), you could keep track of whether you’ve already posted an error for the current break period, and skip posting again. You’re effectively doing this by resetting m_nLastPushErrorTime, so it’s likely fine.
  3. Main Thread vs. MIDI Callback Differences
    • The main thread can keep retrying indefinitely (user chooses “Retry”), which is acceptable for non-real-time code.
    • The MIDI callback bails out at 250 ms of total retry time, which avoids excessive blocking. If it fails, you enter an error state for 2.5 seconds and drop incoming commands during that time. This is a very reasonable compromise in a scenario where losing a few MIDI commands isn’t catastrophic.
  4. No Extra Overhead on Success
    • On the most common path (queue not full), CRenderThread::PushCommand(cmd) returns true immediately, and your code imposes practically zero overhead. This is ideal in normal operation.
  5. Edge Cases
    • Wrap-around of GetTickCount64() is effectively a non-issue on modern systems (it’s 64-bit, so it won’t wrap for ages). If you were using 32-bit GetTickCount, you’d need extra care every ~49.7 days.
    • If the queue remains completely full for a very long time (e.g. user sets a bizarrely slow frame rate), you’ll keep going into the error state and discarding MIDI commands. That’s intentional and safe, but the user could remain effectively locked out of MIDI control. This is consistent with your design, just be sure to handle or log that scenario so it’s not mysterious.

In short, your approach should work well under real-world conditions for a visualizer. The code is clear and handles worst-case scenarios gracefully—there’s no risk of indefinite blocking, yet the user is informed if the queue remains blocked. If you need more fine-grained backoff tuning or alternative error-notification logic, that’s straightforward to tweak, but as is, your design is sound.


CK: Thank you for that appraisal. I’m glad to hear that you agree that the design is sound. But we’re still not quite there. I feel that a bit more work is needed on the user interface side. The reentrance guards are not quite enough. In the case where the system is seriously backed up (due to a very low frame rate and a consistently high data rate from the MIDI input device) the main thread could still show dialogs too often. The user presses OK to close the dialog, and 2500 ms the app shows the same dialog again. This is annoying and unprofessional, and better design is certainly possible.

The fault lies not with PushCommand, which is only doing its job, but with the main thread. The main thread needs to be smarter about how it handles the notifications. My proposal is this:

After the main thread displays the “Render command queue is full” warning message box, but before it clears the reentrance guard flag, the main thread should check to see whether the command queue is still full. This is simply a read of the number of elements in the ring buffer. The answer doesn’t have to be perfect, so there’s no thread safety issue and hence no synchronization object is required. If the main thread finds that the command queue is still full, it should display a second “yes/no” message box, something like this: “Rendering is taking longer than usual. Would you like to reset to default settings?” And, if the user selects “yes”, the handler should display a modal dialog, and then try to load default settings. The loading may fail—due to the command queue being full—in which case the load should be repeated (after a brief timeout) until either it finally succeeds, or the user cancels the dialog. This will eventually clear the “logjam” and get things flowing again.

Tuesday, March 04, 2025

Global Ring Count overruns points buffer

I observed intermittent crashes related to m_posDel while running the mapping undo test and bombarding Whorld with random MIDI input. It looked like a cascading delete issue but this was misdirection. Closer inspection found garbage in all CWhorldThread members above the point array (m_aPt), while members below m_aPt were intact.

This clearly indicated that the draw loop was not respecting the the point array size (MAX_SIDES). And sure enough, a search for MAX_SIDES revealed that max sides clamping is done at the start of AddRings, BEFORE the global nPolySides is added. Thus if the ring count is at or near maximum, AND curves are enabled, AND Fill is enabled, AND the global ring count is one or more, the point array overflows, generally causing havoc, and specifically overwriting m_posDel which will certainly cause Whorld to crash on the next cascading delete, due to an invalid list position.

Demo patch: points buffer overrun set Global Ring Sides 51.whp

More succinctly, the points buffer is overrun when:

  1. Ring Sides = 50
  2. Curves = true
  3. Fill = true
  4. Ring count >= 2
  5. Global Ring Sides >= 1

OR

  1. Ring Sides = 50
  2. Curves = true
  3. Global Ring Sides >= 51

The bug is more probable when Fill is enabled, because in that case the points buffer must accommodate TWO rings rather than only one. And in the Fill case, it's the memcpy that overruns, while copying the previous ring's points to the end of the points buffer.

The following assertion just before the memcpy catches the overrun:

    ASSERT((nPoints + nPrevPoints) * sizeof(D2D_POINT_2F) < sizeof(m_aPt));

Adding a canary member variable after the points buffer and checking it after the ring iteration also catches the overrun:

    D2D_POINT_2F m_aPt[MAX_POINTS * 2]; // enough for two rings [OR NOT!]
    UINT_PTR m_nCanary; // guard band for detecting buffer overrun
    m_nCanary = 0x1234abcd4321dcba; // init in WhorldThread ctor
    ASSERT(m_nCanary == 0x1234abcd4321dcba); // detects overrun

This bug was introduced on January 15, 2008, with the following comment: "add globals for line width and poly sides".

The fix is trivial. In OnDraw, simply replace this line:

    nSides = max(nSides, 1);

with this one:

    nSides = CLAMP(nSides, 1, MAX_SIDES);

Prior to the 2008 revision, MAX_SIDES was only enforced in AddRings because there was no need to enforce it per ring, and doing so anyway would have wasted precious time. But implementing global Ring Sides changed the situation: it became necessary to enforce MAX_SIDES inside the ring-drawing loop, because nSides is the sum of the ring's side count and global Ring Sides, and latter can change at any time.

    nSides = ring.nSides + m_globRing.nPolySides; // the source of the bug
    nSides = max(nSides, 1); // no longer enough, must enforce MAX_SIDES too!

Thursday, February 27, 2025

Whorld Link: Synchronization and Synesthesia

Whorld is driven by oscillators. In order to make smooth graphics, the oscillators require a precise timing source. In V1, that source was a multimedia timer, but in V2, the timer is gone. In V2, the oscillators are driven by the display monitor: rendering is synchronized with the monitor’s vertical retrace, via a DXGI swap chain, just as in a game.

One consequence of this display-driven design is that precise synchronization with an external device is only possible in one direction: Whorld can be the master, but never the slave. Like V1, V2 currently only supports MIDI input, but it could also send MIDI output, such as MIDI clocks, which an external device could synchronize itself with. It would also be possible to send continuous controller messages for the phase of each oscillator, though this would need to be managed carefully to avoid overloading MIDI and/or the receiver.

For such a scheme, it will matter which type of MIDI we’re talking about. Old-school hardware MIDI has extremely limited bandwidth: 30K Baud or about 1000 messages per second at most. USB over MIDI is at least an order of magnitude faster. The fastest is a software connection within the same computer, via Tobias Erichsen's loopMIDI for example: that should be nearly instantaneous.

I like the idea of sending controller messages for oscillator phase, because it opens up possibilities for generative art, and specifically for synesthesia. I implemented a crude version of this scheme back in 2006. The name of that project is Plasmagon. It’s a specific Whorld patch controlling a music generator in real time. It was a hacked version of Whorld V1 that sent the phase of its oscillators to Propellerhead Reason via MIDI CC messages. There’s no proper video because Whorld doesn’t compress well, but the music is lovely and you can hear it HERE.

Wednesday, February 26, 2025

Whorld's new thread architecture

In Whorld V1, configure a MIDI controller to control a setting, one with an immediate and obvious effect, like Master Speed. Now continuously move the controller with one hand, and with your other hand, right-click in the Whorld’s caption bar and keep the right mouse button pressed. The MIDI controller stops affecting the rendering, and this freeze persists until you release the right mouse button.

This happens because you’re blocking the Windows message loop, and in V1, the MIDI events are routed through the Windows message loop on their way to the render thread. If the message loop blocks, MIDI events are blocked too.

This bug is gone in V2.

The MIDI thread has a more complex task now. Instead of merely posting input MIDI messages to the UI thread, the MIDI thread now does the mapping, and queues the resulting commands directly to the render thread. The UI thread has no involvement in that route and cannot impede it. The MIDI thread also posts the corresponding parameter changes to the UI thread, so that it can update the sliders and edit boxes. If the UI thread is busy or blocked, updates pile up, but MIDI control of rendering is unaffected. And of course, the UI thread also sends commands to the render thread, in response to user edits.

This is a proper professional design. The only complication is that the user can modify the MIDI mapping. That means the MIDI and UI threads must share the mapping data, and such sharing must be managed carefully to avoid data corruption. Luckily, the MIDI thread reads the mapping data but never modifies it. Only the UI thread modifies the mappings, and this makes synchronization much easier.

Click to enlarge diagram

Sunday, February 23, 2025

V1 versus V2 side-by-side comparison

Here are side-by-side comparisons for some of my favorite snapshots. The antialiasing is a win.

Snapshot Movie throughput test

At the END of OnDraw:

if (m_hMovieFile != INVALID_HANDLE_VALUE) { // if we're recording a snapshot movie
  CBenchmark b;
  CSnapshot* pSnapshot = GetSnapshot(); // get the snapshot (allocates on heap)
  DWORD	bWritten;
  WriteFile(m_hMovieFile, pSnapshot, pSnapshot->GetSize(), &bWritten, NULL); // write the snapshot
  delete pSnapshot; // delete the snapshot
  stats.Print(b.Elapsed());
}

And open the file somewhere:

m_hMovieFile = CreateFile(_T("test.whm"),
  GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

Don't forget to also CLOSE the file via CloseHandle.

The statistics:

minimum: 13 µs
maximum: 175 µs
average: 45 µs
standard deviation: 35 µs

Sample size: 60 (one second at 60 Hz)

Every now and then I see a maximum two orders of magnitude larger, in the range of 3 ms to 6 ms. This is long enough to worry about, and possibly sufficient justification for asynchronous writes, using either a single buffer, or a fancy ring of buffers as GPT suggests.

Asynchronous I/O is ideal when the application can do other work while the I/O is completing, and that's certainly the case here. Whorld can block in Present, and then completely draw the next frame before it needs the result of the previous write. Using a multi-buffer model, it could go even longer without waiting.

This same justification could apply when playing back the movie! By using asynchronous reads and a ring of buffers, it would be possible to stay several frames ahead, so that while the render thread is drawing or presenting the current frame, the OS is busy reading the next frame(s).

Given how complex all that is, it should definitely be wrapped in an object.

Snapshot movies

CK: I’m making steady progress on the Whorld feature set. Today I got snapshots working. A “snapshot” is a Whorld-specific binary file format that contains all the necessary information to reconstruct a Whorld frame. It’s essentially Whorld’s equivalent of a vector-based format. It’s incredibly compact: a typical snapshot is between 10K and 50K bytes. The size varies with the number of “rings” in the drawing. Internally, a snapshot consists of a fixed-length portion containing various drawing state data, followed by a VLA (Variable Length Array) having one RING element for each of the drawing’s rings.

The advantage of snapshots—compared to the File/Export command, which exports the drawing as a PNG file—is that snapshots are resolution-independent. So a snapshot can subsequently be reloaded and exported at a much higher resolution than was being used when the snapshot was “taken” (captured).

In the original Whorld, it was also possible to record a “movie” consisting of a series of snapshots, one per displayed frame. The snapshot movie is resolution-independent; no pixels are recorded. The snapshot movie could subsequently be reloaded and exported as video at any desired resolution. The snapshot movie could also be viewed without exporting it, since the snapshots are easily redrawn as frames. And, it was possible to export only a subset (range) of the frames.

I hope to implement the above-described movie recording capability in Whorld V2, but with some important differences, as follows:

  1. Export image sequence only: V2 will only export a snapshot movie as a numbered image sequence. The primary advantage of an image sequence is that it keeps video compression completely out of Whorld’s code. Even today, Windows video compression is a bloated hellscape of obscurity, and will never be as flexible or satisfactory as ffmpeg. Not to defend Microsoft, but this is partly due to IP issues that ffmpeg’s developers sidestep.

  2. Reconsider snapshot storage: I would like to revisit how exactly the app stores the snapshots during recording. The render thread is responsible for recording the movie, so storing each frame needs to be very fast, otherwise we’ll fall behind the display frequency and rendering will be disrupted.

In my view, the most fundamental decision is whether to store the snapshots in a file, or in memory. To decide which option makes the most sense, we need some capacity analysis. The original Whorld app recorded the snapshot movie to a file, but it also was running at a slower frame rate, typically 25 Hz.

A typical patch with about 200 rings requires around 40K bytes per frame. At 60 Hz, that’s 2.5 MB per second, 144 MB per minute, or 8.64 GB per hour. Agreed?

Ideally the user should be able to record an entire VJ performance, which could potentially last for hours. That goal seems incompatible with recording to memory. A typical target system might have only 16 GB of memory, so we’d be out of memory in less than two hours of recording.

Let’s examine the file option, which will be much less constrained in terms of maximum recording size. According to Google, a typical hard disk write speed is 30-150 MB/s (in 2024). We need 2.5 MB/s, so we’re well within capacity there.

So based on that simple analysis, I’m leaning towards writing the frames to a file, just as Whorld V1 did. The question then is: what type of file will perform best? There are many options in Windows, but based on my experience (with another app of mine, WaveShop, which recorded audio) in my opinion we’ll get the best performance using CreateFile. Do you concur with that opinion?

CreateFile gives us some powerful and flexible options that other file types don’t. In particular, we should in my opinion consider this sentence from MSDN “When FILE_FLAG_NO_BUFFERING is combined with FILE_FLAG_OVERLAPPED, the flags give maximum asynchronous performance, because the I/O does not rely on the synchronous operations of the memory manager.”

Our top priority is that the write operation should be as fast as possible. Is it reasonable in your view to use asynchronous (overlapped) writes for this application? On the surface, it seems ideal, but I know from experience that it brings many complications. In particular, it’s necessary to keep the data buffer and the OVERLAPPED structure allocated and unmodified until the write completes. So in practice I suspect we wouldn’t be able to do better than overlapping by one frame. But this could still be a big win. Let’s compare the cases:

  1. Synchronous write: The render thread finishes rendering the frame. Before presenting the frame to the swap chain, it stores the snapshot data to a buffer, and then writes that buffer to the snapshot movie file. If the file write takes too long, the render thread calls Present too late and misses the vertical retrace, causing a glitch in the displayed output. Disaster!

  2. Asynchronous write: As before, the render thread finishes rendering the frame, stores the snapshot data to a buffer, and then writes it to the file. But in this case, the write function returns almost instantly. Now the render thread calls Present, and while it’s blocked in Present waiting for vertical retrace, the OS writes to the file asynchronously. When Present unblocks, the render thread begins drawing the next frame, and again, the write continues asynchronously. Only when the next frame is completely drawn do we need to wait for previous frame’s write to complete.

This sure sounds like a significant optimization. But, overlapped I/O incurs OS overhead. The question we must answer: is the increased overhead of overlapped I/O justified by the potential performance gain from asynchronous writing? In order to determine that, we need a basis for comparison. So it seems to me that the first step is to set up CreateFile for regular synchronous I/O, and then have Whorld record a snapshot movie, and while it does so, accumulate statistics—minimum, maximum, average, and standard deviation—on how long the file writes are taking. If it turns out that writing 40K is consistently too fast to be worth optimizing, we can just go with that, and avoid all the additional development time and complexity risk of using overlapped I/O.

Your thoughts?

ChatGPT: Weighing write strategies

I’m thinking through comparing synchronous and asynchronous snapshot recording methods. Synchronous write to a file seems most viable, using 2.5 MB/s.

ChatGPT: Short Answer: It’s perfectly reasonable to start with a regular synchronous file write (CreateFile in normal mode) and measure how long it takes to write ~40 KB per frame. If it’s consistently small (well under the ~16 ms budget at 60 Hz), you can skip the complexity of overlapped I/O. If it’s borderline, then you can explore overlapped (asynchronous) writes with a small ring buffer of snapshots. Yes, CreateFile is the standard approach for best performance/flexibility (especially if you consider flags like FILE_FLAG_NO_BUFFERING and FILE_FLAG_OVERLAPPED).

Below are the detailed considerations:


1) Writing to Disk vs Memory: The 2.5 MB/s Rate

A) Disk Should Easily Handle ~2.5 MB/s

  • Modern HDDs do 30–150 MB/s, SSDs are far faster.
  • 2.5 MB/s is typically trivial. Even random writes at 40 KB chunks might be okay, but the best-case scenario is a decently contiguous file on disk.

B) Why Not Store in Memory?

  • As you said, 8.64 GB/hour is feasible on disk, but not always feasible in memory if the user records for hours. You’d risk using all your 16 GB of system RAM. So disk is the correct solution.


2) Synchronous Write vs Overlapped I/O

A) Synchronous Write Approach

  1. Render thread finishes frame, obtains the ~40 KB snapshot data, calls WriteFile(...) (synchronous).
  2. If the OS can write it quickly, your thread returns in time for the next Present call.
  3. If the disk is momentarily slow, the write call blocks, the render thread might miss its vertical retrace scheduling → stutter in visuals.

Pros:

  • Very simple code.
  • No concurrency overhead, no buffer management.

Cons:
  • Any disk stall >16 ms can cause a missed frame.

B) Overlapped (Asynchronous) Approach

  1. Render thread obtains the 40 KB snapshot, calls WriteFile with an OVERLAPPED structure. The OS returns immediately if the call is queued successfully.
  2. Meanwhile, the OS writes data to disk in parallel while your thread continues.
  3. You must keep that 40 KB buffer + OVERLAPPED struct valid until the write completes. Possibly use a ring buffer with a few entries so you can keep writing new frames without stalling.
  4. Before reusing that same buffer for the next snapshot, you wait on the previous write’s completion event or poll GetOverlappedResult with bWait=FALSE.

Pros:

  • If the disk is normally fast enough, the thread never or rarely stalls.
  • Rare slow writes might get hidden behind the next frame’s rendering/present.

Cons:
  • More complex code: ring buffer of snapshot memory, overlapped data structures, etc.
  • If the disk can’t keep up for many consecutive frames, eventually your ring buffer fills and you stall anyway.


3) Do a Measurement First

Your plan to “do synchronous I/O and gather min/avg/max time” is excellent:

  1. Set up a test scenario: 60 Hz rendering, 40 KB per frame => ~2.5 MB/s.
  2. Log each WriteFile call’s duration (endTick - startTick).
  3. If you see typical times well under 1 ms, you’re safe with synchronous.
  4. If you see occasional spikes of 50 ms or 200 ms, that might cause dropped frames. Then overlapped is more appealing.

Typical Realities

  • On an SSD or a lightly loaded system, 40 KB writes are often <1 ms, especially if the OS does caching.
  • On an HDD with other processes or random seeks, you might see occasional large stalls.


4) FILE_FLAG_NO_BUFFERING and FILE_FLAG_OVERLAPPED

A) FILE_FLAG_NO_BUFFERING

  • Bypasses the OS file cache, reading/writing directly to/from disk. That requires sector-aligned buffers and sizes. This can improve or degrade performance, depending on the disk usage.
  • Usually used in high-throughput servers or certain real-time apps. For your case, you might get better performance from letting the OS cache the writes in memory (the default), especially for many small ~40 KB writes.

B) Overlapped I/O

  • If you do overlapped, you can combine it with or without NO_BUFFERING.
  • If you do NO_BUFFERING, each write must be multiple-of-sector-size and aligned, which is more complexity.
  • If you do overlapped with default buffering, you can just do 40 KB, the OS caches it, returns quickly, then flushes to disk asynchronously. That might be enough to avoid stutters.

Conclusion: Overlapped + default buffering is typically simpler. NO_BUFFERING might not help for small writes unless you’re extremely performance-critical and prepared to handle alignment constraints.


5) Implementation Approach

  1. Try Synchronous:
  2. CreateFile with normal flags, do WriteFile each frame.
  3. Measure durations. If it’s always <2 or 3 ms, likely no problem at 60 Hz.
  4. If you see random 100 ms stalls, you either accept the occasional stutter or move to asynchronous.

  5. If Stalls:
  6. Then do overlapped. Maintain, for instance, a ring of, say, 3–5 snapshot buffers. Each time you finish a frame, pick the next buffer, do WriteFile overlapped, queue the completion. If you loop back around to a buffer that’s still in progress, you stall until that I/O completes.
  7. Typically, you’ll never stall unless the disk truly can’t keep up with average throughput.

  8. (Optional) More advanced:
  9. If your throughput is borderline, consider chunking multiple frames into one larger write or compressing the snapshots in memory. But 2.5 MB/s is so small that it’s likely your disk can handle it with minimal overhead.


6) Memory Mapped Files?

You could also consider a memory-mapped file approach for writing. But for streaming large amounts of data, typical direct WriteFile calls are simpler and more predictable. The memory map might do some lazy writes or cause page faults. So not necessarily better for real-time streaming.


Conclusion

Your plan is exactly right:

  1. Start with synchronous WriteFile in normal buffered mode.
  2. Log your write durations.
  3. If they’re consistently short (likely on modern hardware), done—no complexity needed.
  4. If you see stutters or big outliers, consider overlapped writes with a ring buffer.
  5. CreateFile is indeed the standard method. FILE_FLAG_NO_BUFFERING can be beneficial in special contexts, but for a small 40 KB repeated write at 2.5 MB/s, the OS caching usually helps.

Hence you can proceed with confidence, measure, and only adopt asynchronous I/O if the real data demands it.

Friday, February 21, 2025

Multi-threaded image capture in Direct2D

CK: So I have the whole “bitmap capture” and export solution in place. It works fine. But I want to verify my solution with you. I went with the split solution, where the rendering thread does everything up to the copy from (GPU) target bitmap to CPU-readable bitmap. It then posts the readable bitmap to the main thread (via post message) and never touches it again.

When the main thread handles the bitmap capture message, it does the map, and then does all the WIC stuff, writing the image to disk. It then commits, unmaps, releases the bitmap pointer, and that’s all.

This is a handy division of labor, because the map operation is what actually copies the image from GPU to CPU memory, and that part can be quite slow. So it’s very convenient that that’s occurring on the main thread’s time, because that way it doesn’t disrupt rendering.

My understanding is that this scheme does comply with the COM rules. If I use COM without enabling multi-threaded mode, then I am responsible for preventing race conditions. In other words, I am responsible for ensuring that any given COM instance is accessed by one and only one thread at any given time.

My scheme certainly ensures that. As I said, once the render thread posts the bitmap, it never touches it again. The simple fact of queuing via the windows message loop ensures there is no possibility whatsoever for a race.

Do you agree with the above?

The mere fact that the scheme appears to work proves nothing of course, with multi-threading we must be on sure footing conceptually.

ChatGPT o1: Yes, your handoff scheme should be perfectly valid under Direct2D/COM’s single-thread concurrency rules, as long as:

  1. The render thread never touches the CPU-readable bitmap object again after posting it to the UI thread.
  2. The UI thread fully “owns” that object from that point on (maps it, does WIC, unmaps, releases).

That ensures there is no concurrent access at all—exactly what COM’s single-thread apartment model expects. Below is a bit more detail on why it’s safe:

1) COM Single-Thread Rules

  • In default (STA) COM: Each COM object instance is associated with the thread that creates it, and is not automatically “marshaled” to other threads.
  • However, Direct2D and DXGI objects are somewhat “special.” They aren’t strictly STA for all their internal logic, but the official stance is that any one ID2D1 object is not guaranteed to be thread‐safe. So if you want to pass it across threads, you must ensure there’s no overlapping usage.

Your usage pattern is:

  1. Render thread calls CreateBitmap(..., CPU_READ | CANNOT_DRAW), then does CopyFromBitmap(...).
  2. Render thread posts the resulting ID2D1Bitmap1 * to the UI thread via a Windows message.
  3. Render thread never touches that bitmap pointer again.
  4. UI thread receives the message, does Map(...), WIC encode, Unmap(), Release(), etc.

No concurrency occurs. Each piece of code “owns” the bitmap at different times, with no overlap.

2) Typical Precedent in D2D/DX

  • This pattern is quite similar to a “CPU readback” approach in D3D, where you create a staging resource on one thread, then pass it to another for final saving. The key is that only one thread interacts with that resource at a time.
  • The D2D docs mention that ID2D1Bitmap1 is not thread‐safe, but it’s fine if you exclusively access it from a single thread (the UI thread), after you’re done setting it up on the render thread.

3) The Windows Message “Boundary”

Posting a pointer in a WM_USER + X message is a classic way to pass COM pointers between threads. Since your render thread is done with that pointer, the UI thread can safely do Map(...) and so on. The Win32 message queue ensures a “happens-before” relationship—by the time the UI thread processes that message, the render thread has finished. That eliminates any race condition concerns.

4) Summary

Yes, the fact that you see it “working” is consistent with being on correct conceptual footing: you are ensuring that once the resource is handed off, only the UI thread uses it. That’s exactly the recommended approach for single-thread concurrency in Direct2D/COM. You avoid stalling your render loop (the slow disk I/O or GPU→CPU copy occurs on the UI thread’s time) and you keep the concurrency model simple.

So, your scheme is indeed safe and correct for the reasons you outlined. Nice work!

Thursday, February 20, 2025

Whorld V2 Roadmap

The alpha version of V2 is done, and being tested. As with FFRend, rendering runs entirely in its own worker thread. The renderer doesn’t even have a Windows message loop, so it cannot be blocked by the UI. The UI can only communicate with the renderer via a thread-safe command queue. The renderer doesn’t have a timer either, and that’s why there’s no frame rate setting. Instead the renderer runs at the monitor’s display frequency, via a DXGI swap chain, just as game developers do. Whorld has essentially been restructured into a modern Windows game.

Whorld V2 now renders entirely via Direct2D, which is fully hardware-accelerated. Direct2D is a wrapper around Direct3D, so we’re running Direct3D under the hood. Tessellation is on the CPU, possibly on multiple cores, but drawing is on the GPU. But I digress. Here’s a quick roadmap for what lies ahead:

  1. MIDI mapping. This will be a complete do-over. I plan to follow the same paradigm used in my Polymeter app. The advantage of that paradigm is flexibility. A control can map to multiple targets, a target can map to multiple controls, all bases are covered. I expect MIDI mapping to be a long-ish ride because V2 is multithreaded, which adds spice. With the MIDI revamp will come a new Options dialog, based on standard property sheets, again borrowed from the Polymeter project. Various other features are also waiting on an options dialog.

  2. Mirroring. I had a long chat with GPT about this last night. For programming projects, I talk to version o1; slow but comprehensive. It was a bit vague at first, but I busted its imaginary balls a bit, and it coughed up some solid-looking Direct2D code for mirroring. The method is quite complex and may affect performance, but it’s definitely worth a try. The core problem is that there’s no way to copy a rectangle within the back buffer anymore. That went away with DirectDraw and the move to GPUs. In D2D a buffer can be a source or a destination, but never both. So instead, we must use command lists, which are an advanced feature of D3D. I expect some R&D and a fairly steep learning curve.

  3. Playlists. While we’re redoing the MIDI mapping, we should reconsider the Playlist file format, which is how OG Whorld stored its MIDI settings. That file format is a mess and needs a rethink.

  4. Image export. Exporting a PNG will be fairly easy. The tricky part is learning to use WIC, but GPT will help with that. In fact I already have a basic export working as of today, only the UI remains to be done.

  5. Snapshots A snapshots is Whorld's vector format. It captures the entire ring stack, along with all related state information, in a highly compact form that’s losslessly scalable because no pixels are involved. In the original version, snapshot format depends heavily on Whorld’s internal structure, which makes the format somewhat fragile. Perhaps we can do better this time. I have considerable personal motivation to work on snapshots, because I have a large and beloved collection of snapshots, some of which have potential as art, and they will definitely look vastly better with antialiasing.

  6. TRANSPARENCY. It’s got the word “trans” in it, so we gotta go for it. It will hurt performance, but it’ll look so cool it’ll be worth doing a low ring count. Transparency will definitely work in fill mode, and that will be the most awesome use of it, because you will partially see through the “cone” revealing structure that was previously hidden. I envision two types: One where we change the alpha of the entire ring list, and another where the rings gradually get more transparent as they migrate further from the center. Both have potential.

After that, I’m not sure. XOR is vaguely possible, but it would be hard. D2D supports other blending modes, just not that one. But there is the option to create a so-called “effect” which is a type of custom blending mode. It means writing shader code. With GPT’s help, I can probably manage it, but not soon.

Tuesday, February 11, 2025

Whorld V2 is coming!

Whorld V2 renders much faster, allowing higher ring counts and frame rates, and also features anti-aliasing, eliminating jaggies. The UI is completely revamped, with many enhancements such as dockable panes, unlimited undo, and much improved handling of full screen mode, in both single and dual monitor configurations.

It's been a long time since there was news on the Whorld front. That's partly because I've been busy with other projects, but also because I needed to learn new skills. Under the hood, all of the graphics are now done in Direct2D 1.1, and the design is fully multi-threaded so that the UI and the rendering are completely disentangled. Rendering is silky smooth and stable at 60 frames per second, even with high ring counts.

Here's the done list:

  • Direct2D 1.1 initialization and rendering structure (mostly borrowed from FauveEdit).
  • Multithreaded design - all Direct2D access is by a worker thread (render thread).
  • Full screen and windowed modes, in both single and dual monitor configurations.
  • Parameter editing - via row dialog, imported from legacy version, with a few tweaks.
  • Data architecture - CWhorldBase, with structures to organize all app data.
  • Display frame rate in status bar - measure elapsed time via performance counter.
  • Routing of document update notifications, adhering to the MFC SDI scheme.
  • Undo system - imported from other projects, integrates with MFC.

Still to do:

  • Draw the Whorld graphics, replacing the placeholder drawing - first the basics, then:
    • Bézier curves.
    • Fill mode (as opposed to line mode).
    • Quad mirroring.
    • XOR mode (would require custom a Direct2D effect).
  • Options dialog, probably based on CMFCPropertyGridCtrl as in the Polymeter app.
  • MIDI support, with "learn" mode - much can be imported from the Polymeter app.
  • Bitmap export - somewhat complicated by multithreaded design.
  • Snapshot read/write/display - a vector-based format that captures the graphics state.

Saturday, August 15, 2009

Curve Angle

some benchmarks, all in microseconds, 100 rings, running average of 100 samples:

574 previous version: no curve angle support
765 curve angle support, but curve angle2 calc commented out
767 curve angle support, but curve angle2 calc skipped by zero test

same test but with 20 sides instead of 5:
1.843 previous version: no curve angle support
2.547 curve angle support, but curve angle2 calc commented out
2.564 curve angle support, but curve angle2 calc skipped by zero test

same test on bad box:
297 previous version: no curve angle support
378 curve angle support, but curve angle2 calc skipped by zero test

Either way best case is curve calc now takes approx. 33% longer. However the good news is the entire point calc is only about 20% of the draw function time *in line mode*.

Tuesday, July 14, 2009

Whorld Choir

According to my calculations, the plasmagon patch would take almost 6 million years to return to its starting position, and that's not counting the pinwheel global oscillator. Yikes! I guess I can see why there's no such thing as LCM for real numbers but it bothers me somehow.


0.03 33.33333333
0.07 14.28571429
0.08 12.5
0.09 11.11111111
0.003 333.3333333
0.004 250
0.005 200
0.006 166.6666667
1.83715E+14 seconds
5825579.068 years

Saturday, April 05, 2008

Whorld/GL: only the beginning

It seems that Whorld's drawing code can't be directly ported to OpenGL, because OpenGL doesn't even handle concave polygons, never mind self-overlapping polygons or shapes composed of Bezier curves. The good news is, it might be possible to use a combination of GDI and OpenGL. GDI provides a function called FlattenPath, which turns a path containing Bezier curves into a (very large) set of line segments. This flattened path could then be passed to the glu tessellation functions, which would turn the path into a set of simple polygons that could be rendered directly in OpenGL. It all sounds a bit Rube Goldberg, but it might be worth it to achieve transparency and the many other effects available in OpenGL. The GDI FlattenPath function seems to be very fast, at least compared to the actual rendering done by StrokeAndFillPath.

Wednesday, February 06, 2008

compensating frame rate

If you change the frame rate, and want your patches to look the same, you must compensate the following variables: Ring Growth, and Color Speed. If you double the frame rate, halve Ring Growth and Color Speed. Yes, the app should take care of this for you, but for the moment it doesn't.

Patches that depend on a particular relationship to the frame rate (e.g. the Seed of Life patch) will require further tweaking.

Note that changing the frame rate will change the relative speed of the cascading delete, and there's currently no way to compensate for it. Sorry!

Saturday, January 26, 2008

bezier benchmarks (categorized)

frames = 500
playlist: new curve alg bench.whl
patch: default
Master Offsets:
Star Factor = 1
Even Curve = .2
Odd Curve = .2
draw mode = line (or fill/outline)
NOTE: app should be MAXIMIZED at 1024 x 768

Dell / W2K
draw mode: lines only
back buffer: auto (video memory)
OnTimer = 0.054 (0.29%)
Draw = 18.992 (99.71%)
math = 0.600 (3.15%)
GDI = 18.393 (96.57%)
total = 19.047
secs@frame = 0.38 (26.25 FPS)
2nd pass: similar

Bad Box / XP
draw mode: lines only
back buffer: auto (video memory)
OnTimer = 0.008 (0.16%)
Draw = 4.854 (99.84%)
math = 0.263 (5.41%)
GDI = 4.590 (94.42%)
total = 4.862
secs@frame = 0.010 (102.84 FPS)

Bad Box / XP
draw mode: fill/outline
back buffer: auto (video memory)
OnTimer = 0.008 (0.05%)
Draw = 16.735 (99.95%)
math = 0.270 (1.61%)
GDI = 16.466 (98.34%)
total = 16.744
secs@frame = 0.033 (29.86 FPS)
2nd pass: similar
not too good!

Bad Box / XP
draw mode: fill/outline
back buffer: system memory
OnTimer = 0.016 (0.09%)
Draw = 17.916 (99.91%)
math = 0.276 (1.54%)
GDI = 17.639 (98.37%)
total = 17.931
secs@frame = 0.036 (27.88 FPS)
2nd pass: similar
system memory is not helping

Z Dell / XP
FPS: 25
draw mode: fill/outline
back buffer: auto (video memory)
OnTimer = 0.014 (0.06%)
Draw = 21.519 (99.94%)
math = 0.145 (0.67%)
GDI = 21.374 (99.26%)
total = 21.533
secs@frame = 0.043 (23.22 FPS)
7 FPS *SLOWER* than Bad Box with back buffer in video memory? just terrible

Z Dell / XP
FPS: 25
draw mode: fill/outline
back buffer: system memory
OnTimer = 0.017 (0.12%)
Draw = 14.446 (99.98%)
math = 0.146 (1.01%)
GDI = 14.300 (98.88%)
total = 14.463
secs@frame = 0.029 (34.57 FPS)
brand-new 2.66 GHz Dell is maximum 5 FPS faster than 3-year old Bad Box?
system memory *is* helping in this case, WTF?

Friday, January 25, 2008

benchmarks for improved curve generation

In previous versions, curves were unstable (i.e. they would jitter) when star factor was negative. This occurred because the curve points were being computed from integer vertices. The new version computes the curve points from real vertices. Also, the curves points are now generated at the same time as the vertices, in a single loop, instead of in a second pass. This is more efficient, and eliminates the need for a second point array.

Comparing 1.6.06 and 1.7.03

Only the math portion of Draw is compared.

playlist: new curve alg bench.whl
patch: default
Master Offsets:
Star Factor = 1
Even Curve = .2
Odd Curve = .2

Benchmark includes code between
while (NextPos != NULL) {
and
rp.Delete = !RingVisible;
plus MakeCurves in 1.6.06

1000 frames

pass 1.6.06 1.7.03
---- ------- -------
#1 .001157 .000998
#2 .001161 .000995 (14% faster)

In summary, the Draw math takes less time in 1.7.03, despite having added some major new features (global parameters, curve shear). Presumably the speedup is due to a combination of better-optimized code and reduced memory usage. The global parameters aren't free, but their cost is minimal: 1.7.03 drops to around .000930 if m_GlobRing is removed from Draw.

Total time for Draw in 1.7.03: .025 in line mode, off the chart in fill mode

Same exact tests, but on the bad box:

pass 1.6.06 1.7.03
---- ------- -------
#1 .000654 .000468
#2 .000653 .000468 (28% faster)

Total time for Draw: .009 in line mode, .022 in fill mode

Monday, June 19, 2006

swarm

in AddRing:

int swcnt; // number of vertices in swarm polygon
int swidx; // index of swarm polygon's current vertex
int swrad; // radius of swarm polygon, in pixels

double theta = (PI * 2) * (double(swidx) / swcnt); // can be better optimized
Ring.Shift.x += sin(theta) * swrad;
Ring.Shift.y += cos(theta) * swrad;
swidx++;
swidx %= swcnt; // can be better optimized

NOTE that this feature requires the skew curve fix (see above), otherwise curves will be horribly distorted.

skew curve fix

Skew distorts curved rings; to avoid this, MakeCurves must use the skewed origin.

wrong:
iorg = CPoint(round(org.x), round(org.y));

correct:
iorg = CPoint(round(xshift), round(yshift));

Monday, June 05, 2006

maximize list control within playlist dialog


void CPlaylistDlg::OnHideControls()
{
m_HideControls ^= 1;
CWnd *wp = GetWindow(GW_CHILD);
while (wp != NULL) {
if (wp != &m_List)
wp->ShowWindow(m_HideControls ? SW_HIDE : SW_SHOW);
wp = wp->GetNextWindow();
}
PostMessage(WM_SIZE);
}

void CPlaylistDlg::OnSize(UINT nType, int cx, int cy)
{
CToolDlg::OnSize(nType, cx, cy);
if (m_HideControls) {
CRect r;
GetClientRect(r);
m_List.MoveWindow(r);
} else
m_Resize.OnSize();
}

void CPlaylistDlg::OnShowWindow(BOOL bShow, UINT nStatus)
{
CToolDlg::OnShowWindow(bShow, nStatus);
if (bShow && !m_HideControls)
m_Resize.OnSize();
}

Friday, June 02, 2006

MIDI support for video functions

Assuming MIDI ranges equal 5 (the default):

Video Select

num MIDI Video
pad Values Clip
0 0..12 0
1 13..25 1
2 26..38 2
3 39..51 3
4 52..63 4
5 64..76 5
6 77..89 6
7 90..101 7
8 102..114 8
9 115..126 9
. 127 None

Note that a value of 127 disables video. If this is undesirable, set Video Select's MIDI range to 4.99 instead of 5.

Video Blending

num MIDI
pad Values Blending description ROP code GDI name
0 0..12 ~Src & Dst AND inverted source with destination DSna
1 13..25 ~Src | Dst OR inverted source with destination DSno MERGEPAINT
2 26..38 Src & ~Dst AND source with inverted destination SDna SRCERASE
3 39..51 Src & ~Dst OR source with inverted destination SDno
4 52..63 Src & Dst AND source with destination DSa SRCAND
5 64..76 Src | Dst OR source with destination DSo SRCPAINT
6 77..89 Src ^ Dst XOR source with destination DSx SRCINVERT
7 90..101 ~(Src & Dst) AND source with destination, invert result DSan
8 102..114 ~(Src | Dst) OR source with destination, invert result DSon NOTSRCERASE
9 115..127 ~(Src ^ Dst) XOR source with destination, invert result DSxn

Video Cycle Length

num MIDI Cycle
pad Values Length
1 0..12 1
2 13..25 2
3 26..38 3
4 39..51 4
5 52..63 5
6 64..76 6
7 77..89 7
8 90..101 8
9 102..114 9
0 115..127 10

Note that numpad zero sets the cycle length to "all" which is effectively 10.