#include "libs/avPlayer.cpp" #include "common/archive.h" #include "ArchiveTestFixture.h" #include #include #include #include namespace { std::filesystem::path content_root; } namespace Libs::LibKernel { struct PthreadPrivate { std::thread thread; }; int KYTY_SYSV_ABI PthreadCreate(Pthread* thread, const PthreadAttr*, pthread_entry_func_t entry, void* arg, const char*) { *thread = new PthreadPrivate {std::thread([=] { entry(arg); })}; return 0; } int KYTY_SYSV_ABI PthreadJoin(Pthread thread, void**) { thread->thread.join(); delete thread; return 0; } namespace FileSystem { std::filesystem::path GetRealFilename(const std::string& path) { return path.starts_with("/app0/") ? content_root / path.substr(6) : std::filesystem::path {}; } } } namespace { using namespace Libs::Audio::AvPlayer; void Check(bool condition, const char* message) { if (!condition) { std::fprintf(stderr, "AvPlayerFileTests: %s\n", message); std::abort(); } } void CheckStream(FileStreamer& stream, const std::vector& data) { auto* io = stream.Context(); Check(io->seek(io->opaque, 0, AVSEEK_SIZE) == static_cast(data.size()), "query size"); Check(io->seek(io->opaque, 65531, SEEK_SET | AVSEEK_FORCE) == 65531, "seek across block boundary"); std::array bytes {}; Check(io->read_packet(io->opaque, bytes.data(), bytes.size()) == bytes.size() && std::equal(bytes.begin(), bytes.end(), data.begin() + 65531), "read across block boundary"); Check(io->seek(io->opaque, -16, SEEK_CUR) == 65547, "seek relative to cursor"); Check(io->seek(io->opaque, INT64_MIN, SEEK_CUR) < 0 && io->seek(io->opaque, INT64_MAX, SEEK_END) < 0 && io->seek(io->opaque, 0, 12345) < 0, "reject invalid and overflowing seeks"); Check(io->seek(io->opaque, 0, SEEK_CUR) == 65547, "rejected seeks preserve the cursor"); Check(io->seek(io->opaque, -7, SEEK_END) == static_cast(data.size() - 7), "seek near end"); Check(io->read_packet(io->opaque, bytes.data(), bytes.size()) == 7 && std::equal(bytes.begin(), bytes.begin() + 7, data.end() - 7), "short read at end"); Check(io->read_packet(io->opaque, bytes.data(), bytes.size()) == AVERROR_EOF, "report EOF"); Check(io->seek(io->opaque, 11, SEEK_END) == static_cast(data.size() + 11) && io->read_packet(io->opaque, bytes.data(), bytes.size()) == AVERROR_EOF, "read beyond end"); Check(io->seek(io->opaque, 0, SEEK_SET) == 0, "rewind"); std::vector buffered(data.size()); Check(avio_read(io, buffered.data(), static_cast(buffered.size())) == buffered.size() && buffered == data, "FFmpeg buffered reads preserve content"); Check(avio_seek(io, 123, SEEK_SET) == 123 && avio_read(io, bytes.data(), bytes.size()) == bytes.size() && std::equal(bytes.begin(), bytes.end(), data.begin() + 123), "FFmpeg seeks reset buffered reads"); } struct Callbacks { const std::vector& data; unsigned opens = 0; unsigned closes = 0; bool premature_eof = false; }; int KYTY_SYSV_ABI Open(void* object, const char*) { ++static_cast(object)->opens; return 0; } int KYTY_SYSV_ABI Close(void* object) { ++static_cast(object)->closes; return 0; } int KYTY_SYSV_ABI Read(void* object, uint8_t* buffer, uint64_t offset, uint32_t size) { auto& state = *static_cast(object); if (state.premature_eof) { return 0; } const auto bytes = std::min(size, state.data.size() - offset); std::memcpy(buffer, state.data.data() + offset, bytes); return static_cast(bytes); } uint64_t KYTY_SYSV_ABI Size(void* object) { return static_cast(object)->data.size(); } struct VideoMemory { unsigned allocations = 0; unsigned deallocations = 0; void* protected_buffer = nullptr; bool released_protected = false; }; void* KYTY_SYSV_ABI Allocate(void* object, uint32_t alignment, uint32_t size) { Check(alignment == 0x100, "video allocation alignment"); ++static_cast(object)->allocations; return ::operator new(size, std::align_val_t(alignment), std::nothrow); } void KYTY_SYSV_ABI Deallocate(void* object, void* data) { auto& memory = *static_cast(object); ++memory.deallocations; memory.released_protected |= data == memory.protected_buffer; ::operator delete(data, std::align_val_t(0x100)); } void CheckVideoReadAhead(const std::filesystem::path& root) { // A 16x16 red MPEG-4 intra frame at 30 Hz; repeat it with increasing VOP times. std::array frame { 0x00, 0x00, 0x01, 0xb0, 0x01, 0x00, 0x00, 0x01, 0xb5, 0x89, 0x13, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x20, 0x00, 0xc4, 0x8d, 0x88, 0x00, 0xf5, 0x00, 0x84, 0x02, 0x14, 0x63, 0x00, 0x00, 0x01, 0xb3, 0x00, 0x10, 0x07, 0x00, 0x00, 0x01, 0xb6, 0x10, 0x60, 0x51, 0x85, 0x06, 0xd8, 0x2c, 0x81, 0xe0}; { Common::File output; Check(output.Create(root / "movie.m4v"), "create video fixture"); for (unsigned i = 0; i < 24; ++i) { frame[41] = static_cast(0x10 + i / 2); frame[42] = i % 2 == 0 ? 0x60 : 0xe0; output.Write(frame.data(), frame.size()); } } VideoMemory memory; const AvPlayerMemAllocator allocator {&memory, Allocate, Deallocate, Allocate, Deallocate}; Source source(allocator, {}, {}, 6, false, 0); Check(source.Init("/app0/movie.m4v", AvPlayerSourceFileMp4) == 0, "open video fixture"); source.SetSync(1); Check(source.Start() == 0, "start video workers"); AvPlayerFrameInfoEx info {}; for (unsigned i = 0; i < 8; ++i) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); while (!source.Video(&info)) { Check(std::chrono::steady_clock::now() < deadline, "decode steady-state video"); std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } // Let the producer refill, then model the game's immediate late-frame retries. std::this_thread::sleep_for(std::chrono::milliseconds(50)); for (unsigned i = 0; i < 5; ++i) { const auto previous = info.time_stamp; Check(source.Video(&info), "six output buffers keep five video frames ready"); Check(info.time_stamp > previous, "video retries return successive frames"); } Check(source.Pause() == 0, "pause video"); memory.protected_buffer = info.data; std::array current {}; std::memcpy(current.data(), info.data, current.size()); Check(!source.Video(&info) && info.data == memory.protected_buffer, "failed video get preserves output"); Check(source.Start() == 0, "restart video with retained current output"); Check(!memory.released_protected && std::memcmp(current.data(), memory.protected_buffer, current.size()) == 0, "restart preserves output until the next successful video get"); Check(source.Stop() == 0 && memory.allocations == memory.deallocations, "stop releases all video buffers"); } } int main() { const auto root = std::filesystem::temp_directory_path() / ("kyty_avio_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Check(std::filesystem::create_directories(root), "create fixture directory"); std::vector payload(128 * 1024 + 37); for (size_t i = 0; i < payload.size(); ++i) { payload[i] = static_cast(i * 37); } { Common::File output; Check(output.Create(root / "movie.bin"), "create native fixture"); output.Write(payload.data(), static_cast(payload.size())); } Check(ArchiveTests::CreateArchive(root / "game.zar", payload), "create archive fixture"); content_root = root; { FileStreamer native({}); Check(native.Init("/app0/movie.bin"), "open native stream"); CheckStream(native, payload); } content_root = Common::MakeArchivePath(root / "game.zar"); { FileStreamer archive({}); Check(archive.Init("/app0/assets/subdir/data.bin"), "open archived stream"); CheckStream(archive, payload); } Callbacks callbacks {payload}; const AvPlayerFileReplacement replacement {&callbacks, Open, Close, Read, Size}; { FileStreamer callback(replacement); Check(callback.Init("callback"), "open guest callback stream"); CheckStream(callback, payload); callbacks.premature_eof = true; auto* io = callback.Context(); Check(io->seek(io->opaque, 0, SEEK_SET) == 0, "rewind callback"); uint8_t byte = 0; Check(io->read_packet(io->opaque, &byte, 1) == AVERROR(EIO), "reject premature EOF"); } Check(callbacks.opens == 1 && callbacks.closes == 1, "close callback exactly once"); { FileStreamer missing({}); Check(!missing.Init("/unmounted/movie.bin"), "reject unmapped media"); } content_root = root; CheckVideoReadAhead(root); std::error_code error; std::filesystem::remove_all(root, error); Check(!error, "release all archive handles"); std::puts("AvPlayerFileTests: all cases passed"); }