raze-gles/source/core/screenjob.cpp
Christoph Oelckers b23424485a - re-implemented VP8 support.
Since the decoder cannot handle sound, there's two options:

1: Use the same sounds as the video it replaces.
2: If an identifiable streamable sound with the same base name is found, it will be played along with the video.
Fixes #133
2020-09-05 11:58:19 +02:00

762 lines
18 KiB
C++

/*
** screenjob.cpp
**
** Generic asynchronous screen display
**
**---------------------------------------------------------------------------
** Copyright 2020 Christoph Oelckers
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in the
** documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
** derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/
#include "types.h"
#include "build.h"
#include "screenjob.h"
#include "i_time.h"
#include "v_2ddrawer.h"
#include "animlib.h"
#include "v_draw.h"
#include "s_soundinternal.h"
#include "animtexture.h"
#include "gamestate.h"
#include "menu.h"
#include "raze_sound.h"
#include "SmackerDecoder.h"
#include "movie/playmve.h"
#include "gamecontrol.h"
#include "animvpx.h"
#include "raze_music.h"
IMPLEMENT_CLASS(DScreenJob, true, false)
IMPLEMENT_CLASS(DImageScreen, true, false)
int DBlackScreen::Frame(uint64_t clock, bool skiprequest)
{
int span = int(clock / 1'000'000);
twod->ClearScreen();
return span < wait ? 1 : -1;
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
int DImageScreen::Frame(uint64_t clock, bool skiprequest)
{
if (tilenum > 0)
{
tex = tileGetTexture(tilenum, true);
}
if (!tex)
{
twod->ClearScreen();
return 0;
}
int span = int(clock / 1'000'000);
twod->ClearScreen();
DrawTexture(twod, tex, 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, DTA_LegacyRenderStyle, STYLE_Normal, TAG_DONE);
// Only end after having faded out.
return skiprequest ? -1 : span > waittime? 0 : 1;
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
class DAnmPlayer : public DScreenJob
{
// This doesn't need its own class type
anim_t anim;
TArray<uint8_t> buffer;
int numframes = 0;
int curframe = 1;
int frametime = 0;
int nextframetime = 0;
AnimTextures animtex;
const AnimSound* animSnd;
const int* frameTicks;
bool nostopsound;
public:
bool isvalid() { return numframes > 0; }
DAnmPlayer(FileReader& fr, const AnimSound* ans, const int *frameticks, bool nosoundcutoff)
: animSnd(ans), frameTicks(frameticks), nostopsound(nosoundcutoff)
{
buffer = fr.ReadPadded(1);
fr.Close();
if (ANIM_LoadAnim(&anim, buffer.Data(), buffer.Size() - 1) < 0)
{
return;
}
numframes = ANIM_NumFrames(&anim);
animtex.SetSize(AnimTexture::Paletted, 320, 200);
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
int Frame(uint64_t clock, bool skiprequest) override
{
int currentclock = int(clock * 120 / 1'000'000'000);
if (curframe > 4 && currentclock > frametime + 60)
{
Printf(PRINT_BOLD, "WARNING: slowdown in video playback, aborting\n");
soundEngine->StopAllChannels();
return -1;
}
if (currentclock < nextframetime - 1)
{
twod->ClearScreen();
DrawTexture(twod, animtex.GetFrame(), 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, DTA_Masked, false, TAG_DONE);
if (skiprequest && !nostopsound) soundEngine->StopAllChannels();
return skiprequest? -1 : 1;
}
animtex.SetFrame(ANIM_GetPalette(&anim), ANIM_DrawFrame(&anim, curframe));
frametime = currentclock;
twod->ClearScreen();
DrawTexture(twod, animtex.GetFrame(), 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, DTA_Masked, false, TAG_DONE);
int delay = 20;
if (frameTicks)
{
if (curframe == 1) delay = frameTicks[0];
else if (curframe < numframes - 1) delay = frameTicks[1];
else delay = frameTicks[2];
}
nextframetime += delay;
if (animSnd) for (int i = 0; animSnd[i].framenum >= 0; i++)
{
if (animSnd[i].framenum == curframe)
{
int sound = animSnd[i].soundnum;
if (sound == -1)
soundEngine->StopAllChannels();
else if (SoundEnabled())
soundEngine->StartSound(SOURCE_None, nullptr, nullptr, CHAN_AUTO, CHANF_UI, sound, 1.f, ATTN_NONE);
}
}
curframe++;
if (skiprequest && !nostopsound) soundEngine->StopAllChannels();
return skiprequest ? -1 : curframe < numframes? 1 : 0;
}
void OnDestroy() override
{
buffer.Reset();
animtex.Clean();
}
};
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
class DMvePlayer : public DScreenJob
{
InterplayDecoder decoder;
bool failed = false;
public:
bool isvalid() { return !failed; }
DMvePlayer(FileReader& fr) : decoder(SoundEnabled())
{
failed = !decoder.Open(fr);
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
int Frame(uint64_t clock, bool skiprequest) override
{
if (failed) return -1;
bool playon = decoder.RunFrame(clock);
twod->ClearScreen();
DrawTexture(twod, decoder.animTex().GetFrame(), 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, TAG_DONE);
return skiprequest ? -1 : playon ? 1 : 0;
}
void OnDestroy() override
{
decoder.Close();
}
};
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
class DVpxPlayer : public DScreenJob
{
bool failed = false;
FileReader fr;
AnimTextures animtex;
animvpx_codec_ctx codec;
const AnimSound* animSnd;
uint32_t convnumer;
uint32_t convdenom;
uint32_t msecsperframe;
uint64_t nextframetime;
int framenum = 0;
int lastsoundframe = -1;
public:
int soundtrack = -1;
public:
bool isvalid() { return !failed; }
DVpxPlayer(FileReader& fr_, const AnimSound* animSnd_, int origframedelay)
{
fr = std::move(fr_);
animSnd = animSnd_;
animvpx_ivf_header_t info;
int err = animvpx_read_ivf_header(fr, &info);
if (err)
{
Printf(PRINT_BOLD, "Failed reading IVF file: %s\n", animvpx_read_ivf_header_errmsg[err]);
failed = true;
}
if (animvpx_init_codec(&info, fr, &codec))
{
Printf(PRINT_BOLD, "Error initializing VPX codec.\n");
failed = true;
}
convnumer = 120 * info.fpsdenom;
convdenom = info.fpsnumer * origframedelay;
msecsperframe = scale(info.fpsdenom, 1000, info.fpsnumer);
nextframetime = 0;
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
int Frame(uint64_t clock, bool skiprequest) override
{
if (clock == 0)
{
if (soundtrack > 0)
{
Mus_Play(nullptr, fileSystem.GetFileFullName(soundtrack, false), false);
}
animtex.SetSize(AnimTexture::YUV, codec.width, codec.height);
}
bool stop = false;
if (clock > nextframetime)
{
nextframetime += 1'000'000 * msecsperframe;
uint8_t* pic;
int i = animvpx_nextpic(&codec, &pic);
if (i)
{
Printf(PRINT_BOLD, "Failed getting next pic: %s\n", animvpx_nextpic_errmsg[i]);
if (codec.errmsg)
{
Printf(PRINT_BOLD, " %s\n", codec.errmsg);
if (codec.errmsg_detail)
Printf(PRINT_BOLD, " detail: %s\n", codec.errmsg_detail);
}
stop = true;
}
if (!pic) stop = true;
if (!stop)
{
animtex.SetFrame(nullptr, pic);
}
framenum++;
int soundframe = convdenom ? scale(framenum, convnumer, convdenom) : framenum;
if (soundframe > lastsoundframe)
{
if (animSnd && soundtrack == -1) for (int i = 0; animSnd[i].framenum >= 0; i++)
{
if (animSnd[i].framenum == soundframe)
{
int sound = animSnd[i].soundnum;
if (sound == -1)
soundEngine->StopAllChannels();
else if (SoundEnabled())
soundEngine->StartSound(SOURCE_None, nullptr, nullptr, CHAN_AUTO, CHANF_UI, sound, 1.f, ATTN_NONE);
}
}
lastsoundframe = soundframe;
}
}
DrawTexture(twod, animtex.GetFrame(), 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit, TAG_DONE);
if (stop || skiprequest) Mus_Stop();
if (stop) return 0;
return skiprequest ? -1 : 1;
}
void OnDestroy() override
{
animvpx_uninit_codec(&codec);
}
};
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
class DSmkPlayer : public DScreenJob
{
SmackerHandle hSMK{};
uint32_t nWidth, nHeight;
uint8_t palette[768];
AnimTextures animtex;
TArray<uint8_t> pFrame;
int nFrameRate;
int nFrames;
bool fullscreenScale;
uint64_t nFrameNs;
int nFrame = 0;
const AnimSound* animSnd;
FString filename;
public:
bool isvalid() { return hSMK.isValid; }
DSmkPlayer(const char *fn, const AnimSound* ans, bool fixedviewport)
{
hSMK = Smacker_Open(fn);
if (!hSMK.isValid)
{
return;
}
Smacker_GetFrameSize(hSMK, nWidth, nHeight);
pFrame.Resize(nWidth * nHeight + std::max(nWidth, nHeight));
nFrameRate = Smacker_GetFrameRate(hSMK);
nFrameNs = 1'000'000'000 / nFrameRate;
nFrames = Smacker_GetNumFrames(hSMK);
Smacker_GetPalette(hSMK, palette);
fullscreenScale = (!fixedviewport || (nWidth <= 320 && nHeight <= 200) || nWidth >= 640 || nHeight >= 480);
animSnd = ans;
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
int Frame(uint64_t clock, bool skiprequest) override
{
int frame = clock / nFrameNs;
if (clock == 0)
{
animtex.SetSize(AnimTexture::Paletted, nWidth, nHeight);
}
twod->ClearScreen();
if (frame > nFrame)
{
Smacker_GetPalette(hSMK, palette);
Smacker_GetFrame(hSMK, pFrame.Data());
animtex.SetFrame(palette, pFrame.Data());
}
if (fullscreenScale)
{
DrawTexture(twod, animtex.GetFrame(), 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, TAG_DONE);
}
else
{
DrawTexture(twod, animtex.GetFrame(), 320, 240, DTA_VirtualWidth, 640, DTA_VirtualHeight, 480, DTA_CenterOffset, true, TAG_DONE);
}
if (frame > nFrame)
{
nFrame++;
Smacker_GetNextFrame(hSMK);
for (int i = 0; animSnd[i].framenum >= 0; i++)
{
if (animSnd[i].framenum == nFrame)
{
int sound = animSnd[i].soundnum;
if (sound == -1)
soundEngine->StopAllChannels();
else if (SoundEnabled())
soundEngine->StartSound(SOURCE_None, nullptr, nullptr, CHAN_AUTO, CHANF_UI, sound, 1.f, ATTN_NONE);
}
}
}
return skiprequest ? -1 : nFrame < nFrames ? 1 : 0;
}
void OnDestroy() override
{
Smacker_Close(hSMK);
soundEngine->StopAllChannels();
animtex.Clean();
}
};
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
DScreenJob* PlayVideo(const char* filename, const AnimSound* ans, const int* frameticks, bool nosoundcutoff)
{
auto nothing = []()->DScreenJob* { return Create<DScreenJob>(); };
if (!filename)
{
return nothing();
}
FileReader fr;
// first try as .ivf - but only if sounds are provided - the decoder is video only.
if (ans)
{
auto fn = StripExtension(filename);
DefaultExtension(fn, ".ivf");
fr = fileSystem.OpenFileReader(fn);
}
if (!fr.isOpen()) fr = fileSystem.OpenFileReader(filename);
if (!fr.isOpen())
{
int nLen = strlen(filename);
// Strip the drive letter and retry.
if (nLen >= 3 && isalpha(filename[0]) && filename[1] == ':' && filename[2] == '/')
{
filename += 3;
fr = fileSystem.OpenFileReader(filename);
}
if (!fr.isOpen())
{
Printf(PRINT_BOLD, "%s: Unable to open video\n", filename);
return nothing();
}
}
char id[20] = {};
fr.Read(&id, 20);
fr.Seek(-20, FileReader::SeekCur);
if (!memcmp(id, "LPF ", 4))
{
auto anm = Create<DAnmPlayer>(fr, ans, frameticks, nosoundcutoff);
if (!anm->isvalid())
{
Printf(PRINT_BOLD, "%s: invalid ANM file.\n", filename);
anm->Destroy();
return nothing();
}
return anm;
}
else if (!memcmp(id, "SMK2", 4))
{
fr.Close();
auto anm = Create<DSmkPlayer>(filename, ans, true); // Fixme: Handle Blood's video scaling behavior more intelligently.
if (!anm->isvalid())
{
Printf(PRINT_BOLD, "%s: invalid SMK file.\n", filename);
anm->Destroy();
return nothing();
}
return anm;
}
else if (!memcmp(id, "Interplay MVE File", 18))
{
auto anm = Create<DMvePlayer>(fr);
if (!anm->isvalid())
{
anm->Destroy();
return nothing();
}
return anm;
}
else if (!memcmp(id, "DKIF\0\0 \0VP80", 12))
{
auto anm = Create<DVpxPlayer>(fr, ans, frameticks? frameticks[1] : 0 );
if (!anm->isvalid())
{
anm->Destroy();
return nothing();
}
anm->soundtrack = LookupMusic(filename, true);
return anm;
}
// add more formats here.
else
{
Printf(PRINT_BOLD, "%s: Unknown video format\n", filename);
}
return nothing();
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
class ScreenJobRunner
{
enum
{
State_Clear,
State_Run,
State_Fadeout
};
TArray<JobDesc> jobs;
CompletionFunc completion;
int index = -1;
float screenfade;
bool clearbefore;
int64_t startTime = -1;
int64_t lastTime = -1;
int actionState;
int terminateState;
public:
ScreenJobRunner(JobDesc* jobs_, int count, CompletionFunc completion_, bool clearbefore_)
: completion(std::move(completion_)), clearbefore(clearbefore_)
{
jobs.Resize(count);
memcpy(jobs.Data(), jobs_, count * sizeof(JobDesc));
// Release all jobs from the garbage collector - the code as it is cannot deal with them getting collected. This should be removed later once the GC is working.
for (int i = 0; i < count; i++)
{
jobs[i].job->Release();
}
AdvanceJob(false);
}
~ScreenJobRunner()
{
DeleteJobs();
}
void DeleteJobs()
{
for (auto& job : jobs)
{
job.job->ObjectFlags |= OF_YesReallyDelete;
delete job.job;
}
jobs.Clear();
}
void AdvanceJob(bool skip)
{
if (index >= 0)
{
if (jobs[index].postAction) jobs[index].postAction();
jobs[index].job->Destroy();
}
index++;
while (index < jobs.Size() && (jobs[index].job == nullptr || (skip && jobs[index].ignoreifskipped)))
{
if (jobs[index].job != nullptr) jobs[index].job->Destroy();
index++;
}
actionState = clearbefore ? State_Clear : State_Run;
if (index < jobs.Size()) screenfade = jobs[index].job->fadestyle & DScreenJob::fadein ? 0.f : 1.f;
startTime = -1;
inputState.ClearAllInput();
}
int DisplayFrame()
{
auto& job = jobs[index];
auto now = I_nsTime();
bool processed = job.job->ProcessInput();
bool skiprequest = inputState.CheckAllInput() && !processed;
if (startTime == -1) lastTime = startTime = now;
if (M_Active())
{
startTime += now - lastTime;
}
lastTime = now;
auto clock = now - startTime;
if (screenfade < 1.f)
{
float ms = (clock / 1'000'000) / job.job->fadetime;
screenfade = clamp(ms, 0.f, 1.f);
if (!M_Active()) twod->SetScreenFade(screenfade);
job.job->fadestate = DScreenJob::fadein;
}
else job.job->fadestate = DScreenJob::visible;
job.job->SetClock(clock);
int state = job.job->Frame(clock, skiprequest);
startTime -= job.job->GetClock() - clock;
return state;
}
int FadeoutFrame()
{
auto now = I_nsTime();
if (M_Active())
{
startTime += now - lastTime;
}
lastTime = now;
auto clock = now - startTime;
float ms = (clock / 1'000'000) / jobs[index].job->fadetime;
float screenfade2 = clamp(screenfade - ms, 0.f, 1.f);
if (!M_Active()) twod->SetScreenFade(screenfade2);
if (screenfade2 <= 0.f)
{
twod->Unlock(); // must unlock before displaying.
return 0;
}
return 1;
}
bool RunFrame()
{
if (index >= jobs.Size())
{
DeleteJobs();
twod->SetScreenFade(1);
if (completion) completion(false);
return false;
}
if (actionState == State_Clear)
{
actionState = State_Run;
twod->ClearScreen();
}
else if (actionState == State_Run)
{
terminateState = DisplayFrame();
if (terminateState < 1)
{
// Must lock before displaying.
if (jobs[index].job->fadestyle & DScreenJob::fadeout)
{
twod->Lock();
startTime = I_nsTime();
jobs[index].job->fadestate = DScreenJob::fadeout;
actionState = State_Fadeout;
}
else
{
AdvanceJob(terminateState < 0);
}
}
}
else if (actionState == State_Fadeout)
{
int ended = FadeoutFrame();
if (ended < 1)
{
AdvanceJob(terminateState < 0);
}
}
return true;
}
};
ScreenJobRunner *runner;
void RunScreenJob(JobDesc* jobs, int count, CompletionFunc completion, bool clearbefore, bool blockingui)
{
assert(completion != nullptr);
videoclearFade();
if (count)
{
runner = new ScreenJobRunner(jobs, count, completion, clearbefore);
gamestate = blockingui? GS_INTRO : GS_INTERMISSION;
}
}
void DeleteScreenJob()
{
if (runner)
{
delete runner;
runner = nullptr;
}
}
void RunScreenJobFrame()
{
// we cannot recover from this because we have no completion callback to call.
if (!runner)
{
// We can get here before a gameaction has been processed. In that case just draw a black screen and wait.
if (gameaction == ga_nothing) I_Error("Trying to run a non-existent screen job");
twod->ClearScreen();
return;
}
auto res = runner->RunFrame();
if (!res)
{
assert((gamestate != GS_INTERMISSION && gamestate != GS_INTRO) || gameaction != ga_nothing);
DeleteScreenJob();
}
}