/* ** 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 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 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(); }; 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(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(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(fr); if (!anm->isvalid()) { anm->Destroy(); return nothing(); } return anm; } else if (!memcmp(id, "DKIF\0\0 \0VP80", 12)) { auto anm = Create(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 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(); } }