Consume background thread movie decoding.

This commit is contained in:
Brandon W
2024-08-07 10:04:18 -07:00
committed by teejusb
parent 7492793463
commit d4ab7b12ed
5 changed files with 134 additions and 143 deletions
-1
View File
@@ -43,7 +43,6 @@ void Banner::Load( RageTextureID ID, bool bIsBanner )
m_bScrolling = false;
TEXTUREMAN->DisableOddDimensionWarning();
TEXTUREMAN->VolatileTexture( ID );
Sprite::Load( ID );
TEXTUREMAN->EnableOddDimensionWarning();
};
+42 -27
View File
@@ -124,11 +124,6 @@ MovieDecoder_FFMpeg::MovieDecoder_FFMpeg()
MovieDecoder_FFMpeg::~MovieDecoder_FFMpeg()
{
if( m_iCurrentPacketOffset != -1 )
{
avcodec::av_packet_unref( &m_Packet );
m_iCurrentPacketOffset = -1;
}
if (m_swsctx)
{
avcodec::sws_freeContext(m_swsctx);
@@ -156,19 +151,13 @@ void MovieDecoder_FFMpeg::Init()
m_iEOF = 0;
m_fTimestamp = 0;
m_fLastFrameDelay = 0;
m_iFrameNumber = -1; /* decode one frame and you're on the 0th */
m_iFrameNumber = 0;
m_totalFrames = 0;
m_fTimestampOffset = 0;
m_fLastFrame = 0;
m_swsctx = nullptr;
m_avioContext = nullptr;
m_buffer = nullptr;
if( m_iCurrentPacketOffset != -1 )
{
avcodec::av_packet_unref( &m_Packet );
m_iCurrentPacketOffset = -1;
}
}
/* Read until we get a frame, EOF or error. Return -1 on error, 0 on EOF, 1 if we have a frame. */
@@ -211,10 +200,28 @@ int MovieDecoder_FFMpeg::DecodeFrame( float fTargetTime )
float MovieDecoder_FFMpeg::GetTimestamp() const
{
return m_fTimestamp - m_fTimestampOffset;
// Always display the first frame.
if (m_iFrameNumber == 0) {
return 0;
}
// In a logical situation, this means that display is outpacing decoding.
if (m_iFrameNumber >= m_FrameBuffer.size()) {
return 0;
}
return m_FrameBuffer[m_iFrameNumber].frameTimestamp;
}
bool MovieDecoder_FFMpeg::IsCurrentFrameReady() {
// We're displaying faster than decoding. Do not even try to display the frame.
if (m_iFrameNumber >= m_FrameBuffer.size()) {
return false;
}
// If the whole movie is decoded, then the frame is definitely ready.
if (m_iEOF) {
return true;
}
std::lock_guard<std::mutex>(m_FrameBuffer[m_iFrameNumber].lock);
if (!m_FrameBuffer[m_iFrameNumber].decoded) {
LOG->Info("Frame %i not decoded, total frames: %i", m_iFrameNumber, m_totalFrames);
@@ -503,32 +510,41 @@ int MovieDecoder_FFMpeg::DecodePacket( float fTargetTime )
return 0; /* packet done */
}
void MovieDecoder_FFMpeg::GetFrame( RageSurface *pSurface )
bool MovieDecoder_FFMpeg::GetFrame(RageSurface* pSurface)
{
avcodec::AVFrame pict;
pict.data[0] = (unsigned char *) pSurface->pixels;
pict.data[0] = (unsigned char*)pSurface->pixels;
pict.linesize[0] = pSurface->pitch;
/* XXX 1: Do this in one of the Open() methods instead?
* XXX 2: The problem of doing this in Open() is that m_AVTexfmt is not
* already initialized with its correct value.
*/
if( m_swsctx == nullptr )
if (m_swsctx == nullptr)
{
m_swsctx = avcodec::sws_getCachedContext( m_swsctx,
GetWidth(), GetHeight(), m_pStreamCodec->pix_fmt,
GetWidth(), GetHeight(), m_AVTexfmt,
sws_flags, nullptr, nullptr, nullptr );
if( m_swsctx == nullptr )
m_swsctx = avcodec::sws_getCachedContext(m_swsctx,
GetWidth(), GetHeight(), m_pStreamCodec->pix_fmt,
GetWidth(), GetHeight(), m_AVTexfmt,
sws_flags, nullptr, nullptr, nullptr);
if (m_swsctx == nullptr)
{
LOG->Warn("Cannot initialize sws conversion context for (%d,%d) %d->%d", GetWidth(), GetHeight(), m_pStreamCodec->pix_fmt, m_AVTexfmt);
return;
return false;
}
}
avcodec::sws_scale( m_swsctx,
m_Frame->data, m_Frame->linesize, 0, GetHeight(),
pict.data, pict.linesize );
avcodec::sws_scale(m_swsctx,
m_FrameBuffer[m_iFrameNumber].frame.data, m_FrameBuffer[m_iFrameNumber].frame.linesize, 0, GetHeight(),
pict.data, pict.linesize);
// Don't advance the frame number past the (potential) end of the buffer.
// This can happen if display is outpacing decoding, or if we're at the
// end of file.
if (m_iFrameNumber >= (m_totalFrames - 1)) {
return m_iEOF;
}
m_iFrameNumber++;
return false;
}
static RString averr_ssprintf( int err, const char *fmt, ... )
@@ -677,8 +693,7 @@ void MovieDecoder_FFMpeg::Close()
void MovieDecoder_FFMpeg::Rewind()
{
avcodec::av_seek_frame( m_fctx, -1, 0, 0 );
OpenCodec();
m_iFrameNumber = 0;
}
RageSurface *MovieDecoder_FFMpeg::CreateCompatibleSurface( int iTextureWidth, int iTextureHeight, bool bPreferHighColor, MovieDecoderPixelFormatYCbCr &fmtout )
+3 -1
View File
@@ -70,7 +70,9 @@ public:
void Close();
void Rewind();
void GetFrame( RageSurface *pOut );
// This draws a frame from the buffer onto the provided RageSurface.
// Returns true if returning the last frame in the movie.
bool GetFrame(RageSurface* pOut);
int DecodeFrame( float fTargetTime );
// Decode a single frame. Return -2 on cancel, -1 on error, 0 on EOF, 1 if we have a frame.
+82 -113
View File
@@ -6,6 +6,7 @@
#include "RageSurface.h"
#include "RageTextureManager.h"
#include "RageTextureRenderTarget.h"
#include "RageTimer.h"
#include "RageUtil.h"
#include "Sprite.h"
@@ -43,30 +44,38 @@ MovieTexture_Generic::MovieTexture_Generic( RageTextureID ID, MovieDecoder *pDec
RString MovieTexture_Generic::Init()
{
RString sError = m_pDecoder->Open( GetID().filename );
if( sError != "" )
RString sError = m_pDecoder->Open(GetID().filename);
if (sError != "")
return sError;
CreateTexture();
CreateFrameRects();
/* Decode one frame, to guarantee that the texture is drawn when this function returns. */
int ret = m_pDecoder->DecodeFrame( -1 );
if( ret == -1 )
return ssprintf( "%s: error getting first frame", GetID().filename.c_str() );
if( ret == 0 )
{
/* There's nothing there. */
return ssprintf( "%s: EOF getting first frame", GetID().filename.c_str() );
}
m_ImageWaiting = FRAME_DECODED;
// Draw the first frame immediately, to guarantee that the texture is drawn
// when this function returns--if possible.
if (m_pDecoder->DecodeNextFrame() < 0) {
LOG->Trace("Failure to decode first frame of video file \"%s\"", GetID().filename.c_str());
m_failure = true;
return RString("Failure to display movie.");
};
UpdateMovie(0);
LOG->Trace( "Resolution: %ix%i (%ix%i, %ix%i)",
m_iSourceWidth, m_iSourceHeight,
m_iImageWidth, m_iImageHeight, m_iTextureWidth, m_iTextureHeight );
decoding_thread = std::make_unique<std::thread>([this]() {
LOG->Trace("Beginning to decode video file \"%s\"", GetID().filename.c_str());
auto timer = RageTimer();
UpdateFrame();
int ret = m_pDecoder->DecodeMovie();
if (ret == -1) {
m_failure = true;
}
LOG->Trace("Done decoding video file \"%s\", took %f seconds", GetID().filename.c_str(), timer.Ago());
});
LOG->Trace("Resolution: %ix%i (%ix%i, %ix%i)",
m_iSourceWidth, m_iSourceHeight,
m_iImageWidth, m_iImageHeight, m_iTextureWidth, m_iTextureHeight);
CHECKPOINT_M("Generic initialization completed. No errors found.");
@@ -75,8 +84,11 @@ RString MovieTexture_Generic::Init()
MovieTexture_Generic::~MovieTexture_Generic()
{
if( m_pDecoder )
if (m_pDecoder) {
m_pDecoder->Cancel();
decoding_thread->join();
m_pDecoder->Close();
}
/* m_pSprite may reference the texture; delete it before DestroyTexture. */
delete m_pSprite;
@@ -361,88 +373,33 @@ bool MovieTexture_Generic::DecodeFrame()
/*
* Returns:
* == 0 if the currently decoded frame is ready to be displayed
* > 0 (seconds) if it's not yet time to display;
* <= 0 if it's time for the next frame to display
* > 0 (seconds) if it's not yet time to display
*/
float MovieTexture_Generic::CheckFrameTime()
{
if( m_fRate == 0 )
if (m_fRate == 0) {
return 1; // "a long time until the next frame"
const float fOffset = (m_pDecoder->GetTimestamp() - m_fClock) / m_fRate;
/* If we're ahead, we're decoding too fast; delay. */
if( fOffset > 0.00001f )
{
if( m_bFrameSkipMode )
{
/* We're caught up; stop skipping frames. */
LOG->Trace( "stopped skipping frames" );
m_bFrameSkipMode = false;
}
return fOffset;
}
/*
* We're behind by -Offset seconds.
*
* If we're just slightly behind, don't worry about it; we'll simply
* not sleep, so we'll move as fast as we can to catch up.
*
* If we're far behind, we're short on CPU. Skip texture updates; this
* is a big bottleneck on many systems.
*
* If we hit a threshold, start skipping frames via #1. If we do that,
* don't stop once we hit the threshold; keep doing it until we're fully
* caught up.
*
* We should try to notice if we simply don't have enough CPU for the video;
* it's better to just stay in frame skip mode than to enter and exit it
* constantly, but we don't want to do that due to a single timing glitch.
*/
const float FrameSkipThreshold = 0.5f;
if( -fOffset >= FrameSkipThreshold && !m_bFrameSkipMode )
{
LOG->Trace( "(%s) Time is %f, and the movie is at %f. Entering frame skip mode.",
GetID().filename.c_str(), m_fClock, m_pDecoder->GetTimestamp() );
m_bFrameSkipMode = true;
}
return 0;
return (m_pDecoder->GetTimestamp() - m_fClock) / m_fRate;
}
/* Decode data. */
void MovieTexture_Generic::UpdateMovie( float fSeconds )
void MovieTexture_Generic::UpdateMovie(float fSeconds)
{
m_fClock += fSeconds * m_fRate;
/* We might need to decode more than one frame per update. However, there
* have been bugs in ffmpeg that cause it to not handle EOF properly, which
* could make this never return, so let's play it safe. */
int iMax = 4;
while( --iMax )
{
/* If we don't have a frame decoded, decode one. */
if( m_ImageWaiting == FRAME_NONE )
{
if( !DecodeFrame() )
break;
m_ImageWaiting = FRAME_DECODED;
}
/* If we have a frame decoded, see if it's time to display it. */
float fTime = CheckFrameTime();
if ( fTime <= 0 )
{
UpdateFrame();
m_ImageWaiting = FRAME_NONE;
}
// Quick exit in case we failed to decode the movie.
if (m_failure) {
return;
}
m_fClock += fSeconds * m_fRate;
LOG->MapLog( "movie_looping", "MovieTexture_Generic::Update looping" );
// If the frame isn't ready, don't update. This does mean the video
// will "speed up" to catch up when decoding does outpace display.
//
// In practice, display should rarely, if ever, outpace decoding.
if (m_pDecoder->IsCurrentFrameReady() && CheckFrameTime() <= 0) {
UpdateFrame();
return;
}
}
void MovieTexture_Generic::UpdateFrame()
@@ -450,42 +407,54 @@ void MovieTexture_Generic::UpdateFrame()
/* Just in case we were invalidated: */
CreateTexture();
if( m_pTextureLock != nullptr )
if(m_pTextureLock != nullptr)
{
std::uintptr_t iHandle = m_pTextureIntermediate != nullptr? m_pTextureIntermediate->GetTexHandle(): this->GetTexHandle();
m_pTextureLock->Lock( iHandle, m_pSurface );
std::uintptr_t iHandle = m_pTextureIntermediate != nullptr ? m_pTextureIntermediate->GetTexHandle(): this->GetTexHandle();
m_pTextureLock->Lock(iHandle, m_pSurface);
}
m_pDecoder->GetFrame( m_pSurface );
if( m_pTextureLock != nullptr )
m_pTextureLock->Unlock( m_pSurface, true );
/* Are we looping? */
if (m_pDecoder->GetFrame(m_pSurface) && m_bLoop) {
LOG->Trace("File \"%s\" looping", GetID().filename.c_str());
m_pDecoder->Rewind();
// There's a gap in the audio when the music preview loops. This value
// is dynamic based on the ending and starting beats (see
// GameSoundManager.cpp::StartMusic).
//
// This means that the video will be off-sync during the loop, since
// the movie texture doesn't have access to the SoundManager's offset.
// Until it does, we can either freeze at the end of the video banner,
// or give it a best effort approximation (0.5 seconds).
m_fClock = 0.5;
};
if( m_pRenderTarget != nullptr )
if (m_pTextureLock != nullptr) {
m_pTextureLock->Unlock(m_pSurface, true);
}
if (m_pRenderTarget != nullptr)
{
CHECKPOINT_M( "About to upload the texture.");
CHECKPOINT_M("About to upload the texture.");
/* If we have no m_pTextureLock, we still have to upload the texture. */
if( m_pTextureLock == nullptr )
{
if (m_pTextureLock == nullptr) {
DISPLAY->UpdateTexture(
m_pTextureIntermediate->GetTexHandle(),
m_pSurface,
0, 0,
m_pSurface->w, m_pSurface->h );
m_pSurface->w, m_pSurface->h);
}
m_pRenderTarget->BeginRenderingTo( false );
m_pRenderTarget->BeginRenderingTo(false);
m_pSprite->Draw();
m_pRenderTarget->FinishRenderingTo();
}
else
{
if( m_pTextureLock == nullptr )
{
else {
if (m_pTextureLock == nullptr) {
DISPLAY->UpdateTexture(
m_uTexHandle,
m_pSurface,
0, 0,
m_iImageWidth, m_iImageHeight );
m_iImageWidth, m_iImageHeight);
}
}
}
@@ -506,19 +475,19 @@ void MovieTexture_Generic::Reload()
{
}
void MovieTexture_Generic::SetPosition( float fSeconds )
void MovieTexture_Generic::SetPosition(float fSeconds)
{
/* We can reset to 0, but I don't think this API supports fast seeking
* yet. I don't think we ever actually seek except to 0 right now,
* anyway. XXX */
if( fSeconds != 0 )
// In theory, we can math out fSeconds and frame counts to seek the video,
// but there's likely no practical use case of this.
if (fSeconds != 0)
{
LOG->Warn( "MovieTexture_Generic::SetPosition(%f): non-0 seeking unsupported; ignored", fSeconds );
LOG->Warn("MovieTexture_Generic::SetPosition(%f): non-0 seeking unsupported; ignored", fSeconds);
return;
}
LOG->Trace( "Seek to %f", fSeconds );
m_bWantRewind = true;
LOG->Trace("Seek to %f", fSeconds);
m_fClock = 0;
m_pDecoder->Rewind();
}
std::uintptr_t MovieTexture_Generic::GetTexHandle() const
+7 -1
View File
@@ -4,6 +4,7 @@
#include "MovieTexture.h"
#include <cstdint>
#include <thread>
class FFMpeg_Helper;
struct RageSurface;
@@ -51,7 +52,7 @@ public:
/*
* Get the currently-decoded frame.
*/
virtual void GetFrame( RageSurface *pOut ) = 0;
virtual bool GetFrame( RageSurface *pOut ) = 0;
/* Return the dimensions of the image, in pixels (before aspect ratio
* adjustments). */
@@ -117,12 +118,17 @@ public:
private:
MovieDecoder *m_pDecoder;
std::unique_ptr<std::thread> decoding_thread;
float m_fRate;
enum {
FRAME_NONE, /* no frame available; call GetFrame to get one */
FRAME_DECODED /* frame decoded; waiting until it's time to display it */
} m_ImageWaiting;
bool m_bLoop;
// If true, halts all decoding and display.
bool m_failure = false;
bool m_bWantRewind;
enum State { DECODER_QUIT, DECODER_RUNNING } m_State;