foxygit / doom Log in
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);