From 5c4c1f9e8843f506fe54de938a96481f4c7d70ec Mon Sep 17 00:00:00 2001 From: Shane Michael Mathews Date: Mon, 20 Jul 2026 20:02:26 -0400 Subject: [PATCH] fix(mpeg): pace sceMpegGetPicture to the stream's frame rate With the FFmpeg-backed MpegFfmpegDecoder, sceMpegGetPicture pops and returns the next decoded frame as fast as the guest asks for it. Decode is driven synchronously by the guest's demux/AddBs calls and host file I/O is instant, so a guest that feeds the elementary stream in a burst decodes far ahead of real time: the standard vsync-gated PS2 FMV pump (one GetPicture ~ one field of movie time) instead gets frames handed back near-instantly, so the movie runs at 2x+ or drains to a premature end-of-stream before its own present loop has shown more than the first frames. On hardware the rate is bounded by the disc-fed stream buffer; that throttle has no analogue here unless delivery is metered at the library boundary. Meter delivery at the sceMpegGetPicture seam without throttling the decoder (which stays as fast as possible, good for prefetch): the first time a handle serves a real decoded frame, capture a base vsync tick; latch the stream rate from AVCodecContext::framerate on the first valid, in-range read (re-reading on later frames until it is known, falling back to 30000/1001 meanwhile, and rejecting rates outside 10..120 fps as garbage (a plausibility gate, not a correctness one: an interlaced 59.94 field rate would pass, but PS2 FMVs are overwhelmingly 29.97/25)). Frame N is then due at base + floor(N * 60 * fpsDen / fpsNum), computed fresh from N every call so integer rounding never drifts across a long movie. While the current tick is before the due tick, park on the existing WaitForNextVSyncTick primitive. The meter is ahead-only and bounded: the park is a no-op unless the guest is running ahead of the movie's own timeline, so a normal or slow consumer is never throttled; and a single park is hard-capped at a few vblanks, so a pathological rate can never stall one GetPicture call. 60 is the emulator's fixed vsync-tick-worker rate (kVblankPeriod), not a display-refresh number, so a park's real duration is N * fpsDen / fpsNum seconds regardless of NTSC/PAL display mode -- PAL content is paced correctly by its own frame rate. kVblankPeriod is exported from Interrupt.h and a static_assert in MPEG.cpp pins this rate to it, so retuning the vsync worker can never silently miscalibrate pacing. The park holds no lock: the pacing slot is reserved under g_mpeg_stub_mutex and the mutex is released before the wait, so a multi-vblank park never blocks stream-feed callbacks or another handle's GetPicture. The park loop also re-checks runtime->isStopRequested() each iteration and breaks out, so it cannot spin during shutdown once the tick counter stops advancing. Pacing is per-handle and inert for any path that never serves a real decoded frame -- the duplicate-last-frame and blank/no-frame paths and any title that does not use the sceMpeg FMV path see no behavioral change. Pacing math is split into mpegFrameDueTick()/mpegFrameRateIsSane() and the park into mpegParkUntilDueTick() (internal header, not the guest syscall surface), pinned by a new regression suite: the due-tick sequence equals the exact drift-free rational; the rate clamp accepts real rates and rejects garbage; the park blocks the expected vblanks, releases promptly on stop, and is hard-capped against a pathological due tick; concurrent stub calls make progress during a park (the park holds no global mutex); and a real embedded MPEG-2 stream fed through sceMpegAddBs and drained via sceMpegGetPicture is delivered at the stream's encoded rate, with the duplicate-last-frame and no-frame paths both left unpaced. Full build clean; ps2x_tests 340/340. --- ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp | 180 ++++++++ .../src/lib/Kernel/Stubs/MPEG_internal.h | 33 ++ .../src/lib/Kernel/Syscalls/Interrupt.cpp | 1 - .../src/lib/Kernel/Syscalls/Interrupt.h | 7 + ps2xTest/CMakeLists.txt | 1 + ps2xTest/src/main.cpp | 2 + ps2xTest/src/mpeg_pacing_fixture_m2v.h | 223 ++++++++++ ps2xTest/src/ps2_mpeg_pacing_tests.cpp | 405 ++++++++++++++++++ 8 files changed, 851 insertions(+), 1 deletion(-) create mode 100644 ps2xRuntime/src/lib/Kernel/Stubs/MPEG_internal.h create mode 100644 ps2xTest/src/mpeg_pacing_fixture_m2v.h create mode 100644 ps2xTest/src/ps2_mpeg_pacing_tests.cpp 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); + }); + }); +}