commit 5c3649b64ddf963fe60cd31bba5d359529383f34
Author: Roman Fomin <rfomin@gmail.com>
AuthorDate: Sat Nov 25 13:52:41 2023 +0700
Commit: Roman Fomin <rfomin@gmail.com>
CommitDate: Sat Nov 25 13:52:41 2023 +0700
win midi: Always prepare/unprepare the buffer before modifying
From Win API docs:
> After the header has been prepared, do not modify the buffer. After the driver
> is done using the buffer, call the midiOutUnprepareHeader function.
However, the docs also say this:
> The application can re-use the same buffer, or allocate multiple buffers and
> call midiOutPrepareHeader for each buffer. If you re-use the same buffer, it
> is not necessary to prepare the buffer each time. You can call
> midiOutPrepareHeader once at the beginning and then call
> midiOutUnprepareHeader once at the end.
Since ZMusic (GZDoom) and Portmidi (PrBoom+/DSDA-Doom) still call the
midiOutUnprepareHeader, we decided to call it as well. Also, we still have a
bug report with the occasional playback stuck. We can't reproduce it reliably,
but it seems to have been fixed after this change.
---
src/i_winmusic.c | 77 +++++++++++++++++++++++++++++++++++---------------------
1 file changed, 48 insertions(+), 29 deletions(-)
diff --git a/src/i_winmusic.c b/src/i_winmusic.c
index 910a8f59..63a30c74 100644
--- a/src/i_winmusic.c
+++ b/src/i_winmusic.c
@@ -150,6 +150,7 @@ typedef struct
byte *data;
unsigned int size;
unsigned int position;
+ boolean prepared;
} buffer_t;
static buffer_t buffer;
@@ -195,41 +196,36 @@ static void CALLBACK MidiStreamProc(HMIDIOUT hMidi, UINT uMsg,
}
}
-// Allocates the buffer and prepares the MIDI header. Set during initialization
-// by the main thread. BUFFER_INITIAL_SIZE should be large enough to avoid
-// reallocation by the MIDI thread during playback, due to a known memory bug
-// with midiOutUnprepareHeader() (detected by ASan). The calling thread must
-// have exclusive access to the shared resources in this function.
+// Unprepare MIDI header. The calling thread must have exclusive access to the
+// shared resources in this function.
-static void AllocateBuffer(const unsigned int size)
+static void UnprepareHeader(void)
{
+ // Avoid ASan detection. Commentary by Microsoft: "It looks like
+ // midiOutPrepareHeader() allocates with HeapAlloc(), and then
+ // midiOutUnprepareHeader() deallocates with GlobalFree(GlobalHandle
+ // (...)). By design, this kind of allocator mismatch is an issue that ASan
+ // is designed to catch. It is theoretically possible for us to support
+ // this kind of code, but it’s not very high priority since it is undefined
+ // behavior, though it happens to work right now outside of ASan."
+ // https://developercommunity.visualstudio.com/t/1597288
+
+#ifndef __SANITIZE_ADDRESS__
MIDIHDR *hdr = &MidiStreamHdr;
MMRESULT mmr;
- if (buffer.data)
+ mmr = midiOutUnprepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR));
+ if (mmr != MMSYSERR_NOERROR)
{
- // Windows doesn't always immediately clear the MHDR_INQUEUE flag, even
- // after midiStreamStop() is called. There doesn't seem to be any side
- // effect to just forcing the flag off.
- hdr->dwFlags &= ~MHDR_INQUEUE;
- mmr = midiOutUnprepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR));
- if (mmr != MMSYSERR_NOERROR)
- {
- MidiError("midiOutUnprepareHeader", mmr);
- }
+ MidiError("midiOutUnprepareHeader", mmr);
}
+#endif
+}
+static void AllocateBuffer(const unsigned int size)
+{
buffer.size = PADDED_SIZE(size);
buffer.data = I_Realloc(buffer.data, buffer.size);
-
- hdr->lpData = (LPSTR)buffer.data;
- hdr->dwBytesRecorded = 0;
- hdr->dwBufferLength = buffer.size;
- mmr = midiOutPrepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR));
- if (mmr != MMSYSERR_NOERROR)
- {
- MidiError("midiOutPrepareHeader", mmr);
- }
}
// Pads the buffer with zeros so that an integral number of DWORDs are stored.
@@ -248,6 +244,13 @@ static void WriteBufferPad(void)
static void WriteBuffer(const byte *ptr, unsigned int size)
{
+ if (buffer.prepared)
+ {
+ UnprepareHeader();
+ buffer.prepared = false;
+ buffer.position = 0;
+ }
+
if (buffer.position + size >= buffer.size)
{
AllocateBuffer(size + buffer.size * 2);
@@ -265,8 +268,18 @@ static void StreamOut(void)
MIDIHDR *hdr = &MidiStreamHdr;
MMRESULT mmr;
+ memset(hdr, 0, sizeof(*hdr));
hdr->lpData = (LPSTR)buffer.data;
hdr->dwBytesRecorded = buffer.position;
+ hdr->dwBufferLength = buffer.size;
+
+ mmr = midiOutPrepareHeader((HMIDIOUT)hMidiStream, hdr, sizeof(MIDIHDR));
+ if (mmr != MMSYSERR_NOERROR)
+ {
+ MidiError("midiOutPrepareHeader", mmr);
+ }
+
+ buffer.prepared = true;
mmr = midiStreamOut(hMidiStream, hdr, sizeof(MIDIHDR));
if (mmr != MMSYSERR_NOERROR)
@@ -1457,8 +1470,6 @@ static DWORD WINAPI PlayerProc(void)
// the end of the current loop iteration or when the thread exits.
EnterCriticalSection(&CriticalSection);
- buffer.position = 0;
-
switch (win_midi_state)
{
case STATE_STARTUP:
@@ -1846,8 +1857,11 @@ static void I_WIN_ShutdownMusic(void)
}
StreamStop();
- // Don't free the buffer to avoid calling midiOutUnprepareHeader() which
- // contains a memory error (detected by ASan).
+ if (buffer.prepared)
+ {
+ UnprepareHeader();
+ buffer.prepared = false;
+ }
mmr = midiStreamClose(hMidiStream);
if (mmr != MMSYSERR_NOERROR)
@@ -1856,6 +1870,11 @@ static void I_WIN_ShutdownMusic(void)
}
hMidiStream = NULL;
+ free(buffer.data);
+ buffer.data = NULL;
+ buffer.size = 0;
+ buffer.position = 0;
+
CloseHandle(hBufferReturnEvent);
CloseHandle(hStoppedEvent);
DeleteCriticalSection(&CriticalSection);