I got code checked out from mobyfab's git and got it up and running. But I ran into something weird and it is not clear to me if it's the driver code and the way it approaches DMA, or if it's a hardware bug, or hardware not quite configured right.
For a first test I set up a 1024-sample buffer and 10000 Hz playback. (I had tried 1000Hz playback first, turns out the calculation of the divisor for TIM6 doesn't think about whether the result is going to overflow, which it does when the clock is 84MHz and the target freq is 1KHz, since that's a divisor of 84000 and TIM6 only uses 16bit divisor. No biggie, just stay over about 1282 Hz playback and it works fine

)
For the callback, I didn't use any source data buffer or a DMA engine to write samples, I just put in a CPU loop to write ramped values - a global amplitude accumulator that gets incremented by a constant amount with each sample written. Value just wraps as you would expect so the output is just ramps from 0-max and then repeating.
So I get it running, and it's being called with requests for 512 samples each time, as expected. And the dest addresses ('samples' argument) are in bounds for the buffer I set up, at sample 0 or sample 512 base, so this all makes sense. The context as I understand it at this time is that the transfer is "complete" for the section you are about to fill, i.e. no more reads happening there, and that reads will be starting to happen on the alternate section while you are filling the requested section.
What happens on the scope is that periodically, I can see a one-sample glitch in the triangle wave, and the timing of that glitch is that it looks like an echo of a previous sample's amplitude, just delayed by an amount of time that is proportional to the buffer size. Change the buffer size and the distance from that glitch to where that value previously appeared changes with it.
My interpretation is that the DMA, when completing a transfer from either half-buffer section in the circular mode, goes ahead and reads one extra sample that it should not (wrapping around, staying in bounds, but reading from a section of buffer not yet re-generated with data for the current time). I suspect this happens too fast for the ISR to have a chance of putting fresh data in front of it, in fact it may happen before the ISR even triggers.
As an experiment, I changed my callback to write out the 512 samples as asked, but then to predict where the next sample would need to go and the value it would need to have - a 513th sample written. Just had to check for the end of buffer and wrap back to store it in that case. So if the DMA should have finished half-buffer A, and is asking me to refill it, I fill up A and then one sample at the start of B. Later on the opposite will happen and the ISR will be asked to refill section B, I still write all 512 samples there plus one sample at the start of A.
Voila, perfect triangle waves. At 20KHz or 200KHz, looks great.
If I understand this chronology right, the DMA should always be active on the section you are not currently being asked to fill. i.e. you can take your time to write the inactive section, because the DMA engine is currently starting to play the other one.
I think this behavior might be "hiding" in demo DAC code because the wave being played is a sine table being looped and in a way that matches the size of the buffer. Restated I don't think any of the buffer writes in that scenario ever replace an old value with a different value.
Here is some text from the STM32F4 ref manual that got me thinking along these lines:
Section 10.2: Doc ID 018909 Rev 5 299/1705
Direct mode
Each DMA request immediately initiates a transfer from/to the memory. When it is configured in direct mode (FIFO disabled), to transfer data in memory-to- peripheral mode, the DMA preloads only one data from the memory to the internal DMA controller (DMA) FIFO to ensure an immediate data transfer as soon as a DMA request is triggered by a peripheral.
So I am wondering in circular mode, if at the end of a section, where the last sample from that section has been put into the DAC, if one additional sample has been fetched and is now sitting in the FIFO, possibly fetched before its time. The ISR might be working at top speed to refill that location, but the old value has already made it into the FIFO.
BTW this is "non FIFO mode", not 16 sample lookahead or anything like that, just one sample that it wants to fetch.
If this behavior is non negotiable in the hardware, the driver design might need to change to acknowledge it, and ask the app to fill in sections that are offset ahead by one sample.