diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp index c9f50293a..ccdbf0e67 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp @@ -1,5 +1,6 @@ #include "Common.h" #include "MPEG.h" +#include "MPEG_internal.h" extern "C" { @@ -9,12 +10,14 @@ extern "C" #include } +#include #include #include #include #include #include "Syscalls/Helpers/State.h" +#include "Syscalls/Interrupt.h" namespace ps2_stubs { @@ -62,6 +65,25 @@ namespace ps2_stubs MpegFfmpegDecoder(const MpegFfmpegDecoder &) = delete; MpegFfmpegDecoder &operator=(const MpegFfmpegDecoder &) = delete; + // The decoded stream's frame rate (AVCodecContext::framerate), populated + // once the MPEG-2 sequence header has been parsed. Returns false (leaving + // num/den untouched) until it is known; callers fall back to a default. + bool getFrameRate(int &num, int &den) const + { + if (!m_codecCtx) + { + return false; + } + const AVRational fr = m_codecCtx->framerate; + if (fr.num <= 0 || fr.den <= 0) + { + return false; + } + num = fr.num; + den = fr.den; + return true; + } + bool feed(const uint8_t *data, size_t size, std::deque &frames) { if (!data || size == 0) @@ -451,6 +473,15 @@ namespace ps2_stubs std::deque decodedFrames; bool hasLastFrame = false; MpegDecodedFrame lastFrame; + // Frame-rate pacing state (see mpegFrameDueTick / sceMpegGetPicture). Captured the + // first time this handle serves a real decoded frame; frame N is then due at + // baseVsyncTick + floor(N * kMpegPacingVsyncHz * fpsDen / fpsNum). + bool baseVsyncSet = false; + uint64_t baseVsyncTick = 0u; + uint64_t nextPacingFrameIndex = 0u; + int fpsNum = 30000; // NTSC video rate 30000/1001 (29.97 fps) until the + int fpsDen = 1001; // decoder reports the stream's real rate. + bool fpsKnown = false; // set once a valid in-range rate has been latched. std::unique_ptr decoder; }; @@ -485,6 +516,43 @@ namespace ps2_stubs std::condition_variable g_mpeg_cv; MpegStubState g_mpeg_stub_state; + // Rate of the emulator's vsync tick worker: GetCurrentVSyncTick() advances once per + // kVblankPeriod (~16667us == 60Hz), a fixed rate independent of the guest's NTSC/PAL + // display mode. Pacing converts a movie frame index to a due tick with this rate, so a + // park's real duration is frameIndex * fpsDen / fpsNum seconds regardless of display mode + // -- PAL content (e.g. 25fps) is paced correctly because the stream's own frame rate + // drives the math. + constexpr uint64_t kMpegPacingVsyncHz = 60u; + // Compile-time link to the vsync worker's authoritative period (kVblankPeriod, now + // exported from Interrupt.h): retuning the worker without revisiting this rate breaks the + // build. A window rather than equality because 16667us is ~1/60s but not its exact + // reciprocal (60 * 16667us = 1000020us, 20us over one second). + static_assert( + kMpegPacingVsyncHz * ps2_syscalls::interrupt_state::kVblankPeriod.count() >= 999000 && + kMpegPacingVsyncHz * ps2_syscalls::interrupt_state::kVblankPeriod.count() <= 1001000, + "kMpegPacingVsyncHz must track the vsync worker's kVblankPeriod (Interrupt.h)"); + + // Lower bound of the frame-rate plausibility gate (mpegFrameRateIsSane): a stream slower + // than this is rejected as garbage. Named here rather than left a literal in the gate so + // the park-cap coupling below asserts against the SAME floor the gate enforces. + constexpr int kMpegMinSaneFps = 10; + // Upper bound of the same gate: a stream faster than this is rejected as garbage. Named + // alongside the floor so the gate reads against two constants and neither bound can drift + // to a bare literal in the comparison. + constexpr int kMpegMaxSaneFps = 120; + + // Hard ceiling on a single sceMpegGetPicture pacing park (defense in depth: even a + // bogus stream rate can never stall one GetPicture call more than a few vblanks). See + // mpegParkUntilDueTick. + constexpr uint64_t kMpegPacingMaxParkVsyncTicks = 8u; + // The cap must cover the widest per-frame spacing a *sane* stream can produce, or a slow- + // but-in-range movie has its legitimate park silently clipped and plays too fast. The + // slowest sane stream (kMpegMinSaneFps) spaces frames kMpegPacingVsyncHz / kMpegMinSaneFps + // ticks apart, so the cap must be at least that. This ties the gate floor and the park + // cap -- two constants that would otherwise drift apart unnoticed. + static_assert(kMpegPacingMaxParkVsyncTicks * kMpegMinSaneFps >= kMpegPacingVsyncHz, + "pacing park cap must cover the slowest sane stream's per-frame spacing"); + // TODO this resolution should follow runtime resolution constexpr uint32_t kStubMovieWidth = 320u; constexpr uint32_t kStubMovieHeight = 240u; @@ -1506,6 +1574,24 @@ namespace ps2_stubs g_mpeg_cv.notify_all(); } + bool mpegLatchedFrameRate(uint32_t mpegAddr, int &num, int &den) + { + std::lock_guard lock(g_mpeg_stub_mutex); + const auto it = g_mpeg_stub_state.playbackByMpeg.find(mpegAddr); + if (it == g_mpeg_stub_state.playbackByMpeg.end()) + { + return false; + } + const auto &playback = it->second; + if (!playback.fpsKnown) + { + return false; + } + num = playback.fpsNum; + den = playback.fpsDen; + return true; + } + void notifyMpegCdStreamStart() { std::lock_guard lock(g_mpeg_stub_mutex); @@ -1909,6 +1995,53 @@ namespace ps2_stubs setReturnU32(ctx, getPlaybackState(mpegAddr).decodeMode); } + bool mpegFrameRateIsSane(int num, int den) + { + // Accept only plausible movie frame rates (10..120 fps). A garbage-low rate would make + // each frame's due tick enormous; a garbage-high rate would collapse pacing. Anything + // outside the range is rejected so the caller keeps the default NTSC fallback. + // This is a plausibility gate, not a correctness one: an interlaced field rate like 59.94 + // (interlaced MPEG-2) would pass and double-pace -- but PS2 FMVs are overwhelmingly + // 29.97/25 fps, so the frame-rate read is trusted rather than field/frame-disambiguated. + if (num <= 0 || den <= 0) + { + return false; + } + const int64_t n = num; + const int64_t d = den; + return n >= kMpegMinSaneFps * d && n <= kMpegMaxSaneFps * d; + } + + uint64_t mpegFrameDueTick(uint64_t baseTick, uint64_t frameIndex, uint64_t vsyncHz, + int fpsNum, int fpsDen) + { + if (fpsNum <= 0 || fpsDen <= 0) + { + return baseTick; + } + return baseTick + + (frameIndex * vsyncHz * static_cast(fpsDen)) / + static_cast(fpsNum); + } + + void mpegParkUntilDueTick(uint8_t *rdram, PS2Runtime *runtime, uint64_t dueTick) + { + // No lock held across the park (caller released g_mpeg_stub_mutex first). Hard-cap a + // single park at kMpegPacingMaxParkVsyncTicks so one GetPicture call can never block + // more than a few vblanks even if the due tick is pathological -- pacing meters + // delivery, it must never stall the guest. Break out on shutdown so parking never + // wedges runtime teardown: without the isStopRequested() guard, once the tick counter + // stops advancing at shutdown WaitForNextVSyncTick would return immediately every + // iteration and this loop would spin instead of exiting. + const uint64_t nowTick = ps2_syscalls::GetCurrentVSyncTick(); + const uint64_t cappedDueTick = std::min(dueTick, nowTick + kMpegPacingMaxParkVsyncTicks); + while (runtime && !runtime->isStopRequested() && + ps2_syscalls::GetCurrentVSyncTick() < cappedDueTick) + { + (void)ps2_syscalls::WaitForNextVSyncTick(rdram, runtime); + } + } + void sceMpegGetPicture(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { const uint32_t mpegAddr = getRegU32(ctx, 4); @@ -1918,6 +2051,8 @@ namespace ps2_stubs uint32_t frameCount = 0u; bool haveFrame = false; MpegDecodedFrame frame; + bool shouldPace = false; + uint64_t pacingDueTick = 0u; { PS2Runtime::GuestExecutionReleaseScope releaseGuestExecution(runtime); std::unique_lock lock(g_mpeg_stub_mutex); @@ -2013,6 +2148,39 @@ namespace ps2_stubs playback.lastFrame = frame; playback.hasLastFrame = true; haveFrame = true; + // Frame-rate pacing: meter real decoded frames to the stream's own frame rate, + // so host decode (far faster than real IPU delivery) does not drain the + // elementary stream across a handful of calls. Reserved here under + // g_mpeg_stub_mutex; the actual park runs after the lock is released. + if (!playback.baseVsyncSet) + { + playback.baseVsyncSet = true; + playback.baseVsyncTick = ps2_syscalls::GetCurrentVSyncTick(); + } + // Latch the stream's real frame rate on the first VALID, in-range read + // (AVCodecContext::framerate is populated once the MPEG-2 sequence header is + // parsed). Re-read on later served frames until it is known, so a not-yet- + // populated rate on the first frame never pins the handle to the fallback + // forever; out-of-range rates are rejected as garbage. + if (!playback.fpsKnown) + { + int detectedNum = 0; + int detectedDen = 0; + if (playback.decoder && + playback.decoder->getFrameRate(detectedNum, detectedDen) && + mpegFrameRateIsSane(detectedNum, detectedDen)) + { + playback.fpsNum = detectedNum; + playback.fpsDen = detectedDen; + playback.fpsKnown = true; + } + } + pacingDueTick = mpegFrameDueTick(playback.baseVsyncTick, + playback.nextPacingFrameIndex++, + kMpegPacingVsyncHz, + playback.fpsNum, + playback.fpsDen); + shouldPace = true; if (g_mpeg_stub_state.pictureTraceCount < 32u) { PS2_IF_AGRESSIVE_LOGS({ @@ -2065,6 +2233,18 @@ namespace ps2_stubs } } + // Park AFTER the lock/scope above closes, mirroring sceGsSyncV (which calls + // WaitForNextVSyncTick with no enclosing release scope): the park must not run while + // g_mpeg_stub_mutex is held, or a multi-vblank wait would block stream-feed callbacks and + // other handles. Nesting it back inside the scope would NOT double-release -- + // releaseGuestExecution is idempotent (a depth-0 release is a no-op; see ps2_runtime.cpp): + // WaitForNextVSyncTick's own per-wait release/reacquire only does real handoff work when + // guest execution is actually held on entry. Do not "tidy" the park into the block above. + if (shouldPace) + { + mpegParkUntilDueTick(rdram, runtime, pacingDueTick); + } + mpegGuestWrite32(rdram, mpegAddr + 0x00u, width); mpegGuestWrite32(rdram, mpegAddr + 0x04u, height); mpegGuestWrite32(rdram, mpegAddr + 0x08u, frameCount); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG_internal.h b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG_internal.h new file mode 100644 index 000000000..006faf39f --- /dev/null +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG_internal.h @@ -0,0 +1,33 @@ +#pragma once + +#include "ps2_stubs.h" + +// Internal helpers for the sceMpegGetPicture frame-rate-pacing path, split out of MPEG.cpp so the +// pacing regression tests can link them directly. Not part of the guest syscall surface +// (MPEG.h); never registered as guest entry points. +namespace ps2_stubs +{ + // due(N): the vsync tick at which movie frame N is due, computed fresh from the frame + // index every call (never accumulated) so integer rounding cannot drift over a long movie: + // base + floor(frameIndex * vsyncHz * fpsDen / fpsNum) + // Returns base unchanged when the rate is unknown/degenerate (fpsNum/fpsDen <= 0). + uint64_t mpegFrameDueTick(uint64_t baseTick, uint64_t frameIndex, uint64_t vsyncHz, + int fpsNum, int fpsDen); + + // True when num/den is a plausible movie frame rate (10..120 fps). Rejects a not-yet- + // populated or garbage decoder rate before it is latched for pacing. + bool mpegFrameRateIsSane(int num, int den); + + // Park the calling thread until the movie's frame-rate cadence reaches dueTick, using the existing + // vsync-tick primitive. Holds no lock; caps a single park at a few vblanks; breaks out + // promptly on runtime stop. + void mpegParkUntilDueTick(uint8_t *rdram, PS2Runtime *runtime, uint64_t dueTick); + + // Test seam: report the frame rate latched for a handle's pacing. The latch captures + // AVCodecContext::framerate on the first valid, in-range read (30000/1001 fallback until + // then). Returns true once a real stream rate has been latched for mpegAddr, with num/den + // set to it; false (num/den left untouched) if the handle is unknown or still on the + // fallback. Not a guest entry point; lets the pacing suite pin the latch directly + // (jitter-free), independent of the end-to-end delivery span. Takes g_mpeg_stub_mutex. + bool mpegLatchedFrameRate(uint32_t mpegAddr, int &num, int &den); +} diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.cpp index 220eadf18..7155070c9 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.cpp @@ -9,7 +9,6 @@ namespace ps2_syscalls { constexpr uint32_t kIntcVblankStart = 2u; constexpr uint32_t kIntcVblankEnd = 3u; - constexpr auto kVblankPeriod = std::chrono::microseconds(16667); constexpr int kMaxCatchupTicks = 4; std::mutex g_irq_handler_mutex; diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.h b/ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.h index eb9ba5f03..2fd58db95 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "ps2_syscalls.h" @@ -7,6 +8,12 @@ namespace ps2_syscalls { namespace interrupt_state { + // Period of the emulator's vsync tick worker (GetCurrentVSyncTick advances once per + // period). Authoritative single source of truth: the interrupt worker sleeps this long + // between ticks (Interrupt.cpp), and sceMpeg frame-rate pacing static_asserts its 60Hz rate + // against it (Stubs/MPEG.cpp). ~1/60s, independent of the guest's NTSC/PAL display mode. + constexpr auto kVblankPeriod = std::chrono::microseconds(16667); + struct VSyncFlagRegistration { uint32_t flagAddr; diff --git a/ps2xTest/CMakeLists.txt b/ps2xTest/CMakeLists.txt index ebe5b90ae..ade129c6c 100644 --- a/ps2xTest/CMakeLists.txt +++ b/ps2xTest/CMakeLists.txt @@ -58,6 +58,7 @@ add_library(ps2_test_lib STATIC src/ps2_sif_dma_tests.cpp src/ps2_recompiler_tests.cpp src/ps2_runtime_expansion_tests.cpp + src/ps2_mpeg_pacing_tests.cpp ) target_include_directories(ps2_test_lib PRIVATE diff --git a/ps2xTest/src/main.cpp b/ps2xTest/src/main.cpp index 71d2cd0e3..9e2f6cd2a 100644 --- a/ps2xTest/src/main.cpp +++ b/ps2xTest/src/main.cpp @@ -9,6 +9,7 @@ void register_pad_input_tests(); void register_ps2_runtime_io_tests(); void register_ps2_runtime_kernel_tests(); void register_ps2_runtime_interrupt_tests(); +void register_ps2_mpeg_pacing_tests(); void register_ps2_memory_tests(); void register_ps2_vu1_tests(); void register_ps2_gs_tests(); @@ -30,6 +31,7 @@ int main() register_ps2_runtime_io_tests(); register_ps2_runtime_kernel_tests(); register_ps2_runtime_interrupt_tests(); + register_ps2_mpeg_pacing_tests(); register_ps2_memory_tests(); register_ps2_vu1_tests(); register_ps2_gs_tests(); diff --git a/ps2xTest/src/mpeg_pacing_fixture_m2v.h b/ps2xTest/src/mpeg_pacing_fixture_m2v.h new file mode 100644 index 000000000..712449020 --- /dev/null +++ b/ps2xTest/src/mpeg_pacing_fixture_m2v.h @@ -0,0 +1,223 @@ +#pragma once + +// Synthetic MPEG-2 test vector for the sceMpegGetPicture pacing regression suite: a 16x16, +// 25fps (PAL) intra-only (all-I, no B-frames) elementary stream (6 coded pictures), generated with: +// +// ffmpeg -y -f lavfi -i "testsrc=size=16x16:rate=25:duration=0.24" \ +// -c:v mpeg2video -g 1 -bf 0 -pix_fmt yuv420p -f mpeg2video /tmp/fixture.m2v +// xxd -i -n kMpegPacingFixtureM2v /tmp/fixture.m2v > mpeg_pacing_fixture_m2v.h +// +// 25fps is deliberately NOT the 30000/1001 pacing fallback: at 60Hz its due-tick spacing +// (floor(N*60/25) = 0,2,4,7,9,12) diverges from the fallback's (0,2,4,6,8,10) by frame 3, so the +// end-to-end delivery test genuinely exercises the getFrameRate() latch and the sane-rate clamp +// through the real decode path -- a fallback-rate fixture would pass even with the latch broken. +// All-intra (-g 1) so every coded picture decodes independently and immediately, one MPEG-2 +// access unit in, one decoded frame out -- the simplest reliable shape for pinning per-frame +// pacing without depending on a decoder's P-frame reference/skip-frame warm-up behavior. + +unsigned char kMpegPacingFixtureM2v[] = { + 0x00, 0x00, 0x01, 0xb3, 0x01, 0x00, 0x10, 0x13, 0xff, 0xff, 0xe0, 0x18, + 0x00, 0x00, 0x01, 0xb5, 0x14, 0x8a, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xb8, 0x00, 0x08, 0x00, 0x40, 0x00, 0x00, 0x01, 0x00, 0x00, 0x0f, + 0xff, 0xf8, 0x00, 0x00, 0x01, 0xb5, 0x8f, 0xff, 0xf3, 0x41, 0x80, 0x00, + 0x00, 0x01, 0x01, 0x13, 0xf3, 0xa8, 0x00, 0x6e, 0x01, 0x70, 0x20, 0x7e, + 0x38, 0x20, 0x7e, 0x18, 0x03, 0x10, 0x1d, 0x00, 0x9c, 0x03, 0x42, 0x60, + 0x0c, 0x40, 0x30, 0x00, 0xb8, 0x04, 0xe4, 0x30, 0x07, 0xc8, 0x01, 0xd0, + 0x06, 0xa0, 0x3a, 0x21, 0x06, 0xa1, 0x24, 0xd0, 0x0d, 0x10, 0x43, 0x01, + 0xd0, 0x0c, 0x43, 0x4b, 0x0c, 0x4f, 0x21, 0x94, 0x5e, 0x48, 0x0e, 0xc0, + 0x4c, 0x34, 0x85, 0xfa, 0xf0, 0x62, 0x0a, 0xe3, 0x10, 0x9e, 0x9b, 0xd6, + 0x82, 0x00, 0x18, 0x82, 0x00, 0x15, 0x13, 0x41, 0x03, 0xea, 0x40, 0x0c, + 0x00, 0x42, 0x01, 0x80, 0x03, 0xd0, 0x0d, 0x40, 0x62, 0x03, 0xb0, 0x07, + 0xe4, 0xd0, 0x1d, 0x10, 0xc0, 0x0d, 0x00, 0x35, 0x01, 0x38, 0x08, 0x40, + 0xaa, 0x79, 0x40, 0x27, 0x28, 0x00, 0xfc, 0x34, 0x84, 0x01, 0x58, 0x69, + 0x78, 0x07, 0x69, 0x48, 0x14, 0x48, 0x68, 0x14, 0x21, 0x14, 0x43, 0x01, + 0x41, 0x65, 0xe2, 0x16, 0xf8, 0x31, 0x92, 0xde, 0xe6, 0x80, 0x38, 0x01, + 0x88, 0x02, 0xf0, 0x40, 0x02, 0x90, 0x40, 0xfc, 0xa0, 0x04, 0xe1, 0xa0, + 0x82, 0x03, 0xa0, 0x0f, 0xc0, 0x42, 0x90, 0x0d, 0x40, 0x1f, 0x80, 0x5e, + 0x00, 0xfc, 0x98, 0x18, 0x03, 0x60, 0x2a, 0x42, 0xe9, 0xc0, 0x21, 0x02, + 0xa0, 0x31, 0x02, 0xbc, 0x30, 0xa0, 0x42, 0x00, 0x80, 0xde, 0x58, 0x0d, + 0x90, 0x03, 0x1c, 0xe5, 0xec, 0xbd, 0xb2, 0x46, 0x61, 0xb5, 0x60, 0x80, + 0x09, 0xc0, 0x0e, 0xc0, 0x16, 0x02, 0x00, 0x1e, 0x00, 0x30, 0x00, 0xc8, + 0x01, 0xd9, 0x0c, 0x01, 0x88, 0x03, 0xc0, 0x1d, 0xe2, 0x68, 0xd2, 0x58, + 0x60, 0x68, 0x0c, 0x40, 0x74, 0x02, 0x60, 0x1b, 0x3f, 0x21, 0xa0, 0x24, + 0xb4, 0x93, 0x49, 0xa7, 0x24, 0x86, 0x31, 0x68, 0xcb, 0x43, 0x1b, 0x20, + 0x0c, 0x39, 0x60, 0x05, 0x85, 0x80, 0x12, 0x94, 0x00, 0x56, 0xe9, 0xe0, + 0x0f, 0xd0, 0x00, 0xa8, 0x10, 0x40, 0x71, 0xf0, 0x00, 0x97, 0x13, 0x11, + 0xb8, 0x05, 0xe3, 0x49, 0x5c, 0x07, 0x40, 0x07, 0xe8, 0x00, 0xbb, 0x17, + 0xd2, 0x05, 0x00, 0x27, 0xe4, 0x20, 0x1b, 0xf4, 0x62, 0x4f, 0x29, 0x52, + 0x01, 0x3a, 0x12, 0x05, 0x00, 0x14, 0x00, 0x39, 0x00, 0x5a, 0x00, 0xe0, + 0x6a, 0x00, 0x2e, 0x26, 0xe0, 0x06, 0xdc, 0x06, 0x05, 0x80, 0xc0, 0x6f, + 0x26, 0x95, 0x89, 0x60, 0x09, 0x38, 0x02, 0xf6, 0x01, 0x81, 0x4e, 0x02, + 0x72, 0x12, 0x03, 0x4b, 0x28, 0x01, 0x88, 0x01, 0xdb, 0x86, 0x23, 0x06, + 0xf2, 0x69, 0x0f, 0x77, 0x00, 0xb4, 0x26, 0x00, 0x00, 0x01, 0xb3, 0x01, + 0x00, 0x10, 0x13, 0xff, 0xff, 0xe0, 0x18, 0x00, 0x00, 0x01, 0xb5, 0x14, + 0x8a, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xb8, 0x00, 0x08, 0x00, + 0xc0, 0x00, 0x00, 0x01, 0x00, 0x00, 0x0f, 0xff, 0xf8, 0x00, 0x00, 0x01, + 0xb5, 0x8f, 0xff, 0xf3, 0x41, 0x80, 0x00, 0x00, 0x01, 0x01, 0x13, 0xf3, + 0xa8, 0x00, 0x6e, 0x01, 0x70, 0x20, 0x7e, 0x38, 0x20, 0x7e, 0x18, 0x03, + 0x10, 0x1d, 0x00, 0x9c, 0x03, 0x42, 0x60, 0x0c, 0x40, 0x30, 0x00, 0xb8, + 0x04, 0xe4, 0x30, 0x07, 0xc8, 0x01, 0xd0, 0x06, 0xa0, 0x3a, 0x21, 0x06, + 0xa1, 0x24, 0xd0, 0x0d, 0x10, 0x43, 0x01, 0xd0, 0x0c, 0x43, 0x4b, 0x0c, + 0x4f, 0x21, 0x94, 0x5e, 0x48, 0x0e, 0xc0, 0x4c, 0x34, 0x85, 0xfa, 0xf0, + 0x62, 0x0a, 0xe3, 0x10, 0x9e, 0x9b, 0xd6, 0x82, 0x00, 0x18, 0x82, 0x00, + 0x15, 0x13, 0x41, 0x03, 0xea, 0x40, 0x0c, 0x00, 0x42, 0x01, 0x80, 0x03, + 0xd0, 0x0d, 0x40, 0x62, 0x03, 0xb0, 0x07, 0xe4, 0xd0, 0x1d, 0x10, 0xc0, + 0x0d, 0x00, 0x35, 0x01, 0x38, 0x08, 0x40, 0xaa, 0x79, 0x40, 0x27, 0x28, + 0x00, 0xfc, 0x34, 0x84, 0x01, 0x58, 0x69, 0x78, 0x07, 0x69, 0x48, 0x14, + 0x48, 0x68, 0x14, 0x21, 0x14, 0x43, 0x01, 0x41, 0x65, 0xe2, 0x16, 0xf8, + 0x31, 0x92, 0xde, 0xe7, 0x00, 0x34, 0x01, 0x88, 0x02, 0xf0, 0x02, 0x00, + 0x40, 0xfc, 0xb0, 0x04, 0xa4, 0xd0, 0x41, 0x01, 0xd0, 0x07, 0xe0, 0x27, + 0x48, 0x06, 0xa0, 0x0f, 0xc0, 0x2f, 0x00, 0x7e, 0x4c, 0x26, 0x00, 0xd8, + 0x0a, 0x90, 0x92, 0x5e, 0x01, 0x08, 0x15, 0x01, 0x88, 0x0e, 0xf8, 0x61, + 0x40, 0x84, 0x01, 0x01, 0xbc, 0xb0, 0x1b, 0x20, 0x04, 0xf9, 0xcb, 0x46, + 0x5e, 0x46, 0x4b, 0x61, 0xb5, 0x20, 0x80, 0x09, 0x80, 0x0f, 0x40, 0x15, + 0x02, 0x00, 0x1f, 0x00, 0x34, 0x00, 0xc0, 0x01, 0xe9, 0x34, 0x01, 0x78, + 0x03, 0xc0, 0x18, 0xe2, 0x68, 0xd2, 0x59, 0x45, 0x80, 0xc4, 0x07, 0x40, + 0x26, 0xc4, 0xc7, 0xe4, 0x34, 0x04, 0x96, 0x92, 0x19, 0x34, 0xe4, 0x90, + 0xc6, 0x2d, 0x19, 0x79, 0x8d, 0x90, 0x05, 0xdc, 0xb0, 0x02, 0xe2, 0xc0, + 0x09, 0x43, 0x00, 0x0a, 0xdd, 0x29, 0x00, 0x7e, 0x80, 0x05, 0x40, 0x82, + 0x03, 0x8f, 0x80, 0x04, 0xa8, 0x26, 0x23, 0x70, 0x0b, 0xc6, 0x92, 0x92, + 0x03, 0xa0, 0x03, 0xf4, 0x00, 0x5d, 0x8b, 0xe9, 0x02, 0x80, 0x13, 0xf2, + 0x10, 0x0d, 0xfa, 0x31, 0x27, 0x94, 0xa9, 0x00, 0x85, 0x09, 0x01, 0xd0, + 0x02, 0xa0, 0x07, 0x20, 0x0b, 0x40, 0x1d, 0x12, 0xd0, 0x01, 0x71, 0x37, + 0x00, 0x36, 0xe0, 0x30, 0x2c, 0x06, 0x03, 0x79, 0x35, 0x18, 0x96, 0x00, + 0x93, 0x80, 0x2f, 0x60, 0x18, 0x14, 0xe0, 0x27, 0x21, 0x20, 0x34, 0xb2, + 0x80, 0x19, 0x80, 0x1d, 0xb8, 0x61, 0x58, 0x37, 0x93, 0x48, 0x7b, 0xb8, + 0x05, 0xa1, 0x30, 0x00, 0x00, 0x01, 0xb3, 0x01, 0x00, 0x10, 0x13, 0xff, + 0xff, 0xe0, 0x18, 0x00, 0x00, 0x01, 0xb5, 0x14, 0x8a, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x01, 0xb8, 0x00, 0x08, 0x01, 0x40, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x0f, 0xff, 0xf8, 0x00, 0x00, 0x01, 0xb5, 0x8f, 0xff, 0xf3, + 0x41, 0x80, 0x00, 0x00, 0x01, 0x01, 0x13, 0xf3, 0xa8, 0x00, 0x6e, 0x01, + 0x70, 0x20, 0x7e, 0x38, 0x20, 0x7e, 0x18, 0x03, 0x10, 0x1d, 0x00, 0x9c, + 0x03, 0x42, 0x60, 0x0c, 0x40, 0x30, 0x00, 0xb8, 0x04, 0xe4, 0x30, 0x07, + 0xc8, 0x01, 0xd0, 0x06, 0xa0, 0x3a, 0x21, 0x06, 0xa1, 0x24, 0xd0, 0x0d, + 0x10, 0x43, 0x01, 0xd0, 0x0c, 0x43, 0x4b, 0x0c, 0x4f, 0x21, 0x94, 0x5e, + 0x48, 0x0e, 0xc0, 0x4c, 0x34, 0x85, 0xfa, 0xf0, 0x62, 0x0a, 0xe3, 0x10, + 0x9e, 0x9b, 0xd6, 0x82, 0x00, 0x18, 0x82, 0x00, 0x15, 0x13, 0x41, 0x03, + 0xea, 0x40, 0x0c, 0x00, 0x42, 0x01, 0x80, 0x03, 0xd0, 0x0d, 0x40, 0x62, + 0x03, 0xb0, 0x07, 0xe4, 0xd0, 0x1d, 0x10, 0xc0, 0x0d, 0x00, 0x35, 0x01, + 0x38, 0x08, 0x40, 0xaa, 0x79, 0x40, 0x27, 0x28, 0x00, 0xfc, 0x34, 0x84, + 0x01, 0x58, 0x69, 0x78, 0x07, 0x69, 0x48, 0x14, 0x48, 0x68, 0x14, 0x21, + 0x14, 0x43, 0x01, 0x41, 0x65, 0xe2, 0x16, 0xf8, 0x31, 0x92, 0xde, 0xe7, + 0x00, 0x30, 0x01, 0x38, 0x02, 0xd0, 0x02, 0x20, 0x40, 0xfc, 0xd0, 0x04, + 0xa4, 0xde, 0x00, 0x88, 0x01, 0xf8, 0x0c, 0x4b, 0x01, 0x08, 0x03, 0xf0, + 0x0b, 0xc0, 0x2f, 0x26, 0x13, 0x00, 0x6c, 0x05, 0x48, 0x49, 0x2f, 0x00, + 0x84, 0x0a, 0x80, 0xc4, 0x07, 0x7c, 0x30, 0x30, 0x10, 0x80, 0x20, 0x37, + 0x97, 0x89, 0x88, 0x01, 0x3e, 0x72, 0xd1, 0x97, 0x91, 0x92, 0xd8, 0x6a, + 0xaa, 0x41, 0x00, 0x12, 0xc0, 0x1f, 0x80, 0x2a, 0x04, 0x00, 0x3f, 0x00, + 0x6c, 0x01, 0x80, 0x03, 0xf2, 0x68, 0x02, 0xd0, 0x07, 0x80, 0x27, 0x41, + 0x34, 0x69, 0x2c, 0xa2, 0xc0, 0x62, 0x03, 0xa0, 0x13, 0x62, 0x63, 0xf2, + 0x1a, 0x02, 0x4b, 0xe4, 0x32, 0x69, 0xc9, 0x21, 0x8c, 0x5e, 0xcb, 0xcc, + 0x6c, 0x80, 0x2e, 0xe5, 0x80, 0x17, 0x06, 0x80, 0x12, 0x93, 0x00, 0x0a, + 0xdd, 0x29, 0x00, 0x7e, 0x80, 0x05, 0x40, 0x82, 0x03, 0x8f, 0x80, 0x04, + 0xa8, 0x26, 0x23, 0x70, 0x0b, 0xc6, 0x92, 0x92, 0x03, 0xa0, 0x03, 0xf4, + 0x00, 0x5d, 0x8b, 0xe9, 0x02, 0x80, 0x13, 0xf2, 0x10, 0x0d, 0xfa, 0x31, + 0x27, 0x94, 0xa9, 0x00, 0x6a, 0x84, 0x80, 0xc0, 0x01, 0x50, 0x03, 0x90, + 0x05, 0xa0, 0x0e, 0x86, 0xa0, 0x02, 0xe2, 0x1e, 0x00, 0x6d, 0xc0, 0x60, + 0x58, 0x09, 0x86, 0xa4, 0x9a, 0x8c, 0x4b, 0x00, 0x49, 0xc0, 0x17, 0xb0, + 0x0c, 0x03, 0x1c, 0x06, 0x24, 0x24, 0x06, 0x96, 0x50, 0x03, 0x30, 0x03, + 0xb7, 0x0c, 0x2b, 0x06, 0xf2, 0x69, 0x0f, 0x77, 0x00, 0xb4, 0x26, 0x00, + 0x00, 0x01, 0xb3, 0x01, 0x00, 0x10, 0x13, 0xff, 0xff, 0xe0, 0x18, 0x00, + 0x00, 0x01, 0xb5, 0x14, 0x8a, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xb8, 0x00, 0x08, 0x01, 0xc0, 0x00, 0x00, 0x01, 0x00, 0x00, 0x0f, 0xff, + 0xf8, 0x00, 0x00, 0x01, 0xb5, 0x8f, 0xff, 0xf3, 0x41, 0x80, 0x00, 0x00, + 0x01, 0x01, 0x13, 0xf3, 0xa8, 0x00, 0x6e, 0x01, 0x70, 0x20, 0x7e, 0x38, + 0x20, 0x7e, 0x18, 0x03, 0x10, 0x1d, 0x00, 0x9c, 0x03, 0x42, 0x60, 0x0c, + 0x40, 0x30, 0x00, 0xb8, 0x04, 0xe4, 0x30, 0x07, 0xc8, 0x01, 0xd0, 0x06, + 0xa0, 0x3a, 0x21, 0x06, 0xa1, 0x24, 0xd0, 0x0d, 0x10, 0x43, 0x01, 0xd0, + 0x0c, 0x43, 0x4b, 0x0c, 0x4f, 0x21, 0x94, 0x5e, 0x48, 0x0e, 0xc0, 0x4c, + 0x34, 0x85, 0xfa, 0xf0, 0x62, 0x0a, 0xe3, 0x10, 0x9e, 0x9b, 0xd6, 0x82, + 0x00, 0x18, 0x82, 0x00, 0x15, 0x13, 0x41, 0x03, 0xea, 0x40, 0x0c, 0x00, + 0x42, 0x01, 0x80, 0x03, 0xd0, 0x0d, 0x40, 0x62, 0x03, 0xb0, 0x07, 0xe4, + 0xd0, 0x1d, 0x10, 0xc0, 0x0d, 0x00, 0x35, 0x01, 0x38, 0x08, 0x40, 0xaa, + 0x79, 0x40, 0x27, 0x28, 0x00, 0xfc, 0x34, 0x84, 0x01, 0x58, 0x69, 0x78, + 0x07, 0x69, 0x48, 0x14, 0x48, 0x68, 0x14, 0x21, 0x14, 0x43, 0x01, 0x41, + 0x65, 0xe2, 0x16, 0xf8, 0x31, 0x92, 0xde, 0xe7, 0x80, 0x2e, 0x01, 0x08, + 0x02, 0xd0, 0x02, 0x40, 0x40, 0xfc, 0xe0, 0x04, 0x64, 0x3e, 0x00, 0x88, + 0x01, 0xe8, 0x0e, 0xcb, 0x01, 0x08, 0x03, 0xd0, 0x0b, 0xc0, 0x2f, 0x26, + 0x10, 0xb9, 0x30, 0x07, 0x64, 0x24, 0x97, 0x80, 0x42, 0x05, 0x40, 0x62, + 0x03, 0xbe, 0x50, 0x60, 0x21, 0x00, 0x48, 0x6f, 0x2f, 0x13, 0x30, 0x09, + 0xf6, 0xe5, 0xa1, 0x0b, 0xc8, 0xc9, 0x6c, 0x35, 0x56, 0x70, 0x40, 0x04, + 0x90, 0x07, 0xe0, 0x0a, 0x81, 0x00, 0x10, 0x00, 0x1d, 0x00, 0x5c, 0x01, + 0x78, 0x68, 0x02, 0xd0, 0x07, 0x80, 0x21, 0x41, 0x0f, 0x24, 0x96, 0x82, + 0xc0, 0x62, 0x03, 0xa0, 0x13, 0x62, 0x13, 0xf0, 0x2a, 0x80, 0x92, 0xf9, + 0x0c, 0x9b, 0x94, 0x92, 0x68, 0xc5, 0xe4, 0x2f, 0x31, 0xb2, 0x00, 0xbb, + 0x96, 0x00, 0x5c, 0x4d, 0x00, 0x25, 0x21, 0x00, 0x15, 0xba, 0x4b, 0x00, + 0x7e, 0x80, 0x05, 0x40, 0x82, 0x03, 0x8f, 0x80, 0x04, 0xa8, 0x26, 0x23, + 0x70, 0x0b, 0xc6, 0x92, 0x92, 0x03, 0xa0, 0x03, 0xf2, 0x80, 0x2e, 0xc5, + 0xf4, 0x81, 0x40, 0x09, 0xf9, 0x08, 0x06, 0xfd, 0x18, 0x93, 0xca, 0x54, + 0x80, 0x33, 0x42, 0x40, 0x40, 0x00, 0xa8, 0x01, 0xd8, 0x02, 0xf0, 0x07, + 0x83, 0x4a, 0x00, 0xb8, 0x87, 0x80, 0x1b, 0x70, 0x13, 0x16, 0x02, 0x61, + 0xa9, 0x26, 0xa3, 0x01, 0x90, 0x04, 0x9c, 0x01, 0x7b, 0x00, 0xc0, 0x30, + 0x68, 0x0c, 0x40, 0xa2, 0x03, 0x4b, 0x28, 0x01, 0x98, 0x01, 0xdb, 0x86, + 0x15, 0x83, 0x79, 0x34, 0x87, 0xbb, 0x80, 0x5a, 0x13, 0x00, 0x00, 0x00, + 0x01, 0xb3, 0x01, 0x00, 0x10, 0x13, 0xff, 0xff, 0xe0, 0x18, 0x00, 0x00, + 0x01, 0xb5, 0x14, 0x8a, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xb8, + 0x00, 0x08, 0x02, 0x40, 0x00, 0x00, 0x01, 0x00, 0x00, 0x0f, 0xff, 0xf8, + 0x00, 0x00, 0x01, 0xb5, 0x8f, 0xff, 0xf3, 0x41, 0x80, 0x00, 0x00, 0x01, + 0x01, 0x13, 0xf3, 0xa8, 0x00, 0x6e, 0x01, 0x70, 0x20, 0x7e, 0x38, 0x20, + 0x7e, 0x18, 0x03, 0x10, 0x1d, 0x00, 0x9c, 0x03, 0x42, 0x60, 0x0c, 0x40, + 0x30, 0x00, 0xb8, 0x04, 0xe4, 0x30, 0x07, 0xc8, 0x01, 0xd0, 0x06, 0xa0, + 0x3a, 0x21, 0x06, 0xa1, 0x24, 0xd0, 0x0d, 0x10, 0x43, 0x01, 0xd0, 0x0c, + 0x43, 0x4b, 0x0c, 0x4f, 0x21, 0x94, 0x5e, 0x48, 0x0e, 0xc0, 0x4c, 0x34, + 0x85, 0xfa, 0xf0, 0x62, 0x0a, 0xe3, 0x10, 0x9e, 0x9b, 0xd6, 0x82, 0x00, + 0x18, 0x82, 0x00, 0x15, 0x13, 0x41, 0x03, 0xea, 0x40, 0x0c, 0x00, 0x42, + 0x01, 0x80, 0x03, 0xd0, 0x0d, 0x40, 0x62, 0x03, 0xb0, 0x07, 0xe4, 0xd0, + 0x1d, 0x10, 0xc0, 0x0d, 0x00, 0x35, 0x01, 0x38, 0x08, 0x40, 0xaa, 0x79, + 0x40, 0x27, 0x28, 0x00, 0xfc, 0x34, 0x84, 0x01, 0x58, 0x69, 0x78, 0x07, + 0x69, 0x48, 0x14, 0x48, 0x68, 0x14, 0x21, 0x14, 0x43, 0x01, 0x41, 0x65, + 0xe2, 0x16, 0xf8, 0x31, 0x92, 0xde, 0xe7, 0x80, 0x2c, 0x00, 0xd4, 0x01, + 0x68, 0x01, 0x30, 0x20, 0x7e, 0x78, 0x02, 0x30, 0x2a, 0x90, 0x04, 0x00, + 0x0f, 0x40, 0xa8, 0x68, 0x08, 0x40, 0x1e, 0x80, 0x5e, 0x01, 0x79, 0x30, + 0x0a, 0x72, 0x60, 0x0e, 0xc8, 0x45, 0x97, 0x80, 0x42, 0x05, 0x40, 0x76, + 0x03, 0xbe, 0x50, 0x60, 0x21, 0x00, 0x49, 0x7c, 0xbc, 0x4c, 0xc0, 0x27, + 0xdb, 0x96, 0x84, 0x2f, 0x20, 0x6b, 0x61, 0xaa, 0xb3, 0x82, 0x00, 0x24, + 0x00, 0x5e, 0x00, 0xa0, 0x10, 0x01, 0x04, 0x01, 0xe0, 0x05, 0xc0, 0x17, + 0x86, 0x80, 0x2b, 0x00, 0x74, 0x02, 0x14, 0x10, 0xf2, 0x79, 0x79, 0x20, + 0x31, 0x01, 0xd0, 0x09, 0x90, 0x42, 0x74, 0x81, 0x54, 0x32, 0x4b, 0xe4, + 0x32, 0x1e, 0x52, 0x49, 0xac, 0xbc, 0x85, 0xe6, 0x36, 0x40, 0x17, 0x72, + 0xc0, 0x0b, 0x88, 0x60, 0x04, 0xa0, 0x50, 0x00, 0xad, 0xd2, 0x58, 0x03, + 0xf4, 0x00, 0x2a, 0x04, 0x10, 0x1c, 0xdf, 0x00, 0x09, 0x50, 0x4c, 0x46, + 0xe0, 0x17, 0x8d, 0x25, 0x16, 0x03, 0xa0, 0x03, 0xf2, 0x80, 0x2e, 0xc5, + 0xf4, 0x81, 0x40, 0x09, 0xf9, 0x08, 0x06, 0xfd, 0x18, 0x92, 0x4a, 0x54, + 0x80, 0x31, 0x42, 0x40, 0x34, 0x00, 0x58, 0x00, 0xec, 0x01, 0x78, 0x03, + 0xe1, 0xa5, 0x00, 0x5c, 0x05, 0x70, 0x03, 0x6e, 0x02, 0x62, 0xc0, 0x4c, + 0x35, 0x24, 0xd4, 0x60, 0x32, 0x00, 0x93, 0x80, 0x2f, 0x60, 0x18, 0x06, + 0x0d, 0x01, 0x88, 0x14, 0x40, 0x69, 0x68, 0x00, 0x66, 0x00, 0x76, 0x34, + 0x30, 0xac, 0x1b, 0xc9, 0xa4, 0x3d, 0xdc, 0x02, 0xd0, 0x98, 0x00, 0x00, + 0x01, 0xb3, 0x01, 0x00, 0x10, 0x13, 0xff, 0xff, 0xe0, 0x18, 0x00, 0x00, + 0x01, 0xb5, 0x14, 0x8a, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0xb8, + 0x00, 0x08, 0x02, 0xc0, 0x00, 0x00, 0x01, 0x00, 0x00, 0x0f, 0xff, 0xf8, + 0x00, 0x00, 0x01, 0xb5, 0x8f, 0xff, 0xf3, 0x41, 0x80, 0x00, 0x00, 0x01, + 0x01, 0x13, 0xf3, 0xa8, 0x00, 0x6e, 0x01, 0x70, 0x20, 0x7e, 0x38, 0x20, + 0x7e, 0x18, 0x03, 0x10, 0x1d, 0x00, 0x9c, 0x03, 0x42, 0x60, 0x0c, 0x40, + 0x30, 0x00, 0xb8, 0x04, 0xe4, 0x30, 0x07, 0xc8, 0x01, 0xd0, 0x06, 0xa0, + 0x3a, 0x21, 0x06, 0xa1, 0x24, 0xd0, 0x0d, 0x10, 0x43, 0x01, 0xd0, 0x0c, + 0x43, 0x4b, 0x0c, 0x4f, 0x21, 0x94, 0x5e, 0x48, 0x0e, 0xc0, 0x4c, 0x34, + 0x85, 0xfa, 0xf0, 0x62, 0x0a, 0xe3, 0x10, 0x9e, 0x9b, 0xd6, 0x82, 0x00, + 0x18, 0x82, 0x00, 0x15, 0x13, 0x41, 0x03, 0xea, 0x40, 0x0c, 0x00, 0x42, + 0x01, 0x80, 0x03, 0xd0, 0x0d, 0x40, 0x62, 0x03, 0xb0, 0x07, 0xe4, 0xd0, + 0x1d, 0x10, 0xc0, 0x0d, 0x00, 0x35, 0x01, 0x38, 0x08, 0x40, 0xaa, 0x79, + 0x40, 0x27, 0x28, 0x00, 0xfc, 0x34, 0x84, 0x01, 0x58, 0x69, 0x78, 0x07, + 0x69, 0x48, 0x14, 0x48, 0x68, 0x14, 0x21, 0x14, 0x43, 0x01, 0x41, 0x65, + 0xe2, 0x16, 0xf8, 0x31, 0x92, 0xde, 0xc0, 0x00, 0xa0, 0x03, 0x50, 0x05, + 0x60, 0x05, 0x00, 0x81, 0xfa, 0x00, 0x08, 0xc0, 0x76, 0x90, 0x03, 0x00, + 0x07, 0xa4, 0x30, 0xd0, 0x10, 0x80, 0x3b, 0x00, 0x7e, 0x01, 0x89, 0x30, + 0x07, 0x5c, 0x98, 0x03, 0xb2, 0x61, 0x61, 0xa8, 0x01, 0x39, 0x0c, 0x07, + 0x60, 0x3b, 0xe8, 0x0c, 0x70, 0x18, 0x17, 0xcb, 0xc4, 0xcc, 0x02, 0x7d, + 0xb9, 0x68, 0x42, 0xf2, 0x06, 0xb6, 0x1a, 0xab, 0x28, 0x20, 0x02, 0x30, + 0x06, 0x20, 0x0a, 0x01, 0x00, 0x10, 0x80, 0x1f, 0x00, 0x5c, 0x01, 0x89, + 0x60, 0x0a, 0x40, 0x1d, 0x00, 0x6a, 0x51, 0x0d, 0x09, 0xe5, 0x8d, 0x01, + 0x88, 0x0e, 0x80, 0x4c, 0x82, 0x17, 0xe9, 0x02, 0xa8, 0x64, 0x97, 0xc8, + 0x64, 0x3c, 0xa4, 0x93, 0x59, 0x79, 0x0b, 0xcc, 0x6c, 0x80, 0x1f, 0x72, + 0xc0, 0x0c, 0x00, 0xa8, 0x01, 0x28, 0x0e, 0x80, 0x0a, 0xdd, 0x21, 0xa0, + 0x0f, 0xd0, 0x00, 0xa8, 0x10, 0x40, 0x73, 0x27, 0x00, 0x09, 0x4a, 0x26, + 0x23, 0x70, 0x0b, 0xc6, 0x92, 0x8b, 0x01, 0xd0, 0x05, 0x61, 0x80, 0x17, + 0x62, 0xfa, 0x40, 0xa0, 0x04, 0xfc, 0x84, 0x03, 0x7e, 0x8c, 0x49, 0xe5, + 0x2a, 0x40, 0x17, 0xa1, 0x20, 0x19, 0x00, 0x2c, 0x00, 0x76, 0x00, 0xc4, + 0x01, 0xf1, 0x2c, 0xa0, 0x0c, 0x00, 0xae, 0x00, 0x6d, 0xc0, 0x4c, 0x58, + 0x09, 0x86, 0x96, 0x4d, 0x46, 0x03, 0x20, 0x09, 0x38, 0x02, 0xf6, 0x01, + 0xd1, 0x30, 0x68, 0x0c, 0x40, 0xa2, 0x03, 0x4b, 0x40, 0x03, 0x30, 0x03, + 0xb1, 0xa1, 0x85, 0x60, 0xde, 0x4d, 0x02, 0xbb, 0xb8, 0x05, 0xa1, 0x30 +}; +unsigned int kMpegPacingFixtureM2v_len = 2436; diff --git a/ps2xTest/src/ps2_mpeg_pacing_tests.cpp b/ps2xTest/src/ps2_mpeg_pacing_tests.cpp new file mode 100644 index 000000000..cf30923b0 --- /dev/null +++ b/ps2xTest/src/ps2_mpeg_pacing_tests.cpp @@ -0,0 +1,405 @@ +#include "MiniTest.h" +#include "ps2_runtime.h" +#include "ps2_runtime_macros.h" +#include "ps2_syscalls.h" +#include "Stubs/MPEG.h" +#include "Stubs/MPEG_internal.h" +#include "mpeg_pacing_fixture_m2v.h" + +#include +#include +#include +#include +#include +#include + +using namespace ps2_syscalls; + +namespace +{ + struct TestEnv + { + std::vector rdram; + PS2Runtime runtime; + + TestEnv() : rdram(PS2_RAM_SIZE, 0u) + { + } + }; + + template + bool waitUntil(Predicate pred, std::chrono::milliseconds timeout) + { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (std::chrono::steady_clock::now() < deadline) + { + if (pred()) + { + return true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return pred(); + } + + void cleanupRuntime(TestEnv &env) + { + env.runtime.requestStop(); + notifyRuntimeStop(); + } + + void setRegU32(R5900Context &ctx, int reg, uint32_t value) + { + SET_GPR_U32(&ctx, reg, value); + } + + const size_t kMpegPacingFixtureM2vLen = kMpegPacingFixtureM2v_len; +} + +void register_ps2_mpeg_pacing_tests() +{ + MiniTest::Case("PS2MpegPacing", [](TestCase &tc) + { + tc.Run("frame due-tick is the exact, drift-free frame-rate rational", [](TestCase &t) + { + using ps2_stubs::mpegFrameDueTick; + const uint64_t base = 1000u; + const uint64_t hz = 60u; + + // N=0 is due immediately at base. + t.Equals(mpegFrameDueTick(base, 0u, hz, 30000, 1001), base, "frame 0 due at base"); + + // Exact closed-form for several N at NTSC 29.97 (30000/1001). + for (uint64_t n : {1u, 2u, 30u, 100u, 4321u}) + { + const uint64_t expected = base + (n * hz * 1001ull) / 30000ull; + t.Equals(mpegFrameDueTick(base, n, hz, 30000, 1001), expected, + "due(N) must equal the closed-form rational"); + } + + // Drift-free over a long movie: computed fresh from N, never accumulated. Pin + // monotonic non-decreasing AND equality with the closed form at 10000 frames. + uint64_t prev = base; + for (uint64_t n = 1u; n <= 10000u; ++n) + { + const uint64_t due = mpegFrameDueTick(base, n, hz, 30000, 1001); + t.IsTrue(due >= prev, "due ticks must be monotonic"); + prev = due; + } + t.Equals(mpegFrameDueTick(base, 10000u, hz, 30000, 1001), + base + (10000ull * hz * 1001ull) / 30000ull, + "due(10000) exact -- no accumulator drift"); + + // Fallback / guard: unknown or degenerate frame rate returns base (no div-by-zero, + // no pacing) -- the inert path for the synthetic/stub picture path. + t.Equals(mpegFrameDueTick(base, 5u, hz, 0, 0), base, "unknown fps => no pacing"); + t.Equals(mpegFrameDueTick(base, 5u, hz, 30000, 0), base, "bad den => no pacing"); + }); + + tc.Run("frame-rate sanity clamp accepts real rates and rejects garbage", [](TestCase &t) + { + using ps2_stubs::mpegFrameRateIsSane; + // Real movie rates accepted. + t.IsTrue(mpegFrameRateIsSane(30000, 1001), "29.97 accepted"); + t.IsTrue(mpegFrameRateIsSane(24, 1), "24 accepted"); + t.IsTrue(mpegFrameRateIsSane(25, 1), "25 (PAL) accepted"); + t.IsTrue(mpegFrameRateIsSane(30, 1), "30 accepted"); + t.IsTrue(mpegFrameRateIsSane(60, 1), "60 accepted"); + // Garbage / out-of-range rejected -> caller keeps the NTSC fallback. + t.IsFalse(mpegFrameRateIsSane(0, 0), "0/0 rejected"); + t.IsFalse(mpegFrameRateIsSane(30000, 0),"bad den rejected"); + t.IsFalse(mpegFrameRateIsSane(-30, 1), "negative rejected"); + t.IsFalse(mpegFrameRateIsSane(5, 1), "5fps (<10) rejected"); + t.IsFalse(mpegFrameRateIsSane(200, 1), "200fps (>120) rejected"); + t.IsFalse(mpegFrameRateIsSane(1, 1000), "0.001fps rejected"); + }); + + tc.Run("park blocks until the due tick, then returns", [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + // Prime the worker and read a current tick. + const uint64_t start = ps2_syscalls::WaitForNextVSyncTick(env.rdram.data(), &env.runtime); + const uint64_t due = start + 3u; // ~50 ms out; below the 8-tick cap + + std::atomic done{false}; + const auto t0 = std::chrono::steady_clock::now(); + std::thread th([&]() { + ps2_stubs::mpegParkUntilDueTick(env.rdram.data(), &env.runtime, due); + done.store(true, std::memory_order_release); + }); + + // Must actually block (not return instantly like the unpaced bug would). + const bool finished = waitUntil([&]() { return done.load(std::memory_order_acquire); }, + std::chrono::milliseconds(2000)); + th.join(); + const auto elapsed = std::chrono::steady_clock::now() - t0; + t.IsTrue(finished, "park must return once the due tick is reached"); + // This wall-clock window rides the shared vsync worker: it carries slack for the + // worker start-race, not correctness -- the tick-count assertion below (tick reached + // the due tick) is the real discriminator. + t.IsTrue(elapsed >= std::chrono::milliseconds(25), + "park must wait ~3 vblanks, not return immediately"); + t.IsTrue(ps2_syscalls::GetCurrentVSyncTick() >= due, "tick reached due before return"); + + cleanupRuntime(env); + }); + + tc.Run("park releases promptly on runtime stop", [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + const uint64_t start = ps2_syscalls::WaitForNextVSyncTick(env.rdram.data(), &env.runtime); + const uint64_t unreachable = start + 1000000u; // never reached in test time + + std::atomic done{false}; + std::thread th([&]() { + ps2_stubs::mpegParkUntilDueTick(env.rdram.data(), &env.runtime, unreachable); + done.store(true, std::memory_order_release); + }); + + env.runtime.requestStop(); // latches isStopRequested() + notifyRuntimeStop(); // wakes the vsync worker + const bool released = waitUntil([&]() { return done.load(std::memory_order_acquire); }, + std::chrono::milliseconds(2000)); + th.join(); + // Without the isStopRequested() guard in mpegParkUntilDueTick, this hangs forever + // (tick never reaches `unreachable`, WaitForNextVSyncTick returns immediately each + // iteration) and the test times out -- which is exactly the guard we are pinning. + // With the per-call cap present, the discriminator still holds: after requestStop() + // the worker stops advancing the tick, so the frozen tick stays below the capped + // due tick and -- absent the isStopRequested() guard -- the loop would spin forever. + t.IsTrue(released, "park must exit promptly when the runtime is stopped"); + + cleanupRuntime(env); + }); + + tc.Run("park is hard-capped and cannot stall on a pathological due tick", [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + const uint64_t start = ps2_syscalls::WaitForNextVSyncTick(env.rdram.data(), &env.runtime); + const uint64_t absurd = start + 1000000u; // far beyond the cap; unreachable in test time + + std::atomic done{false}; + std::thread th([&]() { + ps2_stubs::mpegParkUntilDueTick(env.rdram.data(), &env.runtime, absurd); + done.store(true, std::memory_order_release); + }); + // Worker keeps running; the cap (8 vblanks) must bound the park despite the absurd due. + const bool finished = waitUntil([&]() { return done.load(std::memory_order_acquire); }, + std::chrono::milliseconds(2000)); + th.join(); + t.IsTrue(finished, "an uncapped park would never return here; the cap must bound it"); + // Returned after ~the cap, not the absurd target. + t.IsTrue(ps2_syscalls::GetCurrentVSyncTick() < start + 64u, + "park must return within a few vblanks of the cap, not chase the absurd due tick"); + cleanupRuntime(env); + }); + + tc.Run("pacing park holds no global MPEG mutex (concurrent stub calls make progress)", + [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + ps2_stubs::resetMpegStubState(); + const uint64_t start = ps2_syscalls::WaitForNextVSyncTick(env.rdram.data(), &env.runtime); + const uint64_t due = start + 5u; // ~83ms; below the cap so the park runs its full length + + std::atomic parkDone{false}; + std::thread th([&]() { + ps2_stubs::mpegParkUntilDueTick(env.rdram.data(), &env.runtime, due); + parkDone.store(true, std::memory_order_release); + }); + + int resets = 0; + while (!parkDone.load(std::memory_order_acquire)) + { + ps2_stubs::resetMpegStubState(); // acquires g_mpeg_stub_mutex each call + ++resets; + std::this_thread::yield(); + } + th.join(); + // With the park correctly outside the lock, thousands of resets complete during it; if + // the park held g_mpeg_stub_mutex, each reset would block until the park ended and this + // count would be ~0-1. + t.IsTrue(resets >= 3, "stub calls that take g_mpeg_stub_mutex must progress during a park"); + cleanupRuntime(env); + }); + + tc.Run("sceMpegGetPicture meters real frame delivery to the stream's encoded rate", + [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + ps2_stubs::resetMpegStubState(); + uint8_t *rdram = env.rdram.data(); + const uint32_t mpegAddr = 0x00100000u; + const uint32_t esAddr = 0x00200000u; + const uint32_t imageAddr = 0x00300000u; + + (void)ps2_syscalls::WaitForNextVSyncTick(rdram, &env.runtime); // prime the worker + + // Locate MPEG-2 picture start codes (00 00 01 00) to split the ES per access unit. + const uint8_t *es = kMpegPacingFixtureM2v; + const size_t esLen = kMpegPacingFixtureM2vLen; + std::vector pic; + for (size_t i = 0; i + 3 < esLen; ++i) + if (es[i] == 0 && es[i+1] == 0 && es[i+2] == 1 && es[i+3] == 0x00) pic.push_back(i); + t.IsTrue(pic.size() >= 5, "fixture must contain several coded pictures"); + + auto feed = [&](size_t from, size_t to) { + const size_t n = to - from; + std::memcpy(rdram + esAddr, es + from, n); + R5900Context ctx{}; + setRegU32(ctx, 4, mpegAddr); + setRegU32(ctx, 5, esAddr); + setRegU32(ctx, 6, static_cast(n)); + ps2_stubs::sceMpegAddBs(rdram, &ctx, &env.runtime); // decodes synchronously + }; + feed(0, pic[1]); // headers + picture 0 + for (size_t k = 1; k + 1 < pic.size(); ++k) feed(pic[k], pic[k+1]); + feed(pic.back(), esLen); // last picture + + auto getPic = [&]() { + R5900Context ctx{}; + setRegU32(ctx, 4, mpegAddr); + setRegU32(ctx, 5, imageAddr); + setRegU32(ctx, 6, 0u); + ps2_stubs::sceMpegGetPicture(rdram, &ctx, &env.runtime); + }; + + const int n = static_cast(pic.size()); // frames decoded == pictures fed + std::vector tickAt; + tickAt.reserve(n); + for (int f = 0; f < n; ++f) { getPic(); tickAt.push_back(ps2_syscalls::GetCurrentVSyncTick()); } + + // Deterministic latch pin (jitter-free): after real frames are drained the handle + // must have latched the fixture's true 25fps from the decoder, NOT the 30000/1001 + // fallback. This -- not the wall-clock span below -- is what discriminates a working + // getFrameRate()/sane-clamp from a broken one: the span floor for 25fps (12 ticks) and + // the fallback (10 ticks) differ by only ~2 ticks, which vsync-worker jitter swamps, so + // span alone cannot separate them. See the VALIDATION latch revert-check. + int latchNum = 0, latchDen = 0; + const bool latched = ps2_stubs::mpegLatchedFrameRate(mpegAddr, latchNum, latchDen); + t.IsTrue(latched, + "stream frame rate must latch from the decoder once real frames are served"); + t.Equals(latchNum, 25, "latched frame-rate numerator must be the fixture's real 25fps"); + t.Equals(latchDen, 1, "latched frame-rate denominator must be 1 (25/1, not the fallback)"); + + const uint64_t span = tickAt.back() - tickAt.front(); + // End-to-end pacing pin (NOT the latch discriminator -- that is the direct + // mpegLatchedFrameRate assertion above). Lower bound: paced delivery must span the + // encoded rate's ideal (floor((n-1)*60/25) = 12 for 6 frames), computed from the same + // drift-free due-tick math the runtime uses -- so a wholesale UNPACED regression, which + // drains all frames within a tick or two, fails it. It does not separate 25fps from the + // 30000/1001 fallback: their span floors (12 vs 10) differ by less than the vsync-worker + // jitter, so the latch is pinned directly above, not here. + // + // Deliberately no upper ceiling. Over-throttling shows up only as extra elapsed vsync + // ticks, and the park rides the real worker, so any upper bound on the measured span is + // wall-clock -- it would be the suite's one non-deterministic assertion. The only + // ceiling derivable purely from pinned constants is + // (n-1)*kMpegPacingMaxParkVsyncTicks = 40 for n=6, which sits ABOVE both a legitimate + // span (~12) and the gross-over-throttle failure it would need to catch (every gap + // parking to the cap, span ~= 40): a constant-derived ceiling is necessarily >= the + // worst buggy span and so discriminates nothing. The rate is instead pinned + // deterministically by the latch above (a too-slow rate fails it), and the schedule + // math and single-park bound by the standalone due-tick and per-call-cap cases -- so + // every assertion here stays deterministic. + const uint64_t expected = + ps2_stubs::mpegFrameDueTick(0u, static_cast(n - 1), 60u, 25, 1); + t.IsTrue(span + 1 >= expected, + "paced delivery must span the encoded rate (~12 vblanks), not drain instantly " + "(the unpaced bug)"); + + // No-frame path is inert: the queue is now empty; end the stream so further GetPicture + // calls take the no-frame/blank path, and assert they are NOT paced. + ps2_stubs::notifyMpegCdStreamEof(); + const uint64_t t0 = ps2_syscalls::GetCurrentVSyncTick(); + for (int f = 0; f < 4; ++f) getPic(); + t.IsTrue(ps2_syscalls::GetCurrentVSyncTick() - t0 <= 2u, + "no-frame path must not be paced (no per-call park once frames are exhausted/ended)"); + + cleanupRuntime(env); + }); + + tc.Run("duplicate-last-frame path is inert (re-served without a pacing park)", + [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + ps2_stubs::resetMpegStubState(); + uint8_t *rdram = env.rdram.data(); + const uint32_t mpegAddr = 0x00100000u; + const uint32_t esAddr = 0x00200000u; + const uint32_t imageAddr = 0x00300000u; + + (void)ps2_syscalls::WaitForNextVSyncTick(rdram, &env.runtime); // prime the worker + + // Feed the whole fixture and drain every real decoded frame -- exactly like the + // encoded-rate case -- WITHOUT flagging CD-stream EOF. The handle then holds the + // precise duplicate-branch precondition: empty queue, sawInput, hasLastFrame, + // picturesServed > 0, stream not ended, and no CD-stream EOF seen. + const uint8_t *es = kMpegPacingFixtureM2v; + const size_t esLen = kMpegPacingFixtureM2vLen; + std::vector pic; + for (size_t i = 0; i + 3 < esLen; ++i) + if (es[i] == 0 && es[i+1] == 0 && es[i+2] == 1 && es[i+3] == 0x00) pic.push_back(i); + t.IsTrue(pic.size() >= 5, "fixture must contain several coded pictures"); + + auto feed = [&](size_t from, size_t to) { + const size_t nbytes = to - from; + std::memcpy(rdram + esAddr, es + from, nbytes); + R5900Context ctx{}; + setRegU32(ctx, 4, mpegAddr); + setRegU32(ctx, 5, esAddr); + setRegU32(ctx, 6, static_cast(nbytes)); + ps2_stubs::sceMpegAddBs(rdram, &ctx, &env.runtime); + }; + feed(0, pic[1]); + for (size_t k = 1; k + 1 < pic.size(); ++k) feed(pic[k], pic[k+1]); + feed(pic.back(), esLen); + + auto getPic = [&]() { + R5900Context ctx{}; + setRegU32(ctx, 4, mpegAddr); + setRegU32(ctx, 5, imageAddr); + setRegU32(ctx, 6, 0u); + ps2_stubs::sceMpegGetPicture(rdram, &ctx, &env.runtime); + }; + + const int n = static_cast(pic.size()); // real frames == pictures fed + for (int f = 0; f < n; ++f) getPic(); // drain all real frames (each paced) + + // Queue empty, stream neither ended nor EOF-flagged: the next GetPicture must fall + // through the bounded no-frame wait (~64 ms) and re-serve the last frame -- the + // duplicate-last-frame branch, which must NOT arm pacing. + std::memset(rdram + imageAddr, 0, 64u); // so a written frame is observable + const uint64_t before = ps2_syscalls::GetCurrentVSyncTick(); + getPic(); + const uint64_t after = ps2_syscalls::GetCurrentVSyncTick(); + + // Confirm the duplicate branch ran (not the blank/no-frame branch): it re-serves the + // last frame image (haveFrame == true), whereas the no-frame branch with frameCount>0 + // writes nothing; and it re-serves at frame index == frames already served. + uint32_t frameField = 0u; + std::memcpy(&frameField, rdram + mpegAddr + 0x08u, sizeof(frameField)); + t.Equals(frameField, static_cast(n), + "duplicate branch re-serves at the next frame index (== frames served)"); + bool imageWritten = false; + for (size_t i = 0; i < 64u; ++i) + if (rdram[imageAddr + i] != 0u) { imageWritten = true; break; } + t.IsTrue(imageWritten, "duplicate branch must re-serve the last frame image"); + + // The pin: the duplicate call is bounded by the no-frame wait (~4 vblanks) and is NOT + // extended by a pacing park. A forced park on this branch (see the VALIDATION + // revert-check) runs to the per-call cap (8 vblanks) on top of the wait -- ~12 ticks + // -- so an 8-tick ceiling cleanly separates the inert path (~4) from a paced one. + t.IsTrue(after - before <= 8u, + "duplicate-last-frame path must not add a pacing park"); + + cleanupRuntime(env); + }); + }); +}