#include "Graphics/CMoviePlayer.hpp" #include "Audio/g721.h" #include "CDvdRequest.hpp" #include "Graphics/CGraphics.hpp" #include "Graphics/CCubeRenderer.hpp" #include "Graphics/CGX.hpp" #include "GameGlobalObjects.hpp" #include #include namespace metaforce { static void MyTHPYuv2RgbTextureSetup(void* dataY, void* dataU, void* dataV, u16 width, u16 height) { GXTexObj texV; GXTexObj texU; GXTexObj texY; GXInitTexObj(&texY, dataY, width, height, GX::TF_I8, GX_CLAMP, GX_CLAMP, false); GXInitTexObjLOD(&texY, GX_NEAR, GX_NEAR, 0.0, 0.0, 0.0, false, false, GX_ANISO_1); GXLoadTexObj(&texY, GX::TEXMAP0); GXInitTexObj(&texU, dataU, width / 2, height / 2, GX::TF_I8, GX_CLAMP, GX_CLAMP, false); GXInitTexObjLOD(&texU, GX_NEAR, GX_NEAR, 0.0, 0.0, 0.0, false, false, GX_ANISO_1); GXLoadTexObj(&texU, GX::TEXMAP1); GXInitTexObj(&texV, dataV, width / 2, height / 2, GX::TF_I8, GX_CLAMP, GX_CLAMP, false); GXInitTexObjLOD(&texV, GX_NEAR, GX_NEAR, 0.0, 0.0, 0.0, false, false, GX_ANISO_1); GXLoadTexObj(&texV, GX::TEXMAP2); CTexture::InvalidateTexMap(GX::TEXMAP0); CTexture::InvalidateTexMap(GX::TEXMAP1); CTexture::InvalidateTexMap(GX::TEXMAP2); } const std::array InterlaceTex{ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0, 0, 0, 0, 0, }; static void MyTHPGXYuv2RgbSetup(bool interlaced2ndFrame, bool fieldFlip) { CGX::SetZMode(true, GX::ALWAYS, false); CGX::SetBlendMode(GX::BM_NONE, GX::BL_ONE, GX::BL_ZERO, GX::LO_CLEAR); CGX::SetNumChans(0); CGX::SetTexCoordGen(GX::TEXCOORD0, GX::TG_MTX2x4, GX::TG_TEX0, GX::IDENTITY, false, GX::PTIDENTITY); CGX::SetTexCoordGen(GX::TEXCOORD1, GX::TG_MTX2x4, GX::TG_TEX0, GX::IDENTITY, false, GX::PTIDENTITY); if (!fieldFlip) { CGX::SetNumTexGens(3); CGX::SetTexCoordGen(GX::TEXCOORD2, GX::TG_MTX2x4, GX::TG_POS, GX::TEXMTX0, false, GX::PTIDENTITY); aurora::Mat4x2 mtx; mtx.m0.x = 0.125f; mtx.m2.y = 0.25f; if (interlaced2ndFrame) { mtx.m3.y = 0.25f; } GXLoadTexMtxImm(&mtx, GX::TEXMTX0, GX::MTX2x4); GXTexObj texObj; GXInitTexObj(&texObj, InterlaceTex.data(), 8, 4, GX::TF_I8, GX_REPEAT, GX_REPEAT, false); GXInitTexObjLOD(&texObj, GX_NEAR, GX_NEAR, 0.f, 0.f, 0.f, false, false, GX_ANISO_1); GXLoadTexObj(&texObj, GX::TEXMAP3); CTexture::InvalidateTexMap(GX::TEXMAP3); CGX::SetTevOrder(GX::TEVSTAGE4, GX::TEXCOORD2, GX::TEXMAP3, GX::COLOR_NULL); CGX::SetStandardTevColorAlphaOp(GX::TEVSTAGE4); CGX::SetTevColorIn(GX::TEVSTAGE4, GX::CC_ZERO, GX::CC_ZERO, GX::CC_ZERO, GX::CC_CPREV); CGX::SetTevAlphaIn(GX::TEVSTAGE4, GX::CA_ZERO, GX::CA_ZERO, GX::CA_ZERO, GX::CA_TEXA); CGX::SetAlphaCompare(GX::LESS, 128, GX::AOP_AND, GX::ALWAYS, 0); CGX::SetNumTevStages(5); } else { CGX::SetNumTexGens(2); CGX::SetNumTevStages(4); } constexpr std::array vtxDescList{ GX::VtxDescList{GX::VA_POS, GX::DIRECT}, GX::VtxDescList{GX::VA_TEX0, GX::DIRECT}, GX::VtxDescList{}, }; CGX::SetVtxDescv(vtxDescList.data()); GXSetColorUpdate(true); GXSetAlphaUpdate(false); GXInvalidateTexAll(); GXSetVtxAttrFmt(GX::VTXFMT7, GX::VA_POS, GX::CLR_RGBA, GX::F32, 0); GXSetVtxAttrFmt(GX::VTXFMT7, GX::VA_TEX0, GX::CLR_RGBA, GX::RGBX8, 0); CGX::SetTevOrder(GX::TEVSTAGE0, GX::TEXCOORD1, GX::TEXMAP1, GX::COLOR_NULL); CGX::SetTevColorIn(GX::TEVSTAGE0, GX::CC_ZERO, GX::CC_TEXC, GX::CC_KONST, GX::CC_C0); CGX::SetTevColorOp(GX::TEVSTAGE0, GX::TEV_ADD, GX::TB_ZERO, GX::CS_SCALE_1, false, GX::TEVPREV); CGX::SetTevAlphaIn(GX::TEVSTAGE0, GX::CA_ZERO, GX::CA_TEXA, GX::CA_KONST, GX::CA_A0); CGX::SetTevAlphaOp(GX::TEVSTAGE0, GX::TEV_SUB, GX::TB_ZERO, GX::CS_SCALE_1, false, GX::TEVPREV); CGX::SetTevKColorSel(GX::TEVSTAGE0, GX::TEV_KCSEL_K0); CGX::SetTevKAlphaSel(GX::TEVSTAGE0, GX::TEV_KASEL_K0_A); CGX::SetTevOrder(GX::TEVSTAGE1, GX::TEXCOORD1, GX::TEXMAP2, GX::COLOR_NULL); CGX::SetTevColorIn(GX::TEVSTAGE1, GX::CC_ZERO, GX::CC_TEXC, GX::CC_KONST, GX::CC_CPREV); CGX::SetTevColorOp(GX::TEVSTAGE1, GX::TEV_ADD, GX::TB_ZERO, GX::CS_SCALE_2, false, GX::TEVPREV); CGX::SetTevAlphaIn(GX::TEVSTAGE1, GX::CA_ZERO, GX::CA_TEXA, GX::CA_KONST, GX::CA_APREV); CGX::SetTevAlphaOp(GX::TEVSTAGE1, GX::TEV_SUB, GX::TB_ZERO, GX::CS_SCALE_1, false, GX::TEVPREV); CGX::SetTevKColorSel(GX::TEVSTAGE1, GX::TEV_KCSEL_K1); CGX::SetTevKAlphaSel(GX::TEVSTAGE1, GX::TEV_KASEL_K1_A); CGX::SetTevOrder(GX::TEVSTAGE2, GX::TEXCOORD0, GX::TEXMAP0, GX::COLOR_NULL); CGX::SetTevColorIn(GX::TEVSTAGE2, GX::CC_ZERO, GX::CC_TEXC, GX::CC_ONE, GX::CC_CPREV); CGX::SetTevColorOp(GX::TEVSTAGE2, GX::TEV_ADD, GX::TB_ZERO, GX::CS_SCALE_1, true, GX::TEVPREV); CGX::SetTevAlphaIn(GX::TEVSTAGE2, GX::CA_TEXA, GX::CA_ZERO, GX::CA_ZERO, GX::CA_APREV); CGX::SetTevAlphaOp(GX::TEVSTAGE2, GX::TEV_ADD, GX::TB_ZERO, GX::CS_SCALE_1, true, GX::TEVPREV); CGX::SetTevOrder(GX::TEVSTAGE3, GX::TEXCOORD_NULL, GX::TEXMAP_NULL, GX::COLOR_NULL); CGX::SetTevColorIn(GX::TEVSTAGE3, GX::CC_APREV, GX::CC_CPREV, GX::CC_KONST, GX::CC_ZERO); CGX::SetTevColorOp(GX::TEVSTAGE3, GX::TEV_ADD, GX::TB_ZERO, GX::CS_SCALE_1, true, GX::TEVPREV); CGX::SetTevAlphaIn(GX::TEVSTAGE3, GX::CA_ZERO, GX::CA_ZERO, GX::CA_ZERO, GX::CA_ZERO); CGX::SetTevAlphaOp(GX::TEVSTAGE3, GX::TEV_ADD, GX::TB_ZERO, GX::CS_SCALE_1, true, GX::TEVPREV); CGX::SetTevKColorSel(GX::TEVSTAGE3, GX::TEV_KCSEL_K2); GXSetTevColorS10(GX::TEVREG0, 0xffa60000ff8e0087u); CGX::SetTevKColor(GX::KCOLOR0, zeus::Comp32(0x0000e258)); CGX::SetTevKColor(GX::KCOLOR1, zeus::Comp32(0xb30000b6)); CGX::SetTevKColor(GX::KCOLOR2, zeus::Comp32(0xff00ff80)); } static void MyTHPGXRestore() { CGX::SetZMode(true, GX::ALWAYS, false); CGX::SetBlendMode(GX::BM_NONE, GX::BL_ONE, GX::BL_ZERO, GX::LO_SET); CGX::SetNumTexGens(1); CGX::SetNumChans(0); CGX::SetNumTevStages(1); CGX::SetTevOrder(GX::TEVSTAGE0, GX::TEXCOORD0, GX::TEXMAP0, GX::COLOR_NULL); CGX::SetAlphaCompare(GX::ALWAYS, 0, GX::AOP_AND, GX::ALWAYS, 0); } /* used in the original to look up fixed-point dividends on a * MIDI-style volume scale (0-127) -> (n/0x8000) */ static const std::array StaticVolumeLookup = { 0x0000, 0x0002, 0x0008, 0x0012, 0x0020, 0x0032, 0x0049, 0x0063, 0x0082, 0x00A4, 0x00CB, 0x00F5, 0x0124, 0x0157, 0x018E, 0x01C9, 0x0208, 0x024B, 0x0292, 0x02DD, 0x032C, 0x037F, 0x03D7, 0x0432, 0x0492, 0x04F5, 0x055D, 0x05C9, 0x0638, 0x06AC, 0x0724, 0x07A0, 0x0820, 0x08A4, 0x092C, 0x09B8, 0x0A48, 0x0ADD, 0x0B75, 0x0C12, 0x0CB2, 0x0D57, 0x0DFF, 0x0EAC, 0x0F5D, 0x1012, 0x10CA, 0x1187, 0x1248, 0x130D, 0x13D7, 0x14A4, 0x1575, 0x164A, 0x1724, 0x1801, 0x18E3, 0x19C8, 0x1AB2, 0x1BA0, 0x1C91, 0x1D87, 0x1E81, 0x1F7F, 0x2081, 0x2187, 0x2291, 0x239F, 0x24B2, 0x25C8, 0x26E2, 0x2801, 0x2923, 0x2A4A, 0x2B75, 0x2CA3, 0x2DD6, 0x2F0D, 0x3048, 0x3187, 0x32CA, 0x3411, 0x355C, 0x36AB, 0x37FF, 0x3956, 0x3AB1, 0x3C11, 0x3D74, 0x3EDC, 0x4048, 0x41B7, 0x432B, 0x44A3, 0x461F, 0x479F, 0x4923, 0x4AAB, 0x4C37, 0x4DC7, 0x4F5C, 0x50F4, 0x5290, 0x5431, 0x55D6, 0x577E, 0x592B, 0x5ADC, 0x5C90, 0x5E49, 0x6006, 0x61C7, 0x638C, 0x6555, 0x6722, 0x68F4, 0x6AC9, 0x6CA2, 0x6E80, 0x7061, 0x7247, 0x7430, 0x761E, 0x7810, 0x7A06, 0x7C00, 0x7DFE, 0x8000}; /* shared resources */ static tjhandle TjHandle = nullptr; /* RSF audio state */ static const u8* StaticAudio = nullptr; static u32 StaticAudioSize = 0; static u32 StaticAudioOffset = 0; static u16 StaticVolumeAtten = 0x50F4; static u32 StaticLoopBegin = 0; static u32 StaticLoopEnd = 0; static g72x_state StaticStateLeft = {}; static g72x_state StaticStateRight = {}; /* THP SFX audio */ static float SfxVolume = 1.f; void CMoviePlayer::Initialize() { TjHandle = tjInitDecompress(); } void CMoviePlayer::Shutdown() { tjDestroy(TjHandle); TjHandle = nullptr; } void CMoviePlayer::THPHeader::swapBig() { magic = SBig(magic); version = SBig(version); maxBufferSize = SBig(maxBufferSize); maxAudioSamples = SBig(maxAudioSamples); fps = SBig(fps); numFrames = SBig(numFrames); firstFrameSize = SBig(firstFrameSize); dataSize = SBig(dataSize); componentDataOffset = SBig(componentDataOffset); offsetsDataOffset = SBig(offsetsDataOffset); firstFrameOffset = SBig(firstFrameOffset); lastFrameOffset = SBig(lastFrameOffset); } void CMoviePlayer::THPComponents::swapBig() { numComponents = SBig(numComponents); } void CMoviePlayer::THPVideoInfo::swapBig() { width = SBig(width); height = SBig(height); } void CMoviePlayer::THPAudioInfo::swapBig() { numChannels = SBig(numChannels); sampleRate = SBig(sampleRate); numSamples = SBig(numSamples); } void CMoviePlayer::THPFrameHeader::swapBig() { nextSize = SBig(nextSize); prevSize = SBig(prevSize); imageSize = SBig(imageSize); audioSize = SBig(audioSize); } void CMoviePlayer::THPAudioFrameHeader::swapBig() { channelSize = SBig(channelSize); numSamples = SBig(numSamples); for (int i = 0; i < 2; ++i) { for (int j = 0; j < 8; ++j) { channelCoefs[i][j][0] = SBig(channelCoefs[i][j][0]); channelCoefs[i][j][1] = SBig(channelCoefs[i][j][1]); } channelPrevs[i][0] = SBig(channelPrevs[i][0]); channelPrevs[i][1] = SBig(channelPrevs[i][1]); } } /* Slightly modified from THPAudioDecode present in SDK; always interleaves */ u32 CMoviePlayer::THPAudioDecode(s16* buffer, const u8* audioFrame, bool stereo) { THPAudioFrameHeader header = *((const THPAudioFrameHeader*)audioFrame); header.swapBig(); audioFrame += sizeof(THPAudioFrameHeader); if (stereo) { for (int i = 0; i < 2; ++i) { unsigned samples = header.numSamples; s16* bufferCur = buffer + i; int16_t prev1 = header.channelPrevs[i][0]; int16_t prev2 = header.channelPrevs[i][1]; for (u32 f = 0; f < (header.numSamples + 13) / 14; ++f) { DSPDecompressFrameStereoStride(bufferCur, audioFrame, header.channelCoefs[i], &prev1, &prev2, samples); samples -= 14; bufferCur += 28; audioFrame += 8; } } } else { unsigned samples = header.numSamples; s16* bufferCur = buffer; int16_t prev1 = header.channelPrevs[0][0]; int16_t prev2 = header.channelPrevs[0][1]; for (u32 f = 0; f < (header.numSamples + 13) / 14; ++f) { DSPDecompressFrameStereoDupe(bufferCur, audioFrame, header.channelCoefs[0], &prev1, &prev2, samples); samples -= 14; bufferCur += 28; audioFrame += 8; } } return header.numSamples; } CMoviePlayer::CMoviePlayer(const char* path, float preLoadSeconds, bool loop, bool deinterlace) : CDvdFile(path), xec_preLoadSeconds(preLoadSeconds), xf4_24_loop(loop), m_deinterlace(deinterlace) { /* Read THP header information */ u8 buf[64]; SyncRead(buf, 64); memmove(&x28_thpHead, buf, 48); x28_thpHead.swapBig(); u32 cur = x28_thpHead.componentDataOffset; SyncSeekRead(buf, 32, ESeekOrigin::Begin, cur); memmove(&x58_thpComponents, buf, 20); cur += 20; x58_thpComponents.swapBig(); for (u32 i = 0; i < x58_thpComponents.numComponents; ++i) { switch (x58_thpComponents.comps[i]) { case THPComponents::Type::Video: SyncSeekRead(buf, 32, ESeekOrigin::Begin, cur); memmove(&x6c_videoInfo, buf, 8); cur += 8; x6c_videoInfo.swapBig(); break; case THPComponents::Type::Audio: SyncSeekRead(buf, 32, ESeekOrigin::Begin, cur); memmove(&x74_audioInfo, buf, 12); cur += 12; x74_audioInfo.swapBig(); xf4_25_hasAudio = true; break; default: break; } } /* Initial read state */ xb4_nextReadOff = x28_thpHead.firstFrameOffset; xb0_nextReadSize = x28_thpHead.firstFrameSize; xb8_readSizeWrapped = x28_thpHead.firstFrameSize; xbc_readOffWrapped = x28_thpHead.firstFrameOffset; xe4_totalSeconds = x28_thpHead.numFrames / x28_thpHead.fps; if (xec_preLoadSeconds < 0.f) { /* Load whole video */ xec_preLoadSeconds = xe4_totalSeconds; xf0_preLoadFrames = x28_thpHead.numFrames; } else if (xec_preLoadSeconds > 0.f) { /* Pre-load video portion */ u32 frame = xec_preLoadSeconds * x28_thpHead.fps; xf0_preLoadFrames = std::min(frame, x28_thpHead.numFrames); xec_preLoadSeconds = std::min(xe4_totalSeconds, xec_preLoadSeconds); } if (xf0_preLoadFrames > 0) xa0_bufferQueue.reserve(xf0_preLoadFrames); /* Allocate textures here (rather than at decode time) */ x80_textures.reserve(3); for (int i = 0; i < 3; ++i) { CTHPTextureSet& set = x80_textures.emplace_back(); if (deinterlace) { /* metaforce addition: this way interlaced THPs don't look horrible */ set.Y[0] = aurora::gfx::new_dynamic_texture_2d(x6c_videoInfo.width, x6c_videoInfo.height / 2, 1, GX::TF_I8, fmt::format(FMT_STRING("Movie {} Texture Set {} Y[0]"), path, i)); set.Y[1] = aurora::gfx::new_dynamic_texture_2d(x6c_videoInfo.width, x6c_videoInfo.height / 2, 1, GX::TF_I8, fmt::format(FMT_STRING("Movie {} Texture Set {} Y[1]"), path, i)); set.U = aurora::gfx::new_dynamic_texture_2d(x6c_videoInfo.width / 2, x6c_videoInfo.height / 2, 1, GX::TF_I8, fmt::format(FMT_STRING("Movie {} Texture Set {} U"), path, i)); set.V = aurora::gfx::new_dynamic_texture_2d(x6c_videoInfo.width / 2, x6c_videoInfo.height / 2, 1, GX::TF_I8, fmt::format(FMT_STRING("Movie {} Texture Set {} V"), path, i)); } else { /* normal progressive presentation */ set.Y[0] = aurora::gfx::new_dynamic_texture_2d(x6c_videoInfo.width, x6c_videoInfo.height, 1, GX::TF_I8, fmt::format(FMT_STRING("Movie {} Texture Set {} Y"), path, i)); set.U = aurora::gfx::new_dynamic_texture_2d(x6c_videoInfo.width / 2, x6c_videoInfo.height / 2, 1, GX::TF_I8, fmt::format(FMT_STRING("Movie {} Texture Set {} U"), path, i)); set.V = aurora::gfx::new_dynamic_texture_2d(x6c_videoInfo.width / 2, x6c_videoInfo.height / 2, 1, GX::TF_I8, fmt::format(FMT_STRING("Movie {} Texture Set {} V"), path, i)); } if (xf4_25_hasAudio) set.audioBuf.reset(new s16[x28_thpHead.maxAudioSamples * 2]); } /* Temporary planar YUV decode buffer, resulting planes copied to Boo */ m_yuvBuf.reset(new uint8_t[tjBufSizeYUV(x6c_videoInfo.width, x6c_videoInfo.height, TJ_420)]); /* Schedule initial read */ PostDVDReadRequestIfNeeded(); m_hpad = 0.5f; m_vpad = 0.5f; } void CMoviePlayer::SetStaticAudioVolume(int vol) { StaticVolumeAtten = StaticVolumeLookup[std::max(0, std::min(127, vol))]; } void CMoviePlayer::SetStaticAudio(const void* data, u32 size, u32 loopBegin, u32 loopEnd) { StaticAudio = reinterpret_cast(data); StaticAudioSize = size; StaticLoopBegin = loopBegin; StaticLoopEnd = loopEnd; StaticAudioOffset = 0; g72x_init_state(&StaticStateLeft); g72x_init_state(&StaticStateRight); } void CMoviePlayer::SetSfxVolume(u8 volume) { SfxVolume = std::min(volume, u8(127)) / 127.f; } void CMoviePlayer::MixAudio(s16* out, const s16* in, u32 samples) { /* No audio frames ready */ if (xd4_audioSlot == UINT32_MAX) { if (in) memmove(out, in, samples * 4); else memset(out, 0, samples * 4); return; } while (samples) { CTHPTextureSet* tex = &x80_textures[xd4_audioSlot]; u32 thisSamples = std::min(tex->audioSamples - tex->playedSamples, samples); if (!thisSamples) { /* Advance frame */ ++xd4_audioSlot; if (xd4_audioSlot >= x80_textures.size()) xd4_audioSlot = 0; tex = &x80_textures[xd4_audioSlot]; thisSamples = std::min(tex->audioSamples - tex->playedSamples, samples); } if (thisSamples) { /* mix samples with `in` or no mix */ if (in) { for (u32 i = 0; i < thisSamples; ++i, out += 2, in += 2) { out[0] = DSPSampClamp(in[0] + s32(tex->audioBuf[(i + tex->playedSamples) * 2]) * 0x50F4 / 0x8000 * SfxVolume); out[1] = DSPSampClamp(in[1] + s32(tex->audioBuf[(i + tex->playedSamples) * 2 + 1]) * 0x50F4 / 0x8000 * SfxVolume); } } else { for (u32 i = 0; i < thisSamples; ++i, out += 2) { out[0] = DSPSampClamp(s32(tex->audioBuf[(i + tex->playedSamples) * 2]) * 0x50F4 / 0x8000 * SfxVolume); out[1] = DSPSampClamp(s32(tex->audioBuf[(i + tex->playedSamples) * 2 + 1]) * 0x50F4 / 0x8000 * SfxVolume); } } tex->playedSamples += thisSamples; samples -= thisSamples; } else { /* metaforce addition: failsafe for buffer overrun */ if (in) memmove(out, in, samples * 4); else memset(out, 0, samples * 4); // fprintf(stderr, "dropped %d samples\n", samples); return; } } } void CMoviePlayer::MixStaticAudio(s16* out, const s16* in, u32 samples) { if (!StaticAudio) return; while (samples) { u32 thisSamples = std::min(StaticLoopEnd - StaticAudioOffset, samples); const u8* thisOffsetLeft = &StaticAudio[StaticAudioOffset / 2]; const u8* thisOffsetRight = &StaticAudio[StaticAudioSize / 2 + StaticAudioOffset / 2]; /* metaforce addition: mix samples with `in` or no mix */ if (in) { for (u32 i = 0; i < thisSamples; i += 2) { out[0] = DSPSampClamp( in[0] + s32(g721_decoder(thisOffsetLeft[0] & 0xf, &StaticStateLeft) * StaticVolumeAtten / 0x8000)); out[1] = DSPSampClamp( in[1] + s32(g721_decoder(thisOffsetRight[0] & 0xf, &StaticStateRight) * StaticVolumeAtten / 0x8000)); out[2] = DSPSampClamp( in[2] + s32(g721_decoder(thisOffsetLeft[0] >> 4 & 0xf, &StaticStateLeft) * StaticVolumeAtten / 0x8000)); out[3] = DSPSampClamp( in[3] + s32(g721_decoder(thisOffsetRight[0] >> 4 & 0xf, &StaticStateRight) * StaticVolumeAtten / 0x8000)); thisOffsetLeft += 1; thisOffsetRight += 1; out += 4; in += 4; } } else { for (u32 i = 0; i < thisSamples; i += 2) { out[0] = DSPSampClamp(s32(g721_decoder(thisOffsetLeft[0] & 0xf, &StaticStateLeft) * StaticVolumeAtten / 0x8000)); out[1] = DSPSampClamp(s32(g721_decoder(thisOffsetRight[0] & 0xf, &StaticStateRight) * StaticVolumeAtten / 0x8000)); out[2] = DSPSampClamp( s32(g721_decoder(thisOffsetLeft[0] >> 4 & 0xf, &StaticStateLeft) * StaticVolumeAtten / 0x8000)); out[3] = DSPSampClamp( s32(g721_decoder(thisOffsetRight[0] >> 4 & 0xf, &StaticStateRight) * StaticVolumeAtten / 0x8000)); thisOffsetLeft += 1; thisOffsetRight += 1; out += 4; } } StaticAudioOffset += thisSamples; if (StaticAudioOffset == StaticLoopEnd) StaticAudioOffset = StaticLoopBegin; samples -= thisSamples; } } void CMoviePlayer::Rewind() { if (x98_request) { x98_request->PostCancelRequest(); x98_request.reset(); } xb0_nextReadSize = x28_thpHead.firstFrameSize; xb4_nextReadOff = x28_thpHead.firstFrameOffset; xb8_readSizeWrapped = x28_thpHead.firstFrameSize; xbc_readOffWrapped = x28_thpHead.firstFrameOffset; xc0_curLoadFrame = 0; xc4_requestFrameWrapped = 0; xc8_curFrame = 0; xcc_decodedTexSlot = 0; xd0_drawTexSlot = -1; xd4_audioSlot = -1; xd8_decodedTexCount = 0; xdc_frameRem = 0.f; xe8_curSeconds = 0.f; } bool CMoviePlayer::DrawVideo() { // TODO // if (!xa0_bufferQueue.empty()) { // return false; // } g_Renderer->SetDepthReadWrite(false, false); g_Renderer->SetViewportOrtho(false, -4096.f, 4096.f); const s32 vpHeight = CGraphics::GetViewportHeight(); const s32 vpWidth = CGraphics::GetViewportWidth(); const s32 vpTop = CGraphics::GetViewportTop(); const s32 vpLeft = CGraphics::GetViewportLeft(); const s32 vidWidth = x6c_videoInfo.width; const s32 vidHeight = x6c_videoInfo.height; const s32 centerX = (vidWidth - vpWidth) / 2; const s32 centerY = (vidHeight - vpHeight) / 2; const s32 vl = vpLeft - centerX; const s32 vr = vpLeft + vpWidth + centerX; const s32 vb = vpTop + vpHeight + centerY; const s32 vt = vpTop - centerY; aurora::Vec3 v1; aurora::Vec3 v2; aurora::Vec3 v3; aurora::Vec3 v4; v1.x = vl; v1.y = 0.0; v1.z = vb; v2.x = vr; v2.y = 0.0; v2.z = vb; v3.x = vl; v3.y = 0.0; v3.z = vt; v4.x = vr; v4.y = 0.0; v4.z = vt; DrawFrame(v1, v2, v3, v4); return true; } void CMoviePlayer::DrawFrame(const zeus::CVector3f& v1, const zeus::CVector3f& v2, const zeus::CVector3f& v3, const zeus::CVector3f& v4) { if (xd0_drawTexSlot == UINT32_MAX || !GetIsFullyCached()) { return; } SCOPED_GRAPHICS_DEBUG_GROUP("CMoviePlayer::DrawFrame", zeus::skYellow); CGraphics::SetUseVideoFilter(xf4_26_fieldFlip); /* Correct movie aspect ratio */ float hPad, vPad; if (CGraphics::GetViewportAspect() >= 1.78f) { hPad = 1.78f / CGraphics::GetViewportAspect(); vPad = 1.78f / 1.33f; } else { hPad = 1.f; vPad = CGraphics::GetViewportAspect() / 1.33f; } /* draw appropriate field */ CTHPTextureSet& tex = x80_textures[xd0_drawTexSlot]; aurora::gfx::queue_movie_player(tex.Y[m_deinterlace ? (xfc_fieldIndex != 0) : 0], tex.U, tex.V, hPad, vPad); // MyTHPGXYuv2RgbSetup(CGraphics::g_LastFrameUsedAbove, xf4_26_fieldFlip); // uintptr_t planeSize = x6c_videoInfo.width * x6c_videoInfo.height; // uintptr_t planeSizeQuarter = planeSize / 4; // MyTHPYuv2RgbTextureSetup(m_yuvBuf.get(), m_yuvBuf.get() + planeSize, m_yuvBuf.get() + planeSize + planeSizeQuarter, // x6c_videoInfo.width, x6c_videoInfo.height); // // CGX::Begin(GX::TRIANGLEFAN, GX::VTXFMT7, 4); // GXPosition3f32(v1); // GXTexCoord2f32(0.f, 0.f); // GXPosition3f32(v3); // GXTexCoord2f32(0.f, 1.f); // GXPosition3f32(v4); // GXTexCoord2f32(1.f, 1.f); // GXPosition3f32(v2); // GXTexCoord2f32(1.f, 0.f); // CGX::End(); // MyTHPGXRestore(); /* ensure second field is being displayed by VI to signal advance * (faked in metaforce with continuous xor) */ if (xfc_fieldIndex == 0 && CGraphics::g_LastFrameUsedAbove) xf4_26_fieldFlip = true; ++xfc_fieldIndex; } void CMoviePlayer::Update(float dt) { if (xc0_curLoadFrame < xf0_preLoadFrames) { /* in buffering phase, ensure read data is stored for mem-cache access */ if (x98_request && x98_request->IsComplete()) { ReadCompleted(); if (xc0_curLoadFrame >= xa0_bufferQueue.size() && xc0_curLoadFrame < xf0_preLoadFrames && xa0_bufferQueue.size() < x28_thpHead.numFrames) { PostDVDReadRequestIfNeeded(); } } } else { /* out of buffering phase, skip mem-cache straight to decode */ if (x98_request) { bool flag = false; if (xc4_requestFrameWrapped >= xa0_bufferQueue.size() && xc0_curLoadFrame >= xa0_bufferQueue.size()) flag = true; if (x98_request->IsComplete() && xd8_decodedTexCount < 2 && flag) { DecodeFromRead(x90_requestBuf.get()); ReadCompleted(); PostDVDReadRequestIfNeeded(); ++xd8_decodedTexCount; ++xc4_requestFrameWrapped; if (xc4_requestFrameWrapped >= x28_thpHead.numFrames && xf4_24_loop) xc4_requestFrameWrapped = 0; } } } /* submit request for additional read to keep stream-consumer happy * (if buffer slot is available) */ if (!x98_request && xe0_playMode == EPlayMode::Playing && xa0_bufferQueue.size() < x28_thpHead.numFrames) { PostDVDReadRequestIfNeeded(); } /* decode frame directly from mem-cache if needed */ if (xd8_decodedTexCount < 2) { if (xe0_playMode == EPlayMode::Playing && xc4_requestFrameWrapped < xf0_preLoadFrames) { u32 minFrame = std::min(u32(xa0_bufferQueue.size()) - 1, xc4_requestFrameWrapped); if (minFrame == UINT32_MAX) return; std::unique_ptr& frameData = xa0_bufferQueue[minFrame]; DecodeFromRead(frameData.get()); ++xd8_decodedTexCount; ++xc4_requestFrameWrapped; if (xc4_requestFrameWrapped >= x28_thpHead.numFrames && xf4_24_loop) xc4_requestFrameWrapped = 0; } } /* paused THPs shall not pass */ if (xd8_decodedTexCount <= 0 || xe0_playMode != EPlayMode::Playing) return; /* timing update */ xe8_curSeconds += dt; if (xf4_24_loop) xe8_curSeconds = std::fmod(xe8_curSeconds, xe4_totalSeconds); else xe8_curSeconds = std::min(xe4_totalSeconds, xe8_curSeconds); /* test remainder threshold, determine if frame needs to be advanced */ float frameDt = 1.f / x28_thpHead.fps; float rem = xdc_frameRem - dt; if (rem <= 0.f) { if (!xf4_26_fieldFlip) { /* second field has drawn, advance consumer-queue to next THP frame */ ++xd0_drawTexSlot; if (xd0_drawTexSlot >= x80_textures.size()) xd0_drawTexSlot = 0; if (xd4_audioSlot == UINT32_MAX) xd4_audioSlot = 0; --xd8_decodedTexCount; ++xc8_curFrame; if (xc8_curFrame == x28_thpHead.numFrames && xf4_24_loop) xc8_curFrame = 0; rem += frameDt; xfc_fieldIndex = 0; } else { /* advance timing within second field */ rem += dt; xf4_26_fieldFlip = false; } } xdc_frameRem = rem; } void CMoviePlayer::DecodeFromRead(const void* data) { const u8* inptr = (u8*)data; CTHPTextureSet& tex = x80_textures[xcc_decodedTexSlot]; THPFrameHeader frameHeader = *static_cast(data); frameHeader.swapBig(); inptr += 8 + x58_thpComponents.numComponents * 4; for (u32 i = 0; i < x58_thpComponents.numComponents; ++i) { switch (x58_thpComponents.comps[i]) { case THPComponents::Type::Video: { tjDecompressToYUV(TjHandle, (u8*)inptr, frameHeader.imageSize, m_yuvBuf.get(), 0); inptr += frameHeader.imageSize; uintptr_t planeSize = x6c_videoInfo.width * x6c_videoInfo.height; uintptr_t planeSizeHalf = planeSize / 2; uintptr_t planeSizeQuarter = planeSizeHalf / 2; if (m_deinterlace) { /* Deinterlace into 2 discrete 60-fps half-res textures */ auto buffer = std::make_unique(planeSizeHalf); for (unsigned y = 0; y < x6c_videoInfo.height / 2; ++y) { memcpy(buffer.get() + x6c_videoInfo.width * y, m_yuvBuf.get() + x6c_videoInfo.width * (y * 2), x6c_videoInfo.width); } aurora::gfx::write_texture(*tex.Y[0], {buffer.get(), planeSizeHalf}); for (unsigned y = 0; y < x6c_videoInfo.height / 2; ++y) { memcpy(buffer.get() + x6c_videoInfo.width * y, m_yuvBuf.get() + x6c_videoInfo.width * (y * 2 + 1), x6c_videoInfo.width); } aurora::gfx::write_texture(*tex.Y[1], {buffer.get(), planeSizeHalf}); } else { /* Direct planar load */ aurora::gfx::write_texture(*tex.Y[0], {m_yuvBuf.get(), planeSize}); } aurora::gfx::write_texture(*tex.U, {m_yuvBuf.get() + planeSize, planeSizeQuarter}); aurora::gfx::write_texture(*tex.V, {m_yuvBuf.get() + planeSize + planeSizeQuarter, planeSizeQuarter}); break; } case THPComponents::Type::Audio: memset(tex.audioBuf.get(), 0, x28_thpHead.maxAudioSamples * 4); tex.audioSamples = THPAudioDecode(tex.audioBuf.get(), (u8*)inptr, x74_audioInfo.numChannels == 2); tex.playedSamples = 0; inptr += frameHeader.audioSize; break; default: break; } } /* advance YUV producer-queue slot */ ++xcc_decodedTexSlot; if (xcc_decodedTexSlot == x80_textures.size()) xcc_decodedTexSlot = 0; } void CMoviePlayer::ReadCompleted() { std::unique_ptr buffer = std::move(x90_requestBuf); x98_request.reset(); const THPFrameHeader* frameHeader = reinterpret_cast(buffer.get()); /* transfer request buffer to mem-cache if needed */ if (xc0_curLoadFrame == xa0_bufferQueue.size() && xf0_preLoadFrames > xc0_curLoadFrame) xa0_bufferQueue.push_back(std::move(buffer)); /* store params of next read operation */ xb4_nextReadOff += xb0_nextReadSize; xb0_nextReadSize = SBig(frameHeader->nextSize); ++xc0_curLoadFrame; if (xc0_curLoadFrame == xf0_preLoadFrames) { if (x28_thpHead.numFrames == xc0_curLoadFrame) { xb8_readSizeWrapped = x28_thpHead.firstFrameSize; xbc_readOffWrapped = x28_thpHead.firstFrameOffset; } else { xb8_readSizeWrapped = xb0_nextReadSize; xbc_readOffWrapped = xb4_nextReadOff; } } /* handle loop-event read */ if (xc0_curLoadFrame >= x28_thpHead.numFrames && xf4_24_loop) { xb4_nextReadOff = xbc_readOffWrapped; xb0_nextReadSize = xb8_readSizeWrapped; xc0_curLoadFrame = xf0_preLoadFrames; } } void CMoviePlayer::PostDVDReadRequestIfNeeded() { if (xc0_curLoadFrame < x28_thpHead.numFrames) { x90_requestBuf.reset(new uint8_t[xb0_nextReadSize]); x98_request = AsyncSeekRead(x90_requestBuf.get(), xb0_nextReadSize, ESeekOrigin::Begin, xb4_nextReadOff); } } } // namespace metaforce