Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .github/workflows/windows-vram.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Windows VRAM failure injection
name: Windows codec regression tests

on:
pull_request:
Expand Down Expand Up @@ -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: |
Expand All @@ -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
26 changes: 17 additions & 9 deletions cpp/common/platform/win/win.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@

#define NUMVERTICES 6

namespace {
using QueryClock = std::chrono::steady_clock;
}

typedef struct _VERTEX {
DirectX::XMFLOAT3 Pos;
DirectX::XMFLOAT2 TexCoord;
Expand Down Expand Up @@ -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,
Expand Down
61 changes: 31 additions & 30 deletions cpp/ffmpeg_ram/ffmpeg_ram_encode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
};
Expand Down Expand Up @@ -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;
}
}
6 changes: 5 additions & 1 deletion cpp/ffmpeg_vram/ffmpeg_vram_decode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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();
Expand Down
13 changes: 13 additions & 0 deletions tests/ffmpeg_io/README.md
Original file line number Diff line number Diff line change
@@ -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 <commit>` 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/<revision>/`.
125 changes: 125 additions & 0 deletions tests/ffmpeg_io/decode_query.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
extern "C" {
#include <libavcodec/avcodec.h>
}
#include "platform/win/win.h"
#include "util.h"
#include <cassert>
#include <cstdio>
#include <deque>
#include <vector>

#ifdef NDEBUG
#error These tests require assertions.
#endif

namespace {
int sends, receives, marker, query_fail_at, query_calls;
std::deque<int> receive_results;
std::vector<int> output_markers;
ComPtr<ID3D11Texture2D> 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<uint8_t *>(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<TestNativeDevice>();
int ret = ffmpeg_vram_decode(&codec, input, sizeof(input), output, nullptr);
codec.destroy();
return ret;
}
} // namespace

int main() {
ComPtr<ID3D11Device> 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<int> expected) {
query_fail_at = fail_at;
sends = receives = query_calls = 0;
receive_results = fail_at == 1 ? std::deque<int>{31, AVERROR(EAGAIN)}
: std::deque<int>{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<int>(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;
}
Loading
Loading