diff --git a/.github/workflows/windows-vram.yml b/.github/workflows/windows-vram.yml index e1284bb..867c829 100644 --- a/.github/workflows/windows-vram.yml +++ b/.github/workflows/windows-vram.yml @@ -1,4 +1,4 @@ -name: Windows VRAM failure injection +name: Windows codec regression tests on: pull_request: @@ -80,6 +80,22 @@ jobs: "--overlay-ports=$env:GITHUB_WORKSPACE/target/ci-rustdesk/res/vcpkg" if ($LASTEXITCODE -ne 0) { throw 'Installing FFmpeg dependencies failed.' } + - name: Run RAM input lifetime tests + timeout-minutes: 15 + run: | + $vswhere = "${env:ProgramFiles(x86)}/Microsoft Visual Studio/Installer/vswhere.exe" + $vs = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if (-not $vs) { throw 'MSVC x64 tools are missing.' } + & "$vs/Common7/Tools/Launch-VsDevShell.ps1" -Arch amd64 -HostArch amd64 -SkipAutomaticLocation + $env:VCPKG_ROOT = "$env:GITHUB_WORKSPACE/target/ci-vcpkg" + $PSNativeCommandUseErrorActionPreference = $false + try { + & ./tests/ffmpeg_ram/input_lifetime.ps1 *>&1 | Tee-Object -FilePath target/ram-input-lifetime.log + } catch { + $_ | Out-String | Add-Content -LiteralPath target/ram-input-lifetime.log + throw + } + - name: Run failure injection with WARP timeout-minutes: 15 run: | @@ -106,6 +122,7 @@ jobs: name: windows-vram-logs path: | target/repeat-failures.log + target/ram-input-lifetime.log target/ci-vcpkg/buildtrees/**/*.log if-no-files-found: ignore retention-days: 7 diff --git a/cpp/common/platform/win/win.cpp b/cpp/common/platform/win/win.cpp index 2004166..c81c133 100644 --- a/cpp/common/platform/win/win.cpp +++ b/cpp/common/platform/win/win.cpp @@ -18,6 +18,10 @@ #define NUMVERTICES 6 +namespace { +using QueryClock = std::chrono::steady_clock; +} + typedef struct _VERTEX { DirectX::XMFLOAT3 Pos; DirectX::XMFLOAT2 TexCoord; @@ -380,22 +384,26 @@ void NativeDevice::BeginQuery() { context_->Begin(query_.Get()); } void NativeDevice::EndQuery() { context_->End(query_.Get()); } bool NativeDevice::Query() { - BOOL bResult = FALSE; + const auto deadline = + QueryClock::now() + std::chrono::milliseconds(DECODE_TIMEOUT_MS); int attempts = 0; - while (!bResult) { + // The deadline bounds polling, not a GetData call blocked inside the driver. + while (QueryClock::now() < deadline) { + BOOL bResult = FALSE; HRESULT hr = context_->GetData(query_.Get(), &bResult, sizeof(BOOL), 0); - if (SUCCEEDED(hr)) { - if (bResult) { - break; - } + if (FAILED(hr)) { + LOG_ERROR(std::string("GetData failed, hr = ") + std::to_string(hr)); + return false; } + if (hr == S_OK && bResult == TRUE) + return true; attempts++; + // Keep the existing short spin for queries that complete promptly. if (attempts > 100) Sleep(1); - if (attempts > 1000) - break; } - return bResult == TRUE; + LOG_ERROR("GPU query timed out"); + return false; } bool NativeDevice::Process(ID3D11Texture2D *in, ID3D11Texture2D *out, int width, diff --git a/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp b/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp index 700cab0..7c5ffb6 100644 --- a/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp +++ b/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp @@ -116,6 +116,8 @@ class FFmpegRamEncoder { int gpu_ = 0; RamEncodeCallback callback_ = NULL; int offset_[AV_NUM_DATA_POINTERS] = {0}; + int input_linesize_[AV_NUM_DATA_POINTERS] = {0}; + int input_length_ = 0; AVHWDeviceType hw_device_type_ = AV_HWDEVICE_TYPE_NONE; AVPixelFormat hw_pixfmt_ = AV_PIX_FMT_NONE; @@ -259,9 +261,11 @@ class FFmpegRamEncoder { if (ffmpeg_ram_get_linesize_offset_length(pixfmt_, width_, height_, align_, NULL, offset_, length) != 0) return false; + input_length_ = *length; for (int i = 0; i < AV_NUM_DATA_POINTERS; i++) { linesize[i] = frame_->linesize[i]; + input_linesize_[i] = frame_->linesize[i]; offset[i] = offset_[i]; } return true; @@ -270,11 +274,25 @@ class FFmpegRamEncoder { int encode(const uint8_t *data, int length, const void *obj, uint64_t ms) { int ret; - if ((ret = av_frame_make_writable(frame_)) != 0) { - LOG_ERROR(std::string("av_frame_make_writable failed, ret = ") + av_err2str(ret)); - return ret; + if (!data || length < input_length_) { + LOG_ERROR(std::string("encode: input data length error. length:") + + std::to_string(length) + ", required:" + + std::to_string(input_length_)); + return -1; + } + if (!av_frame_is_writable(frame_)) { + // All pixels will be overwritten; do not copy the previous frame. + // References retained by the encoder keep their existing buffers alive. + av_frame_unref(frame_); + frame_->format = pixfmt_; + frame_->width = width_; + frame_->height = height_; + if ((ret = av_frame_get_buffer(frame_, align_)) < 0) { + LOG_ERROR(std::string("av_frame_get_buffer failed, ret = ") + av_err2str(ret)); + return ret; + } } - if ((ret = fill_frame(frame_, (uint8_t *)data, length, offset_)) != 0) + if ((ret = fill_frame(frame_, data)) != 0) return ret; AVFrame *tmp_frame; if (hw_device_type_ != AV_HWDEVICE_TYPE_NONE) { @@ -366,41 +384,24 @@ class FFmpegRamEncoder { return encoded ? 0 : -1; } - int fill_frame(AVFrame *frame, uint8_t *data, int data_length, - const int *const offset) { + int fill_frame(AVFrame *frame, const uint8_t *data) { + const uint8_t *src[4] = {data, NULL, NULL, NULL}; switch (frame->format) { case AV_PIX_FMT_NV12: - if (data_length < - frame->height * (frame->linesize[0] + frame->linesize[1] / 2)) { - LOG_ERROR(std::string("fill_frame: NV12 data length error. data_length:") + - std::to_string(data_length) + - ", linesize[0]:" + std::to_string(frame->linesize[0]) + - ", linesize[1]:" + std::to_string(frame->linesize[1])); - return -1; - } - frame->data[0] = data; - frame->data[1] = data + offset[0]; + src[1] = data + offset_[0]; break; case AV_PIX_FMT_YUV420P: - if (data_length < - frame->height * (frame->linesize[0] + frame->linesize[1] / 2 + - frame->linesize[2] / 2)) { - LOG_ERROR(std::string("fill_frame: 420P data length error. data_length:") + - std::to_string(data_length) + - ", linesize[0]:" + std::to_string(frame->linesize[0]) + - ", linesize[1]:" + std::to_string(frame->linesize[1]) + - ", linesize[2]:" + std::to_string(frame->linesize[2])); - return -1; - } - frame->data[0] = data; - frame->data[1] = data + offset[0]; - frame->data[2] = data + offset[1]; + src[1] = data + offset_[0]; + src[2] = data + offset_[1]; break; default: LOG_ERROR(std::string("fill_frame: unsupported format, ") + std::to_string(frame->format)); return -1; } + // Encoder references must keep pixels alive after the caller releases data. + av_image_copy(frame->data, frame->linesize, src, input_linesize_, + (AVPixelFormat)frame->format, frame->width, frame->height); return 0; } }; @@ -471,4 +472,4 @@ extern "C" int ffmpeg_ram_set_bitrate(FFmpegRamEncoder *encoder, int kbs) { LOG_ERROR("ffmpeg_ram_set_bitrate: unknown exception"); } return -1; -} \ No newline at end of file +} diff --git a/cpp/ffmpeg_vram/ffmpeg_vram_decode.cpp b/cpp/ffmpeg_vram/ffmpeg_vram_decode.cpp index c93d964..07d5038 100644 --- a/cpp/ffmpeg_vram/ffmpeg_vram_decode.cpp +++ b/cpp/ffmpeg_vram/ffmpeg_vram_decode.cpp @@ -216,6 +216,7 @@ class FFmpegVRamDecoder { locked = true; if (!convert(frame_, callback, obj)) { LOG_ERROR(std::string("Failed to convert")); + decoded = false; goto _exit; } if (callback) @@ -258,7 +259,10 @@ class FFmpegVRamDecoder { return false; } native_->EndQuery(); - native_->Query(); + if (!native_->Query()) { + LOG_ERROR(std::string("Failed to query")); + return false; + } #else native_->BeginQuery(); diff --git a/tests/ffmpeg_io/README.md b/tests/ffmpeg_io/README.md new file mode 100644 index 0000000..b6bb83e --- /dev/null +++ b/tests/ffmpeg_io/README.md @@ -0,0 +1,13 @@ +Run `./tests/ffmpeg_io/run.ps1` from an x64 MSVC developer PowerShell with +`VCPKG_ROOT` set. The eight polling checks compile the exact production +`NativeDevice::Query()` body with scripted GetData results, a fake steady clock +and a 16 ms Sleep(1). They cover completion, errors and elapsed-time bounds. + +The three VRAM decoder checks use a real WARP NV12 texture, scripted FFmpeg I/O, +and injected conversion/completion results. They check the public FFI return +and callbacks on success, Query failure, and failure after a previous output. +These are deterministic comparisons, not measured GPU-hang experiments. + +Use `-Revision ` to run the same fixture against that revision's source. +Historical revisions with the bugs return nonzero. Per-case results are saved +under `target/codec-comparison//`. diff --git a/tests/ffmpeg_io/decode_query.cpp b/tests/ffmpeg_io/decode_query.cpp new file mode 100644 index 0000000..3f8df5b --- /dev/null +++ b/tests/ffmpeg_io/decode_query.cpp @@ -0,0 +1,125 @@ +extern "C" { +#include +} +#include "platform/win/win.h" +#include "util.h" +#include +#include +#include +#include + +#ifdef NDEBUG +#error These tests require assertions. +#endif + +namespace { +int sends, receives, marker, query_fail_at, query_calls; +std::deque receive_results; +std::vector output_markers; +ComPtr texture; + +int send_packet(AVCodecContext *, const AVPacket *packet) { + assert(packet->size == 4 && packet->data[0] == 42); + ++sends; + return 0; +} +int receive_frame(AVCodecContext *, AVFrame *frame) { + av_frame_unref(frame); + ++receives; + assert(!receive_results.empty()); + int ret = receive_results.front(); + receive_results.pop_front(); + if (ret < 0) + return ret; + marker = ret; + frame->width = frame->height = 16; + frame->format = AV_PIX_FMT_D3D11; + frame->data[0] = reinterpret_cast(texture.Get()); + return 0; +} +// Exercise the production decoder while injecting conversion/completion results. +class TestNativeDevice : public NativeDevice { +public: + bool EnsureTexture(int, int) { return true; } + int next() { return 0; } + void BeginQuery() {} + void EndQuery() {} + bool Query() { return ++query_calls != query_fail_at; } + bool Nv12ToBgra(int, int, ID3D11Texture2D *, ID3D11Texture2D *, int) { + return true; + } + ID3D11Texture2D *GetCurrentTexture() { return texture.Get(); } +}; +} // namespace +namespace util { +inline int64_t test_elapsed_ms(std::chrono::steady_clock::time_point) { return 0; } +} // namespace util +#define elapsed_ms test_elapsed_ms +#define avcodec_send_packet send_packet +#define avcodec_receive_frame receive_frame +#define NativeDevice TestNativeDevice +// Supplied by run.ps1, from either the working tree or an exact git revision. +#include "production.inc" +#undef NativeDevice +#undef elapsed_ms +#undef avcodec_send_packet +#undef avcodec_receive_frame + +extern "C" void hwcodec_log(int, const char *) {} +extern "C" void hwcodec_av_log_callback(int, const char *) {} + +namespace { +void output(void *value, const void *) { + assert(value == texture.Get()); + output_markers.push_back(marker); +} +int run_codec() { + uint8_t input[] = {42, 42, 42, 42}; + FFmpegVRamDecoder codec(nullptr, 0, H264); + codec.c_ = avcodec_alloc_context3(nullptr); + codec.frame_ = av_frame_alloc(); + codec.pkt_ = av_packet_alloc(); + codec.native_ = std::make_unique(); + int ret = ffmpeg_vram_decode(&codec, input, sizeof(input), output, nullptr); + codec.destroy(); + return ret; +} +} // namespace + +int main() { + ComPtr device; + assert(SUCCEEDED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, 0, + nullptr, 0, D3D11_SDK_VERSION, &device, + nullptr, nullptr))); + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = desc.Height = 16; + desc.MipLevels = desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_NV12; + desc.SampleDesc.Count = 1; + assert(SUCCEEDED(device->CreateTexture2D(&desc, nullptr, &texture))); + + int failures = 0; + auto check = [&](const char *name, int fail_at, int expected_ret, + int expected_receives, std::initializer_list expected) { + query_fail_at = fail_at; + sends = receives = query_calls = 0; + receive_results = fail_at == 1 ? std::deque{31, AVERROR(EAGAIN)} + : std::deque{30, 31, AVERROR(EAGAIN)}; + output_markers.clear(); + int ret = run_codec(); + bool pass = ret == expected_ret && sends == 1 && + receives == expected_receives && + query_calls == (fail_at ? fail_at : 2) && + output_markers == std::vector(expected); + std::printf("%s %s ret=%d sends=%d receives=%d queries=%d outputs=%zu\n", + pass ? "PASS" : "FAIL", name, ret, sends, receives, query_calls, + output_markers.size()); + if (!pass) + ++failures; + }; + check("query-success", 0, 0, 3, {30, 31}); + check("query-failure", 1, -1, 1, {}); + check("query-failure-after-output", 2, -1, 2, {30}); + std::printf("Failures: %d\n", failures); + return failures ? 1 : 0; +} diff --git a/tests/ffmpeg_io/query.cpp b/tests/ffmpeg_io/query.cpp new file mode 100644 index 0000000..c5d0351 --- /dev/null +++ b/tests/ffmpeg_io/query.cpp @@ -0,0 +1,95 @@ +#include "common.h" +#include +#include +#include +#include +#include +#include +#include + +namespace { +int elapsed, calls, sleeps, sleep_ms, get_data_ms; +struct QueryClock { + static std::chrono::milliseconds now() { + return std::chrono::milliseconds(elapsed); + } +}; +struct Reply { + HRESULT hr; + BOOL complete; +}; +std::deque replies; +Reply pending; +struct TestContext { + HRESULT GetData(void *, void *data, UINT size, UINT flags) { + assert(size == sizeof(BOOL) && flags == 0); + ++calls; + elapsed += get_data_ms; + Reply reply = pending; + if (!replies.empty()) { + reply = replies.front(); + replies.pop_front(); + } + *static_cast(data) = reply.complete; + return reply.hr; + } +}; +struct TestQuery { + void *Get() { return nullptr; } +}; +class NativeDevice { +public: + TestContext *context_; + TestQuery query_; + bool Query(); +}; +void test_sleep(DWORD ms) { + assert(ms == 1); + ++sleeps; + elapsed += sleep_ms; +} +} // namespace +#define Sleep test_sleep +#define LOG_ERROR(message) ((void)(message)) +// Exact production method body, extracted by run.ps1; only dependencies vary. +#include "query.inc" +#undef Sleep +#undef LOG_ERROR + +int main() { + int failures = 0; + auto check = [&](const char *name, std::initializer_list sequence, + Reply fallback, int call_ms, bool expected, + int expected_calls, int expected_ms, int expected_sleeps) { + elapsed = calls = sleeps = 0; + sleep_ms = 16; + get_data_ms = call_ms; + replies = sequence; + pending = fallback; + TestContext context; + NativeDevice device; + device.context_ = &context; + bool ret = device.Query(); + bool pass = ret == expected && calls == expected_calls && + elapsed == expected_ms && sleeps == expected_sleeps; + std::printf("%s %s ret=%d polls=%d elapsed_ms=%d sleeps=%d\n", + pass ? "PASS" : "FAIL", name, ret, calls, elapsed, sleeps); + if (!pass) + ++failures; + }; + check("immediate-completion", {{S_OK, TRUE}}, {S_FALSE, FALSE}, 0, true, 1, 0, + 0); + check("pending-then-complete", + {{S_FALSE, FALSE}, {S_FALSE, FALSE}, {S_OK, TRUE}}, {S_FALSE, FALSE}, 0, + true, 3, 0, 0); + check("device-removed", {}, {DXGI_ERROR_DEVICE_REMOVED, FALSE}, 0, false, 1, + 0, 0); + check("hard-error", {}, {E_FAIL, FALSE}, 0, false, 1, 0, 0); + check("pending-timeout", {}, {S_FALSE, FALSE}, 0, false, 163, 1008, 63); + check("false-event-timeout", {}, {S_OK, FALSE}, 0, false, 163, 1008, 63); + check("pending-data-is-not-completion", {}, {S_FALSE, TRUE}, 0, false, 163, + 1008, 63); + check("slow-get-data-timeout", {}, {S_FALSE, FALSE}, 200, false, 5, 1000, 0); + std::printf("Failures: %d\n", failures); + return failures ? 1 : 0; +} diff --git a/tests/ffmpeg_io/run.ps1 b/tests/ffmpeg_io/run.ps1 new file mode 100644 index 0000000..e7800d0 --- /dev/null +++ b/tests/ffmpeg_io/run.ps1 @@ -0,0 +1,41 @@ +# Run from an x64 MSVC developer PowerShell with VCPKG_ROOT set. +param([string]$Revision = '') +$ErrorActionPreference = 'Stop' +$repo = (Resolve-Path "$PSScriptRoot/../..").Path +Push-Location $repo +try { + $build = @(& cargo +stable build --release --features vram --message-format=json) + if ($LASTEXITCODE -ne 0) { throw 'Building hwcodec failed.' } + $native = $build | ForEach-Object { $_ | ConvertFrom-Json } | + Where-Object { $_.reason -eq 'build-script-executed' -and (Test-Path (Join-Path $_.out_dir 'hwcodec.lib')) } | + Select-Object -Last 1 + if (-not $native) { throw 'Native library not found.' } + $vcpkg = Join-Path $env:VCPKG_ROOT 'installed/x64-windows-static' + $label = if ($Revision) { (& git rev-parse --short $Revision).Trim() } else { 'working' } + if ($LASTEXITCODE -ne 0) { throw 'Invalid revision.' } + $outDir = (New-Item -ItemType Directory -Force "target/codec-comparison/$label").FullName + $failures = 0 + foreach ($test in @('query', 'decode_query')) { + $file = if ($test -eq 'query') { 'cpp/common/platform/win/win.cpp' } else { 'cpp/ffmpeg_vram/ffmpeg_vram_decode.cpp' } + $source = if ($Revision) { (& git show "${Revision}:$file") -join "`n" } else { Get-Content -Raw $file } + if ($Revision -and $LASTEXITCODE -ne 0) { throw "Cannot read $file at $Revision" } + if ($test -eq 'query') { + $method = [regex]::Match($source, '(?ms)^bool NativeDevice::Query\(\) \{.*?^\}') + if (-not $method.Success) { throw 'Cannot locate production Query method.' } + Set-Content -LiteralPath "$outDir/query.inc" -Value $method.Value -Encoding UTF8 + } else { + Set-Content -LiteralPath "$outDir/production.inc" -Value $source -Encoding UTF8 + } + $exe = "$outDir/$test.exe" + & cl.exe /nologo /EHs /O2 /std:c++17 /MT /DNOMINMAX ` + "/I$outDir" "/I$vcpkg/include" "/I$repo/cpp/common" "/I$repo/cpp/common/platform/win" ` + "/Fo$outDir/$test.obj" "/Fe$exe" "$PSScriptRoot/$test.cpp" ` + /link "/LIBPATH:$vcpkg/lib" (Join-Path $native.out_dir 'hwcodec.lib') ` + avcodec.lib avutil.lib avformat.lib libmfx.lib d3d11.lib dxgi.lib ` + user32.lib bcrypt.lib ole32.lib advapi32.lib gdi32.lib shell32.lib oleaut32.lib uuid.lib + if ($LASTEXITCODE -ne 0) { throw "Building $test failed." } + & $exe | Tee-Object -FilePath "$outDir/$test.log" + if ($LASTEXITCODE -ne 0) { ++$failures } + } + if ($failures) { throw "$failures test suites failed; see $outDir" } +} finally { Pop-Location } diff --git a/tests/ffmpeg_ram/README.md b/tests/ffmpeg_ram/README.md new file mode 100644 index 0000000..9210da9 --- /dev/null +++ b/tests/ffmpeg_ram/README.md @@ -0,0 +1,31 @@ +Run from an x64 MSVC developer PowerShell with `VCPKG_ROOT` pointing to an +existing static FFmpeg installation with QSV enabled: + +```powershell +./tests/ffmpeg_ram/input_lifetime.ps1 +``` + +The test compiles the production RAM encoder with codec opening and packet I/O +replaced. It uses real FFmpeg frame allocation, reference counting and copying; +no GPU is required and FFmpeg is not rebuilt. The fake encoder retains frame +references across calls to require fresh buffers. Allocation failure is injected +to verify that retained frames survive and the next submission can recover. + +For both NV12 and YUV420P, it verifies pixel content after overwriting and freeing +the caller's input, after subsequent inputs, and after encoder cleanup. It checks +alignments 0, 1 and 256, including odd chroma strides at 66x34. Encoder dimensions +are even, as required by the Rust API. +A separate case uses different input and destination strides. Empty, null and +truncated inputs must fail without allocating or submitting a frame. Writable +buffers must be reused when the encoder has released its references. + +The Windows workflow runs this suite alongside the VRAM failure-injection tests. + +These tests verify buffer ownership and layout, not hardware driver behavior. + +`./tests/ffmpeg_ram/compare_revisions.ps1 -Revision ` compiles that +revision's production RAM encoder against the same comparison fixture. Omit +`-Revision` to test the working tree. Old revisions intentionally fail the +checks for bugs they still contain; all observed values are logged under +`target/ram-comparison`. The fixture also checks 96 supported even-dimension +layouts against the planar size formula. diff --git a/tests/ffmpeg_ram/compare_revisions.cpp b/tests/ffmpeg_ram/compare_revisions.cpp new file mode 100644 index 0000000..2f6d634 --- /dev/null +++ b/tests/ffmpeg_ram/compare_revisions.cpp @@ -0,0 +1,198 @@ +extern "C" { +#include +} +#include +#include +#include +#include + +namespace { +std::vector retained; +int replacements, stale_copies; +bool fail_allocation, packet_ready; +int get_buffer(AVFrame *frame, int align) { + ++replacements; + if (fail_allocation) { + fail_allocation = false; + return AVERROR(ENOMEM); + } + return av_frame_get_buffer(frame, align); +} +int make_writable(AVFrame *frame) { + if (!av_frame_is_writable(frame)) { + ++replacements; + if (fail_allocation) { + fail_allocation = false; + return AVERROR(ENOMEM); + } + ++stale_copies; + } + return av_frame_make_writable(frame); +} +int open_codec(AVCodecContext *, const AVCodec *, AVDictionary **) { return 0; } +int send_frame(AVCodecContext *, const AVFrame *frame) { + retained.push_back(av_frame_clone(frame)); + assert(retained.back()); + packet_ready = true; + return 0; +} +int receive_packet(AVCodecContext *, AVPacket *packet) { + av_packet_unref(packet); + if (!packet_ready) + return AVERROR(EAGAIN); + packet_ready = false; + return av_new_packet(packet, 1); +} +} // namespace +#define avcodec_open2 open_codec +#define avcodec_send_frame send_frame +#define avcodec_receive_packet receive_packet +#define av_frame_get_buffer get_buffer +#define av_frame_make_writable make_writable +#include "production.inc" +#undef avcodec_open2 +#undef avcodec_send_frame +#undef avcodec_receive_packet +#undef av_frame_get_buffer +#undef av_frame_make_writable +extern "C" void hwcodec_log(int, const char *) {} +extern "C" void hwcodec_av_log_callback(int, const char *) {} + +namespace { +void output(const uint8_t *, int, int64_t, int, const void *) {} +void release_retained() { + for (auto &frame : retained) + av_frame_free(&frame); + retained.clear(); +} +int failures; +void result(const char *name, bool pass, int before, int after) { + std::printf("%s %s observed=%d expected=%d\n", pass ? "PASS" : "FAIL", name, + before, after); + if (!pass) + ++failures; +} +struct Fixture { + FFmpegRamEncoder encoder; + int stride[8] = {}, offset[8] = {}, length = 0; + std::vector input; + Fixture(AVPixelFormat format = AV_PIX_FMT_YUV420P, int align = 1) + : encoder("h264_qsv", nullptr, 66, 34, format, align, 30, 60, RC_CBR, + Quality_Default, 1000, -1, 1, -1, output) { + assert(encoder.init(stride, offset, &length)); + input.resize(length + 256, 0x55); + replacements = stale_copies = 0; + } + int encode(int declared = -1) { + return encoder.encode(input.data(), declared < 0 ? length : declared, + nullptr, 0); + } + ~Fixture() { + encoder.free_encoder(); + release_retained(); + } +}; +} // namespace +int main(int argc, char **) { + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + if (argc > 1) { + Fixture f; + int ret = f.encoder.encode(nullptr, f.length, nullptr, 0); + result("null-input", ret < 0 && retained.empty() && replacements == 0, ret, + -1); + return failures ? 1 : 0; + } + for (AVPixelFormat format : {AV_PIX_FMT_NV12, AV_PIX_FMT_YUV420P}) { + std::printf("format=%d\n", format); + { + Fixture f(format); + assert(f.encode() == 0); + std::fill(f.input.begin(), f.input.end(), 0); + int pixel = retained[0]->data[0][0]; + result("caller-overwrite", pixel == 0x55, pixel, 0x55); + } + { + Fixture f(format, 256); + av_frame_unref(f.encoder.frame_); + f.encoder.frame_->format = format; + f.encoder.frame_->width = 66; + f.encoder.frame_->height = 34; + assert(av_frame_get_buffer(f.encoder.frame_, 0) == 0); + assert(f.encoder.frame_->linesize[0] != f.stride[0]); + for (int y = 0; y < 34; ++y) + std::fill_n(f.input.data() + y * f.stride[0], 66, + static_cast(y + 1)); + assert(f.encode() == 0); + int pixel = retained[0]->data[0][retained[0]->linesize[0]]; + result("independent-strides", pixel == 2, pixel, 2); + } + { + Fixture f(format); + int ret = f.encode(f.length - 1); + result("one-byte-short", ret < 0 && retained.empty(), ret, -1); + } + { + Fixture f(format, 256); + assert(f.encode() == 0); + const int count = replacements; + int ret = f.encode(0); + result("reject-before-allocation", ret < 0 && replacements == count, + replacements - count, 0); + } + { + Fixture f(format, 256); + assert(f.encode() == 0); + assert(f.encode() == 0); + result("no-stale-frame-copy", stale_copies == 0, stale_copies, 0); + result("replacement-alignment", + f.encoder.frame_->linesize[0] == f.stride[0], + f.encoder.frame_->linesize[0], f.stride[0]); + } + { + Fixture f(format, 256); + assert(f.encode() == 0); + fail_allocation = true; + int ret = f.encode(); + bool intact = retained.size() == 1 && retained[0]->data[0][0] == 0x55; + int retry = f.encode(); + result("allocation-failure-retry", + ret == AVERROR(ENOMEM) && intact && retry == 0, ret, + AVERROR(ENOMEM)); + } + { + Fixture f(format); + auto *buffer = f.encoder.frame_->buf[0]->data; + assert(f.encode() == 0); + release_retained(); + const int count = replacements; + assert(f.encode() == 0); + bool reused = + f.encoder.frame_->buf[0]->data == buffer && replacements == count; + result("writable-buffer-reuse", reused, replacements - count, 0); + } + } + // Supported even dimensions must have the same layout with floor/ceil chroma + // height. + int layouts = 0; + for (auto format : {AV_PIX_FMT_NV12, AV_PIX_FMT_YUV420P}) + for (int width : {2, 66, 1920, 3840}) + for (int height : {2, 34, 1080, 2160}) + for (int align : {0, 1, 256}) { + int stride[8] = {}, offset[8] = {}, length = 0; + assert(ffmpeg_ram_get_linesize_offset_length(format, width, height, + align, stride, offset, + &length) == 0); + int expected = + stride[0] * height + + (stride[1] + (format == AV_PIX_FMT_YUV420P ? stride[2] : 0)) * + (height / 2); + assert(length == expected); + assert(offset[0] == stride[0] * height); + if (format == AV_PIX_FMT_YUV420P) + assert(offset[1] == offset[0] + stride[1] * (height / 2)); + ++layouts; + } + std::printf("PASS even-dimension layouts: %d\nFailures: %d\n", layouts, + failures); + return failures ? 1 : 0; +} diff --git a/tests/ffmpeg_ram/compare_revisions.ps1 b/tests/ffmpeg_ram/compare_revisions.ps1 new file mode 100644 index 0000000..923bd48 --- /dev/null +++ b/tests/ffmpeg_ram/compare_revisions.ps1 @@ -0,0 +1,33 @@ +param([string]$Revision = '') +$ErrorActionPreference = 'Stop' +$repo = (Resolve-Path "$PSScriptRoot/../..").Path +Push-Location $repo +try { + $build = @(& cargo +stable build --release --message-format=json) + if ($LASTEXITCODE -ne 0) { throw 'Building hwcodec failed.' } + $native = $build | ForEach-Object { $_ | ConvertFrom-Json } | + Where-Object { $_.reason -eq 'build-script-executed' -and (Test-Path (Join-Path $_.out_dir 'hwcodec.lib')) } | + Select-Object -Last 1 + if (-not $native) { throw 'Native library not found.' } + $label = if ($Revision) { (& git rev-parse --short $Revision).Trim() } else { 'working' } + $outDir = (New-Item -ItemType Directory -Force "target/ram-comparison/$label").FullName + $file = 'cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp' + $source = if ($Revision) { (& git show "${Revision}:$file") -join "`n" } else { Get-Content -Raw $file } + if ($Revision -and $LASTEXITCODE -ne 0) { throw 'Cannot read revision.' } + Set-Content -LiteralPath "$outDir/production.inc" -Value $source -Encoding UTF8 + $vcpkg = Join-Path $env:VCPKG_ROOT 'installed/x64-windows-static' + $exe = "$outDir/compare.exe" + & cl.exe /nologo /EHs /O2 /std:c++17 /MT /DNOMINMAX ` + "/I$outDir" "/I$vcpkg/include" "/I$repo/cpp/common" "/I$repo/cpp/common/platform/win" ` + "/Fo$outDir/compare.obj" "/Fe$exe" "$PSScriptRoot/compare_revisions.cpp" ` + /link "/LIBPATH:$vcpkg/lib" (Join-Path $native.out_dir 'hwcodec.lib') ` + avcodec.lib avutil.lib avformat.lib libmfx.lib d3d11.lib dxgi.lib ` + user32.lib bcrypt.lib ole32.lib advapi32.lib gdi32.lib shell32.lib oleaut32.lib uuid.lib + if ($LASTEXITCODE -ne 0) { throw 'Building comparison failed.' } + & $exe | Tee-Object -FilePath "$outDir/result.log" + $failed = $LASTEXITCODE -ne 0 + & $exe null | Tee-Object -FilePath "$outDir/null.log" + $nullExit = $LASTEXITCODE + "null-input exit=$nullExit" | Tee-Object -FilePath "$outDir/null.log" -Append + if ($failed -or $nullExit -ne 0) { throw "Regression found; see $outDir" } +} finally { Pop-Location } diff --git a/tests/ffmpeg_ram/input_lifetime.cpp b/tests/ffmpeg_ram/input_lifetime.cpp new file mode 100644 index 0000000..fd30a6c --- /dev/null +++ b/tests/ffmpeg_ram/input_lifetime.cpp @@ -0,0 +1,199 @@ +extern "C" { +#include +} + +#include +#include +#include +#include + +#ifdef NDEBUG +#error These tests require assertions. +#endif + +namespace { +std::vector retained; +bool packet_ready; +int callbacks; +bool fail_next_buffer; +int buffer_allocations; + +int injected_get_buffer(AVFrame *frame, int align) { + if (fail_next_buffer) { + fail_next_buffer = false; + return AVERROR(ENOMEM); + } + ++buffer_allocations; + return av_frame_get_buffer(frame, align); +} + +int injected_open(AVCodecContext *, const AVCodec *, AVDictionary **) { + return 0; +} + +int injected_send(AVCodecContext *, const AVFrame *frame) { + AVFrame *copy = av_frame_clone(frame); + assert(copy); + retained.push_back(copy); + packet_ready = true; + return 0; +} + +int injected_receive(AVCodecContext *, AVPacket *packet) { + if (!packet_ready) + return AVERROR(EAGAIN); + packet_ready = false; + assert(av_new_packet(packet, 1) == 0); + packet->data[0] = 42; + packet->pts = retained.back()->pts; + return 0; +} +} // namespace + +// Keep real buffers, references and copies; inject codec I/O and allocation failure. +#define av_frame_get_buffer injected_get_buffer +#define avcodec_open2 injected_open +#define avcodec_send_frame injected_send +#define avcodec_receive_packet injected_receive +#include "../../cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp" +#undef avcodec_open2 +#undef avcodec_send_frame +#undef avcodec_receive_packet +#undef av_frame_get_buffer + +extern "C" void hwcodec_log(int, const char *) {} +extern "C" void hwcodec_av_log_callback(int, const char *) {} + +namespace { +void output(const uint8_t *data, int size, int64_t, int, const void *) { + assert(size == 1 && data[0] == 42); + ++callbacks; +} + +uint8_t pixel(int plane, int x, int y, int seed) { + return static_cast(plane * 47 + x * 3 + y * 7 + seed); +} + +void check_image(const AVFrame *frame, int seed) { + const bool nv12 = frame->format == AV_PIX_FMT_NV12; + for (int p = 0; p < (nv12 ? 2 : 3); ++p) { + const int width = p == 0 ? frame->width : + ((frame->width + 1) / 2) * (nv12 ? 2 : 1); + const int height = p == 0 ? frame->height : (frame->height + 1) / 2; + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + assert(frame->data[p][y * frame->linesize[p] + x] == pixel(p, x, y, seed)); + } +} + +void check_lifetime(AVPixelFormat format, int align, int height, + bool different_destination = false) { + const int width = 66; + FFmpegRamEncoder encoder("h264_qsv", nullptr, width, height, format, align, + 30, 60, RC_CBR, Quality_Default, 1000, -1, 1, -1, + output); + int stride[AV_NUM_DATA_POINTERS] = {}; + int offset[AV_NUM_DATA_POINTERS] = {}; + int length = 0; + assert(encoder.init(stride, offset, &length)); + const bool nv12 = format == AV_PIX_FMT_NV12; + const int chroma_height = height / 2; + assert(offset[0] == stride[0] * height); + if (!nv12) + assert(offset[1] == offset[0] + stride[1] * chroma_height); + assert(length == stride[0] * height + + (stride[1] + (nv12 ? 0 : stride[2])) * chroma_height); + if (format == AV_PIX_FMT_YUV420P && align == 1) + assert(stride[1] == 33 && stride[2] == 33); + if (different_destination) { + // Exercise copying between different layouts independently of how the + // encoder chooses replacement buffers for retained frames. + av_frame_unref(encoder.frame_); + encoder.frame_->format = format; + encoder.frame_->width = width; + encoder.frame_->height = height; + assert(av_frame_get_buffer(encoder.frame_, 0) == 0); + assert(encoder.frame_->linesize[0] != stride[0]); + } + callbacks = 0; + for (int i = 0; i < 3; ++i) { + const int seed = 23 + i * 59; + std::vector input(length, 0xee); + for (int p = 0; p < (nv12 ? 2 : 3); ++p) { + const int row_bytes = p == 0 || nv12 ? width : width / 2; + const int rows = p == 0 ? height : chroma_height; + const int start = p == 0 ? 0 : offset[p - 1]; + for (int y = 0; y < rows; ++y) + for (int x = 0; x < row_bytes; ++x) + input[start + y * stride[p] + x] = pixel(p, x, y, seed); + } + if (i == 1) { + fail_next_buffer = true; + assert(encoder.encode(input.data(), length, nullptr, i * 33) == AVERROR(ENOMEM)); + assert(!fail_next_buffer); + assert(retained.size() == 1 && callbacks == 1); + check_image(retained[0], 23); + } + assert(encoder.encode(input.data(), length, nullptr, i * 33) == 0); + assert(callbacks == i + 1); + assert(retained.size() == static_cast(i + 1)); + check_image(retained.back(), seed); + std::fill(input.begin(), input.end(), 0); + check_image(retained.back(), seed); + std::vector().swap(input); + for (int j = 0; j <= i; ++j) + check_image(retained[j], 23 + j * 59); + + const int allocations_before = buffer_allocations; + for (int short_length : {0, length - 1}) { + std::vector short_input(short_length); + assert(encoder.encode(short_input.data(), short_length, nullptr, i * 33 + 1) < 0); + assert(callbacks == i + 1); + assert(retained.size() == static_cast(i + 1)); + } + assert(encoder.encode(nullptr, length, nullptr, i * 33 + 1) < 0); + assert(buffer_allocations == allocations_before); + for (int j = 0; j <= i; ++j) + check_image(retained[j], 23 + j * 59); + } + encoder.free_encoder(); + for (size_t i = 0; i < retained.size(); ++i) { + check_image(retained[i], 23 + static_cast(i) * 59); + av_frame_free(&retained[i]); + } + retained.clear(); + std::printf("PASS RAM input lifetime: format=%d align=%d height=%d different_destination=%d\n", + format, align, height, different_destination); +} + +void check_buffer_reuse(AVPixelFormat format) { + FFmpegRamEncoder encoder("h264_qsv", nullptr, 66, 34, format, 256, + 30, 60, RC_CBR, Quality_Default, 1000, -1, 1, -1, + output); + int stride[AV_NUM_DATA_POINTERS] = {}; + int offset[AV_NUM_DATA_POINTERS] = {}; + int length = 0; + assert(encoder.init(stride, offset, &length)); + std::vector input(length, 0x55); + const uint8_t *buffer = encoder.frame_->data[0]; + const int allocations_before = buffer_allocations; + for (int i = 0; i < 3; ++i) { + assert(encoder.encode(input.data(), length, nullptr, i * 33) == 0); + assert(encoder.frame_->data[0] == buffer); + av_frame_free(&retained.back()); + retained.clear(); + } + assert(buffer_allocations == allocations_before); + encoder.free_encoder(); + std::printf("PASS RAM writable buffer reuse: format=%d\n", format); +} +} // namespace + +int main() { + for (AVPixelFormat format : {AV_PIX_FMT_NV12, AV_PIX_FMT_YUV420P}) { + for (int align : {0, 1, 256}) + check_lifetime(format, align, 34); + check_lifetime(format, 256, 34, true); + check_buffer_reuse(format); + } +} diff --git a/tests/ffmpeg_ram/input_lifetime.ps1 b/tests/ffmpeg_ram/input_lifetime.ps1 new file mode 100644 index 0000000..1d30f74 --- /dev/null +++ b/tests/ffmpeg_ram/input_lifetime.ps1 @@ -0,0 +1,37 @@ +# Run from an x64 MSVC developer PowerShell with VCPKG_ROOT set. +$ErrorActionPreference = 'Stop' +if (-not (Get-Command cl.exe -ErrorAction SilentlyContinue)) { + throw 'Run from an x64 MSVC developer PowerShell.' +} +if (-not $env:VCPKG_ROOT) { + throw 'Set VCPKG_ROOT to the existing vcpkg installation used by hwcodec.' +} + +$repo = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path +Push-Location $repo +try { + $build = @(& cargo +stable build --release --message-format=json) + if ($LASTEXITCODE -ne 0) { throw 'Building hwcodec failed.' } + $native = $build | ForEach-Object { $_ | ConvertFrom-Json } | + Where-Object { + $_.reason -eq 'build-script-executed' -and + (Test-Path (Join-Path $_.out_dir 'hwcodec.lib')) + } | Select-Object -Last 1 + if (-not $native) { throw 'Cargo did not report the hwcodec native library.' } + + $outDir = (New-Item -ItemType Directory -Force 'target/ram-input-lifetime').FullName + $vcpkg = Join-Path $env:VCPKG_ROOT 'installed/x64-windows-static' + $testExe = Join-Path $outDir 'input_lifetime.exe' + & cl.exe /nologo /EHs /O2 /std:c++17 /MT /DNOMINMAX ` + "/I$vcpkg/include" "/I$repo/cpp/common" "/I$repo/cpp/common/platform/win" ` + "/Fo$outDir/input_lifetime.obj" "/Fe$testExe" ` + "$PSScriptRoot/input_lifetime.cpp" /link "/LIBPATH:$vcpkg/lib" ` + (Join-Path $native.out_dir 'hwcodec.lib') ` + avcodec.lib avutil.lib avformat.lib libmfx.lib d3d11.lib dxgi.lib ` + user32.lib bcrypt.lib ole32.lib advapi32.lib gdi32.lib shell32.lib oleaut32.lib uuid.lib + if ($LASTEXITCODE -ne 0) { throw 'Building RAM input lifetime tests failed.' } + & $testExe + if ($LASTEXITCODE -ne 0) { throw 'RAM input lifetime tests failed.' } +} finally { + Pop-Location +} diff --git a/tests/vram/repeat_failures.ps1 b/tests/vram/repeat_failures.ps1 index 5593784..82f1214 100644 --- a/tests/vram/repeat_failures.ps1 +++ b/tests/vram/repeat_failures.ps1 @@ -58,6 +58,7 @@ try { } } } + & "$PSScriptRoot/../ffmpeg_io/run.ps1" } finally { Pop-Location }