diff --git a/stepmania/src/arch/Sound/DSoundHelpers.cpp b/stepmania/src/arch/Sound/DSoundHelpers.cpp index cb2477de40..87189306f0 100644 --- a/stepmania/src/arch/Sound/DSoundHelpers.cpp +++ b/stepmania/src/arch/Sound/DSoundHelpers.cpp @@ -334,6 +334,167 @@ DSoundBuf::~DSoundBuf() buf->Release(); } +/* Figure out if we've underrun, and act if appropriate. */ +void DSoundBuf::CheckUnderrun( int cursorstart, int cursorend, int chunksize ) +{ + /* XXX We can figure out if we've underrun, and increase the write-ahead + * when it happens. Two problems: + * 1. It's ugly to wait until we actually underrun. (We could store the + * write-ahead, though.) + * 2. We don't want a random underrun (eg. virus scanner starts) to + * permanently increase our write-ahead. We want the smallest possible + * that will give us reliable audio in normal conditions. I'm not sure + * of a robust way to do this. + * + * Also, writeahead should be a static (all buffers write ahead the same + * amount); writeahead in the ctor should be a hint only (initial value), + * and the sound driver should query a sound to get the current writeahead + * in GetLatencySeconds(). + */ + + /* If the buffer is full, we can't be underrunning. */ + if( buffer_bytes_filled >= buffersize ) + return; + + /* If there's no data in the buffer at all, then we've completely underrun. Our + * write cursor is irrelevant; we might be unrelated to the play position completely. + * Realign. */ + if( buffer_bytes_filled == 0 ) + { + /* There's no data filled at all. We've completely underrun. */ + /* XXX */ + + int missed_by = cursorend - write_cursor; + wrap( missed_by, buffersize ); + int first_byte_filled = write_cursor-buffer_bytes_filled; + wrap( first_byte_filled, buffersize ); + + LOG->Trace("major underrun: %i..%i filled but cursor at %i..%i (missed it by %i) %i/%i", + first_byte_filled, write_cursor, cursorstart, cursorend, + missed_by, buffer_bytes_filled, buffersize); + + write_cursor = cursorstart; + } + + /* + * Invariant: the filled region now starts at the beginning of the play region. + * The "Update buffer_bytes_filled" logic, combined with the above empty buffer + * check, guarantees this. + * + * This is important, so let's consider all cases as of the start of get_output_buf: + * + * 1: ....ffff....pppp... (no overlap) + * invalid: buffer_bytes_filled should have been set to 0. + * + * 2: ....ffff.... (overlap, filled is earlier) + * ..pppp...... + * invalid: buffer_bytes_filled should have been set to 2, resulting in: + * ......ff.... + * ...pppp..... + * + * 3: ......ffff.. (overlap, filled is later) + * ....pppp.... + * invalid: This case can only happen if the play cursor has wrapped all the + * way around, in which case buffer_bytes_filled should have been set to 0. + * + * 4: ....ffff.... valid + * ....pppp.... + * 5: ....ff...... valid + * ....pppp.... + * 6: ............ valid (no data filled) + * ....pppp.... + */ + int first_byte_filled = write_cursor-buffer_bytes_filled; + wrap( first_byte_filled, buffersize ); + if( first_byte_filled != cursorstart ) + { + LOG->Trace("%i..%i filled but cursor at %i..%i (%i max)", + first_byte_filled, write_cursor, cursorstart, cursorend, buffer_bytes_filled ); + FAIL_M( "DSoundBuf::CheckUnderrun internal error" ); + } + + if( contained(first_byte_filled, write_cursor, cursorend) ) + { + /* The end of the play cursor has data. We haven't underrun (case #4). */ + return; + } + + /* We've underrun. Let's figure out whether, if we continue filling, we'll fill + * the buffer enough to stop underrunning. This is a little ugly, since we need + * to simulate. */ + int fake_buffer_bytes_filled = buffer_bytes_filled; + int fake_write_cursor = write_cursor; + while(1) + { + if( fake_buffer_bytes_filled > writeahead ) + break; + + int num_bytes_empty = writeahead-fake_buffer_bytes_filled; + if( num_bytes_empty < chunksize ) + break; + + num_bytes_empty = min(num_bytes_empty, buffersize - fake_write_cursor); +// num_bytes_empty = min(num_bytes_empty, chunksize); + + fake_buffer_bytes_filled += num_bytes_empty; + fake_write_cursor += num_bytes_empty; + wrap( fake_write_cursor, buffersize ); + } + + LOG->Trace("write_cursor %i, fake_write_cursor %i, fake_buffer_bytes_filled %i", + write_cursor, fake_write_cursor, fake_buffer_bytes_filled ); + + bool bCanCatchUp = contained(first_byte_filled, fake_write_cursor, cursorend); + /* + * If bCanCatchUp is false, then based on our writeahead and the chunksize, we'll + * never fill the buffer. This isn't fuzzy; we simply aren't filling enough, and + * we need to increase the writeahead. + * + * If bCanCatchUp is true, we simply fell behind and we can catch up. If this + * happens repeatedly, then while the writeahead is sufficient for the prefetch, + * the scheduler isn't keeping up, and we probably need to increase the writeahead. + * + * This is a tricky support issue. We don't want to have to adjust the writeahead + * dynamically if we can help it, since that means we've already underrun. It's + * much better to have a properly tuned writeahead for all systems to begin with. + * I'd much prefer to receive bug reports when the writeahead wasn't enough, so we + * can figure out the correct writeahead and use it by default. + * + * Also, unless we write the writeahead to a preference to keep it long-term (which + * I'm wary of doing), we'll go through this every game. + */ + if( bCanCatchUp ) + { + /* If we simply continue, we'll catch up. We'll probably have an audible + * glitch, but we can't prevent that. However, we can probably avoid an + * arrow skip. */ + /* XXX: if this happens repeatedly over a period of time, increase writeahead */ + int missed_by = cursorend - write_cursor; + wrap( missed_by, buffersize ); + LOG->Trace("minor underrun: %i..%i filled but cursor at %i..%i (missed it by %i) %i/%i", + first_byte_filled, write_cursor, cursorstart, cursorend, + missed_by, buffer_bytes_filled, buffersize); + return; + } + + /* + * Based on our writeahead and the chunksize, we'll never fill the buffer. We + * need to increase the writeahead. + */ + + int prefetch = cursorend - cursorstart; + wrap( prefetch, buffersize ); + + int old_writeahead = writeahead; + writeahead = writeahead * 4 / 3; + /* Snap to bytes_per_frame. */ + writeahead = (writeahead / bytes_per_frame()) * bytes_per_frame(); + writeahead = min( writeahead, buffersize ); + + LOG->Trace("insufficient writeahead: wants %i, but we'll only fill to %i; writeahead adjusted from %i to %i", + prefetch/bytes_per_frame(), fake_buffer_bytes_filled/bytes_per_frame(), old_writeahead/bytes_per_frame(), writeahead/bytes_per_frame() ); +} + bool DSoundBuf::get_output_buf( char **buffer, unsigned *bufsiz, int chunksize ) { ASSERT(!buffer_locked); @@ -369,65 +530,17 @@ bool DSoundBuf::get_output_buf( char **buffer, unsigned *bufsiz, int chunksize ) /* Update buffer_bytes_filled. */ { int first_byte_filled = write_cursor-buffer_bytes_filled; - if( first_byte_filled < 0 ) - first_byte_filled += buffersize; /* unwrap */ + wrap( first_byte_filled, buffersize ); /* The number of bytes that have been played since the last time we got here: */ int bytes_played = cursorstart - first_byte_filled; - if( bytes_played < 0 ) - bytes_played += buffersize; /* unwrap */ + wrap( bytes_played, buffersize ); buffer_bytes_filled -= bytes_played; buffer_bytes_filled = max( 0, buffer_bytes_filled ); } - /* XXX We can figure out if we've underrun, and increase the write-ahead - * when it happens. Two problems: - * 1. It's ugly to wait until we actually underrun. (We could store the - * write-ahead, though.) - * 2. We don't want a random underrun (eg. virus scanner starts) to - * permanently increase our write-ahead. We want the smallest possible - * that will give us reliable audio in normal conditions. I'm not sure - * of a robust way to do this. - * - * Also, writeahead should be a static (all buffers write ahead the same - * amount); writeahead in the ctor should be a hint only (initial value), - * and the sound driver should query a sound to get the current writeahead - * in GetLatencySeconds(). - */ - - /* If the buffer is full, we can't be underrunning. */ - if( buffer_bytes_filled < buffersize ) - { - int first_byte_filled = write_cursor-buffer_bytes_filled; - if( first_byte_filled < 0 ) - first_byte_filled += buffersize; /* unwrap */ - - /* Data between the play cursor and the write cursor is committed to be - * played. If we don't actually have data there, we've underrun. */ - - /* We're already underrunning, which means the play cursor has passed valid - * data. Let's move the cursor forward. */ - if( !contained(first_byte_filled, write_cursor, cursorstart) || - !contained(first_byte_filled, write_cursor, cursorend) ) - { - int missed_by = cursorend - write_cursor; - wrap( missed_by, buffersize ); - LOG->Trace("underrun %p: %i..%i filled but cursor at %i..%i (missed it by %i) %i/%i", - this, first_byte_filled, write_cursor, cursorstart, cursorend, - missed_by, buffer_bytes_filled, buffersize); - - /* Pretend the space between the play and write cursor is filled - * with data, and continue filling from there. */ - int no_write_zone_size = cursorend - cursorstart; - if( no_write_zone_size < 0 ) - no_write_zone_size += buffersize; /* unwrap */ - - buffer_bytes_filled = no_write_zone_size; - write_cursor = cursorend; - } - } - + CheckUnderrun( cursorstart, cursorend, chunksize ); /* If we already have enough bytes written ahead, stop. */ if( buffer_bytes_filled > writeahead ) @@ -447,7 +560,11 @@ bool DSoundBuf::get_output_buf( char **buffer, unsigned *bufsiz, int chunksize ) /* Don't fill more than one chunk at a time. This reduces the maximum * amount of time until we give data; that way, if we're short on time, * we'll give some data soon instead of lots of data later. */ - num_bytes_empty = min(num_bytes_empty, chunksize); + /* Let's not do this; treat chunksize as a "min bytes to fill" above (so we're not + * constantly filling in a few frames at a time), but not "max bytes to fill". This + * reduces cases where we don't fill the buffer as much as we should, and in practice + * makes the "increase the writeahead" logic work much better. */ +// num_bytes_empty = min(num_bytes_empty, chunksize); // LOG->Trace("gave %i at %i (%i, %i) %i filled", num_bytes_empty, write_cursor, cursor, write, buffer_bytes_filled); diff --git a/stepmania/src/arch/Sound/DSoundHelpers.h b/stepmania/src/arch/Sound/DSoundHelpers.h index 4fd046bbcf..26bb3cca55 100644 --- a/stepmania/src/arch/Sound/DSoundHelpers.h +++ b/stepmania/src/arch/Sound/DSoundHelpers.h @@ -53,6 +53,8 @@ private: int buffersize_frames() const { return buffersize / bytes_per_frame(); } int bytes_per_frame() const { return channels*samplebits/8; } + void CheckUnderrun( int cursorstart, int cursorend, int chunksize ); + IDirectSoundBuffer *buf; int channels, samplerate, samplebits, writeahead;