From f08ff39f05ade85c584a4c8f92692982dc842ad8 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 24 Sep 2026 12:08:50 +0800 Subject: [PATCH 1/3] fix: copy RAM encoder input into owned frame buffers Keep caller buffers from outliving their Rust borrow through retained AVFrame pointers. Preserve input strides across frame reallocations and add NV12/YUV420P lifetime and alignment regression tests. --- cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp | 30 ++++--- tests/ffmpeg_ram/README.md | 18 ++++ tests/ffmpeg_ram/input_lifetime.cpp | 126 +++++++++++++++++++++++++++ tests/ffmpeg_ram/input_lifetime.ps1 | 37 ++++++++ 4 files changed, 198 insertions(+), 13 deletions(-) create mode 100644 tests/ffmpeg_ram/README.md create mode 100644 tests/ffmpeg_ram/input_lifetime.cpp create mode 100644 tests/ffmpeg_ram/input_lifetime.ps1 diff --git a/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp b/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp index 700cab0..5a71679 100644 --- a/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp +++ b/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp @@ -116,6 +116,7 @@ class FFmpegRamEncoder { int gpu_ = 0; RamEncodeCallback callback_ = NULL; int offset_[AV_NUM_DATA_POINTERS] = {0}; + int input_linesize_[AV_NUM_DATA_POINTERS] = {0}; AVHWDeviceType hw_device_type_ = AV_HWDEVICE_TYPE_NONE; AVPixelFormat hw_pixfmt_ = AV_PIX_FMT_NONE; @@ -262,6 +263,7 @@ class FFmpegRamEncoder { 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; @@ -368,39 +370,41 @@ class FFmpegRamEncoder { int fill_frame(AVFrame *frame, uint8_t *data, int data_length, const int *const offset) { + 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)) { + frame->height * (input_linesize_[0] + input_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])); + ", linesize[0]:" + std::to_string(input_linesize_[0]) + + ", linesize[1]:" + std::to_string(input_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)) { + frame->height * (input_linesize_[0] + input_linesize_[1] / 2 + + input_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])); + ", linesize[0]:" + std::to_string(input_linesize_[0]) + + ", linesize[1]:" + std::to_string(input_linesize_[1]) + + ", linesize[2]:" + std::to_string(input_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; } }; diff --git a/tests/ffmpeg_ram/README.md b/tests/ffmpeg_ram/README.md new file mode 100644 index 0000000..e87954a --- /dev/null +++ b/tests/ffmpeg_ram/README.md @@ -0,0 +1,18 @@ +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 force copy-on-write. + +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 +normal alignment and a 256-byte input stride that differs from the reallocated +frame's stride. Truncated input must fail without submitting a frame. + +These tests verify buffer ownership and layout, not hardware driver behavior. diff --git a/tests/ffmpeg_ram/input_lifetime.cpp b/tests/ffmpeg_ram/input_lifetime.cpp new file mode 100644 index 0000000..b50b8d5 --- /dev/null +++ b/tests/ffmpeg_ram/input_lifetime.cpp @@ -0,0 +1,126 @@ +extern "C" { +#include +} + +#include +#include +#include +#include + +#ifdef NDEBUG +#error These tests require assertions. +#endif + +namespace { +std::vector retained; +bool packet_ready; +int callbacks; + +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 frame allocation, reference counting and copies; replace codec I/O. +#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 + +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 || nv12 ? frame->width : frame->width / 2; + const int height = p == 0 ? frame->height : frame->height / 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) { + const int width = 66, height = 34; + 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)); + callbacks = 0; + for (int i = 0; i < 3; ++i) { + const int seed = 23 + i * 59; + std::vector input(length, 0xee); + const bool nv12 = format == AV_PIX_FMT_NV12; + 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 : height / 2; + 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); + } + assert(encoder.encode(input.data(), length, nullptr, i * 33) == 0); + assert(callbacks == 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); + + std::vector short_input(length - 1); + assert(encoder.encode(short_input.data(), length - 1, nullptr, i * 33 + 1) < 0); + assert(callbacks == i + 1); + if (align == 256) + assert(encoder.frame_->linesize[0] != stride[0]); + } + 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\n", format, align); +} +} // namespace + +int main() { + for (AVPixelFormat format : {AV_PIX_FMT_NV12, AV_PIX_FMT_YUV420P}) + for (int align : {0, 256}) + check_lifetime(format, align); +} diff --git a/tests/ffmpeg_ram/input_lifetime.ps1 b/tests/ffmpeg_ram/input_lifetime.ps1 new file mode 100644 index 0000000..ac1a0e3 --- /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 --offline --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 +} From 04f2d8e362c6c320bb01f51328c4d4afe9cedf13 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 24 Sep 2026 21:28:39 +0800 Subject: [PATCH 2/3] fix: validate RAM input layout and avoid copying stale pixels Cache the full initialized input length and reject invalid input before allocation or copying. Reuse writable buffers; otherwise allocate with the original alignment without copying old pixels. Keep the original chroma formulas for the even-dimension API contract. Combine 1731aae and 6cd7702, including their runtime comparison fixtures. --- .github/workflows/windows-vram.yml | 19 ++- cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp | 53 ++++--- tests/ffmpeg_ram/README.md | 19 ++- tests/ffmpeg_ram/compare_revisions.cpp | 198 +++++++++++++++++++++++++ tests/ffmpeg_ram/compare_revisions.ps1 | 33 +++++ tests/ffmpeg_ram/input_lifetime.cpp | 105 +++++++++++-- tests/ffmpeg_ram/input_lifetime.ps1 | 2 +- 7 files changed, 380 insertions(+), 49 deletions(-) create mode 100644 tests/ffmpeg_ram/compare_revisions.cpp create mode 100644 tests/ffmpeg_ram/compare_revisions.ps1 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/ffmpeg_ram/ffmpeg_ram_encode.cpp b/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp index 5a71679..7c5ffb6 100644 --- a/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp +++ b/cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp @@ -117,6 +117,7 @@ class FFmpegRamEncoder { 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; @@ -260,6 +261,7 @@ 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]; @@ -272,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) { @@ -368,34 +384,15 @@ 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 * (input_linesize_[0] + input_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(input_linesize_[0]) + - ", linesize[1]:" + std::to_string(input_linesize_[1])); - return -1; - } - src[1] = data + offset[0]; + src[1] = data + offset_[0]; break; case AV_PIX_FMT_YUV420P: - if (data_length < - frame->height * (input_linesize_[0] + input_linesize_[1] / 2 + - input_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(input_linesize_[0]) + - ", linesize[1]:" + std::to_string(input_linesize_[1]) + - ", linesize[2]:" + std::to_string(input_linesize_[2])); - return -1; - } - src[1] = data + offset[0]; - src[2] = data + offset[1]; + src[1] = data + offset_[0]; + src[2] = data + offset_[1]; break; default: LOG_ERROR(std::string("fill_frame: unsupported format, ") + @@ -475,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/tests/ffmpeg_ram/README.md b/tests/ffmpeg_ram/README.md index e87954a..9210da9 100644 --- a/tests/ffmpeg_ram/README.md +++ b/tests/ffmpeg_ram/README.md @@ -8,11 +8,24 @@ existing static FFmpeg installation with QSV enabled: 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 force copy-on-write. +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 -normal alignment and a 256-byte input stride that differs from the reallocated -frame's stride. Truncated input must fail without submitting a frame. +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 index b50b8d5..fd30a6c 100644 --- a/tests/ffmpeg_ram/input_lifetime.cpp +++ b/tests/ffmpeg_ram/input_lifetime.cpp @@ -15,6 +15,17 @@ 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; @@ -39,7 +50,8 @@ int injected_receive(AVCodecContext *, AVPacket *packet) { } } // namespace -// Keep real frame allocation, reference counting and copies; replace codec I/O. +// 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 @@ -47,6 +59,7 @@ int injected_receive(AVCodecContext *, AVPacket *packet) { #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 *) {} @@ -64,16 +77,18 @@ uint8_t pixel(int plane, int x, int y, int 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 || nv12 ? frame->width : frame->width / 2; - const int height = p == 0 ? frame->height : frame->height / 2; + 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) { - const int width = 66, height = 34; +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); @@ -81,21 +96,47 @@ void check_lifetime(AVPixelFormat format, int align) { 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); - const bool nv12 = format == AV_PIX_FMT_NV12; 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 : height / 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); @@ -103,11 +144,17 @@ void check_lifetime(AVPixelFormat format, int align) { for (int j = 0; j <= i; ++j) check_image(retained[j], 23 + j * 59); - std::vector short_input(length - 1); - assert(encoder.encode(short_input.data(), length - 1, nullptr, i * 33 + 1) < 0); - assert(callbacks == i + 1); - if (align == 256) - assert(encoder.frame_->linesize[0] != stride[0]); + 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) { @@ -115,12 +162,38 @@ void check_lifetime(AVPixelFormat format, int align) { av_frame_free(&retained[i]); } retained.clear(); - std::printf("PASS RAM input lifetime: format=%d align=%d\n", format, align); + 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, 256}) - check_lifetime(format, align); + 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 index ac1a0e3..1d30f74 100644 --- a/tests/ffmpeg_ram/input_lifetime.ps1 +++ b/tests/ffmpeg_ram/input_lifetime.ps1 @@ -10,7 +10,7 @@ if (-not $env:VCPKG_ROOT) { $repo = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path Push-Location $repo try { - $build = @(& cargo +stable build --offline --release --message-format=json) + $build = @(& cargo +stable build --release --message-format=json) if ($LASTEXITCODE -ne 0) { throw 'Building hwcodec failed.' } $native = $build | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object { From d3305fb8c44590062b72917ccf38b155d1641aa0 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 24 Sep 2026 21:28:39 +0800 Subject: [PATCH 3/3] fix: bound GPU query polling and propagate decode failures Stop on GetData errors, bound polling with a steady-clock deadline, and report failed shader completion through the existing decode error path. Keep codec send/receive behavior unchanged. Compare the production Query method and decoder FFI against 04f2d8e: eight failing cases become passing, and three successful cases remain passing. --- cpp/common/platform/win/win.cpp | 26 +++-- cpp/ffmpeg_vram/ffmpeg_vram_decode.cpp | 6 +- tests/ffmpeg_io/README.md | 13 +++ tests/ffmpeg_io/decode_query.cpp | 125 +++++++++++++++++++++++++ tests/ffmpeg_io/query.cpp | 95 +++++++++++++++++++ tests/ffmpeg_io/run.ps1 | 41 ++++++++ tests/vram/repeat_failures.ps1 | 1 + 7 files changed, 297 insertions(+), 10 deletions(-) create mode 100644 tests/ffmpeg_io/README.md create mode 100644 tests/ffmpeg_io/decode_query.cpp create mode 100644 tests/ffmpeg_io/query.cpp create mode 100644 tests/ffmpeg_io/run.ps1 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_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/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 }