raze/source/core/screenjob.cpp
Christoph Oelckers 41a2a63efd - moved the VP8 decoding loop into the movie player class and got rid of animvpx.
This allowed significant simplification of code data and many of the error checks could also be simplified because this player doesn't really need it all.
Also use nanoseconds to count frame delays, not milliseconds, as milliseconds can cause timing anomalies with common frame rates very easily.
2020-09-10 17:54:27 +02:00

901 lines
22 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 <vpx/vpx_decoder.h>
#include <vpx/vp8dx.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);
}
}
if (!skiprequest && !nostopsound && curframe == numframes && soundEngine->GetSoundPlayingInfo(SOURCE_None, nullptr, -1)) return 1;
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;
const AnimSound* animSnd;
unsigned width, height;
TArray<uint8_t> Pic;
TArray<uint8_t> readBuf;
vpx_codec_ctx_t codec{};
vpx_codec_iter_t iter = nullptr;
uint32_t convnumer;
uint32_t convdenom;
uint64_t nsecsperframe;
uint64_t nextframetime;
int decstate = 0;
int framenum = 0;
int numframes;
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_;
if (!ReadIVFHeader(origframedelay))
{
// We should never get here, because any file failing this has been eliminated before this constructor got called.
Printf(PRINT_BOLD, "Failed reading IVF header\n");
failed = true;
}
Pic.Resize(width * height * 4);
// Todo: Support VP9 as well?
vpx_codec_dec_cfg_t cfg = { 1, width, height };
if (vpx_codec_dec_init(&codec, &vpx_codec_vp8_dx_algo, &cfg, 0))
{
Printf(PRINT_BOLD, "Error initializing VPX codec.\n");
failed = true;
}
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
bool ReadIVFHeader(int origframedelay)
{
// IVF format: http://wiki.multimedia.cx/index.php?title=IVF
uint32_t magic; fr.Read(&magic, 4); // do not byte swap!
if (magic != MAKE_ID('D', 'K', 'I', 'F')) return false;
uint16_t version = fr.ReadUInt16();
if (version != 0) return false;
uint16_t length = fr.ReadUInt16();
if (length != 32) return false;
fr.Read(&magic, 4);
if (magic != MAKE_ID('V', 'P', '8', '0')) return false;
width = fr.ReadUInt16();
height = fr.ReadUInt16();
uint32_t fpsdenominator = fr.ReadUInt32();
uint32_t fpsnumerator = fr.ReadUInt32();
numframes = fr.ReadUInt32();
if (numframes == 0) return false;
fr.Seek(4, FileReader::SeekCur);
if (fpsdenominator > 1000 || fpsnumerator == 0 || fpsdenominator == 0)
{
// default to 30 fps if the header does not provide useful info.
fpsdenominator = 30;
fpsnumerator = 1;
}
convnumer = 120 * fpsnumerator;
convdenom = fpsdenominator * origframedelay;
nsecsperframe = int64_t(fpsnumerator) * 1'000'000'000 / fpsdenominator;
nextframetime = 0;
return true;
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
bool ReadFrame()
{
int corrupted = 0;
int framesize = fr.ReadInt32();
fr.Seek(8, FileReader::SeekCur);
if (framesize == 0) return false;
readBuf.Resize(framesize);
if (fr.Read(readBuf.Data(), framesize) != framesize) return false;
if (vpx_codec_decode(&codec, readBuf.Data(), readBuf.Size(), NULL, 0) != VPX_CODEC_OK) return false;
if (vpx_codec_control(&codec, VP8D_GET_FRAME_CORRUPTED, &corrupted) != VPX_CODEC_OK) return false;
return true;
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
vpx_image_t *GetFrameData()
{
vpx_image_t *img;
do
{
if (decstate == 0) // first time / begin
{
if (!ReadFrame()) return nullptr;
decstate = 1;
}
img = vpx_codec_get_frame(&codec, &iter);
if (img == nullptr)
{
decstate = 0;
iter = nullptr;
}
} while (img == nullptr);
return img->d_w == width && img->d_h == height? img : nullptr;
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
void SetPixel(uint8_t* dest, uint8_t y, uint8_t u, uint8_t v)
{
dest[0] = y;
dest[1] = u;
dest[2] = v;
}
bool CreateNextFrame()
{
auto img = GetFrameData();
if (!img) return false;
uint8_t const* const yplane = img->planes[VPX_PLANE_Y];
uint8_t const* const uplane = img->planes[VPX_PLANE_U];
uint8_t const* const vplane = img->planes[VPX_PLANE_V];
const int ystride = img->stride[VPX_PLANE_Y];
const int ustride = img->stride[VPX_PLANE_U];
const int vstride = img->stride[VPX_PLANE_V];
for (unsigned int y = 0; y < height; y += 2)
{
unsigned int y1 = y + 1;
unsigned int wy = width * y;
unsigned int wy1 = width * y1;
for (unsigned int x = 0; x < width; x += 2)
{
uint8_t u = uplane[ustride * (y >> 1) + (x >> 1)];
uint8_t v = vplane[vstride * (y >> 1) + (x >> 1)];
SetPixel(&Pic[(wy + x) << 2], yplane[ystride * y + x], u, v);
SetPixel(&Pic[(wy + x + 1) << 2], yplane[ystride * y + x + 1], u, v);
SetPixel(&Pic[(wy1 + x) << 2], yplane[ystride * y1 + x], u, v);
SetPixel(&Pic[(wy1 + x + 1) << 2], yplane[ystride * y1 + x + 1], u, v);
}
}
return true;
}
//---------------------------------------------------------------------------
//
//
//
//---------------------------------------------------------------------------
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, width, height);
}
bool stop = false;
if (clock > nextframetime)
{
nextframetime += nsecsperframe;
if (!CreateNextFrame())
{
Printf(PRINT_BOLD, "Failed reading next frame\n");
stop = true;
}
else
{
animtex.SetFrame(nullptr, Pic.Data());
}
framenum++;
if (framenum >= numframes) stop = true;
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
{
vpx_codec_destroy(&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;
uint64_t clock = 0;
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;
lastTime= startTime = -1;
clock = 0;
inputState.ClearAllInput();
}
int DisplayFrame()
{
auto& job = jobs[index];
auto now = I_GetTimeNS();
bool processed = job.job->ProcessInput();
bool skiprequest = inputState.CheckAllInput() && !processed;
if (startTime == -1)
{
lastTime = startTime = now;
}
else if (!M_Active())
{
clock += now - lastTime;
if (clock == 0) clock = 1;
}
lastTime = now;
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);
clock = job.job->GetClock();
if (clock == 0) clock = 1;
return state;
}
int FadeoutFrame()
{
auto now = I_GetTimeNS();
if (!M_Active())
{
clock += now - lastTime;
if (clock == 0) clock = 1;
}
lastTime = now;
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);
twod->ClearScreen(); // This must not leave the 2d buffer empty.
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);
gameaction = blockingui? ga_intro : ga_intermission;
}
else
{
completion(false);
}
}
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();
}
}