Skip to content

Commit e5cba44

Browse files
committed
perf(windows): repeat the last frame instead of reading back an unchanged screen
The video writer read the full GPU frame back on every tick, even when WGC delivered nothing new: a CopyResource, a Map that waits on the GPU, and the BGRA to NV12 conversion, for a picture it already had. On a static screen, most of a demo, that was every tick. When no new WGC frame arrived and no new camera frame is drawn into the picture, the encoder now hands over a new sample on the last buffer. Measured at 1080p: 5.0 ms per readback, under 0.001 ms per repeat. The DXGI path and the legacy callback path read back as before. Fixes #925
1 parent ccf901e commit e5cba44

4 files changed

Lines changed: 130 additions & 1 deletion

File tree

‎electron/native/wgc-capture/src/main.cpp‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,10 @@ int wmain(int argc, wchar_t* argv[]) {
10981098
Microsoft::WRL::ComPtr<IMFSample> webcamSample;
10991099
bool hasVideoSample = false;
11001100
bool hasWebcamSample = false;
1101+
// Whether the picture this tick encodes differs from the last one:
1102+
// a new WGC frame, or a new camera frame drawn into it. The legacy
1103+
// callback path cannot tell, so it always reads back.
1104+
bool pictureChanged = legacyFrameCallback;
11011105

11021106
std::unique_lock<std::timed_mutex> legacyLock;
11031107
{
@@ -1136,6 +1140,7 @@ int wmain(int argc, wchar_t* argv[]) {
11361140
ID3D11Texture2D* wgcTexture = nullptr;
11371141
int64_t wgcTimestampHns = 0;
11381142
const bool gotFrame = session.tryGetNextFrame(&wgcTexture, &wgcTimestampHns);
1143+
pictureChanged = gotFrame;
11391144
if (gotFrame) {
11401145
if (!latestFrameTexture) {
11411146
D3D11_TEXTURE2D_DESC desc{};
@@ -1179,6 +1184,7 @@ int wmain(int argc, wchar_t* argv[]) {
11791184
latestWebcamHeight = candidateWebcamFrame.height;
11801185
latestWebcamSequence = candidateWebcamFrame.sequence;
11811186
hasVisibleWebcamFrame = true;
1187+
pictureChanged = pictureChanged || !writeSeparateWebcam;
11821188
}
11831189
}
11841190
const BgraFrameView webcamFrame{
@@ -1272,7 +1278,12 @@ int wmain(int argc, wchar_t* argv[]) {
12721278
// this struct's request: it falls back to the CPU path on
12731279
// its own when the GPU path does not fit the machine.
12741280
bool captured = false;
1275-
if (usesDxgiInput) {
1281+
if (!usesDxgiInput && !pictureChanged &&
1282+
encoder.repeatLastVideoSample(frameTimestampHns, videoSample)) {
1283+
// Nothing new to read back (#925): the static screen,
1284+
// most of a demo, costs no GPU copy and no conversion.
1285+
captured = true;
1286+
} else if (usesDxgiInput) {
12761287
captured = encoder.captureDxgiSample(
12771288
latestFrameTexture.Get(),
12781289
frameTimestampHns,

‎electron/native/wgc-capture/src/mf_encoder.cpp‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1761,6 +1761,26 @@ bool MFEncoder::captureVideoSample(
17611761
sample->SetSampleTime(sampleTime);
17621762
sample->SetSampleDuration(sampleDuration);
17631763

1764+
lastVideoBuffer_ = buffer;
1765+
outSample = sample;
1766+
return true;
1767+
}
1768+
1769+
bool MFEncoder::repeatLastVideoSample(int64_t timestampHns, Microsoft::WRL::ComPtr<IMFSample>& outSample) {
1770+
outSample.Reset();
1771+
if (!lastVideoBuffer_) {
1772+
return false;
1773+
}
1774+
const int64_t sampleDuration = 10'000'000LL / fps_;
1775+
Microsoft::WRL::ComPtr<IMFSample> sample;
1776+
if (!succeeded(MFCreateSample(&sample), "MFCreateSample(repeat)")) {
1777+
return false;
1778+
}
1779+
// Shared, not copied: a buffer is never written again once its sample is
1780+
// built, so two samples can hand the encoder the same bytes.
1781+
sample->AddBuffer(lastVideoBuffer_.Get());
1782+
sample->SetSampleTime(nextSampleTime(timestampHns, sampleDuration));
1783+
sample->SetSampleDuration(sampleDuration);
17641784
outSample = sample;
17651785
return true;
17661786
}

‎electron/native/wgc-capture/src/mf_encoder.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ class MFEncoder {
138138
int64_t timestampHns,
139139
const BgraFrameView* webcamFrame,
140140
Microsoft::WRL::ComPtr<IMFSample>& outSample);
141+
// The last frame captureVideoSample read back, again, at a new time: for a
142+
// tick on which WGC delivered nothing new. No GPU copy, no Map, no
143+
// conversion -- the readback a static screen used to pay on every tick
144+
// (getopenscreen/openscreen#925). False before any frame was captured.
145+
bool repeatLastVideoSample(int64_t timestampHns, Microsoft::WRL::ComPtr<IMFSample>& outSample);
141146
bool captureDxgiSample(
142147
ID3D11Texture2D* texture,
143148
int64_t timestampHns,
@@ -257,6 +262,7 @@ class MFEncoder {
257262
// The BGRA frame when it has to be drawn on or rescaled before the NV12
258263
// conversion; reused so a frame does not allocate one.
259264
std::vector<BYTE> bgraScratch_;
265+
Microsoft::WRL::ComPtr<IMFMediaBuffer> lastVideoBuffer_;
260266
int width_ = 0;
261267
int height_ = 0;
262268
int fps_ = 60;

‎electron/native/wgc-capture/src/mf_encoder_color_test.cpp‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,97 @@ void timeFullHd(ID3D11Device* device, ID3D11DeviceContext* context) {
258258
<< captureMs / frames << " ms, submit " << submitMs / frames << " ms per frame" << std::endl;
259259
}
260260

261+
// A tick with no new WGC frame repeats the last buffer instead of reading the
262+
// texture back again (getopenscreen/openscreen#925). The file must still hold
263+
// one frame per tick, each the captured picture; and the repeat must cost next
264+
// to nothing next to a readback, or it is not worth having.
265+
void checkRepeatedFrames(ID3D11Device* device, ID3D11DeviceContext* context) {
266+
const auto makeTexture = [&](int width, int height, Microsoft::WRL::ComPtr<ID3D11Texture2D>& texture) {
267+
std::vector<BYTE> bgra(static_cast<size_t>(width) * height * 4);
268+
for (int y = 0; y < height; y += 1) {
269+
for (int x = 0; x < width; x += 1) {
270+
BYTE* pixel = &bgra[(static_cast<size_t>(y) * width + x) * 4];
271+
pixel[2] = 255; // solid red
272+
pixel[3] = 255;
273+
}
274+
}
275+
D3D11_TEXTURE2D_DESC desc{};
276+
desc.Width = width;
277+
desc.Height = height;
278+
desc.MipLevels = 1;
279+
desc.ArraySize = 1;
280+
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
281+
desc.SampleDesc.Count = 1;
282+
desc.Usage = D3D11_USAGE_DEFAULT;
283+
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
284+
D3D11_SUBRESOURCE_DATA initial{bgra.data(), static_cast<UINT>(width * 4), 0};
285+
return SUCCEEDED(device->CreateTexture2D(&desc, &initial, &texture));
286+
};
287+
char tempDir[MAX_PATH]{};
288+
GetTempPathA(MAX_PATH, tempDir);
289+
290+
{
291+
const std::string path = std::string(tempDir) + "openscreen-mf-encoder-repeat.mp4";
292+
const std::wstring widePath(path.begin(), path.end());
293+
Microsoft::WRL::ComPtr<ID3D11Texture2D> texture;
294+
MFEncoder encoder;
295+
if (!makeTexture(kWidth, kHeight, texture) ||
296+
!encoder.initialize(widePath, kWidth, kHeight, 30, 2'000'000, device, context, nullptr, {})) {
297+
expect("repeat-setup", false, "texture or encoder");
298+
return;
299+
}
300+
Microsoft::WRL::ComPtr<IMFSample> sample;
301+
bool wrote = !encoder.repeatLastVideoSample(0, sample) && // nothing to repeat yet
302+
encoder.captureVideoSample(texture.Get(), 0, nullptr, sample) && encoder.submitVideoSample(sample.Get());
303+
for (int i = 1; i < kFrames && wrote; i += 1) {
304+
wrote = encoder.repeatLastVideoSample(static_cast<int64_t>(i) * 333'333, sample) &&
305+
encoder.submitVideoSample(sample.Get());
306+
}
307+
expect("repeat-writes", wrote && encoder.finalize(), "write or finalize failed");
308+
const std::string frames = run(
309+
"ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames "
310+
"-of default=nw=1:nk=1 \"" + path + "\"");
311+
const std::string yuv = run(
312+
"ffmpeg -v error -sseof -0.2 -i \"" + path + "\" -frames:v 1 -f rawvideo -pix_fmt yuv420p -");
313+
const int luma = yuv.empty() ? -1 : static_cast<unsigned char>(yuv[static_cast<size_t>(kHeight / 2) * kWidth + kWidth / 2]);
314+
std::cout << "REPEAT_RAW frames=" << std::atoi(frames.c_str()) << " last-frame Y=" << luma << std::endl;
315+
expect("repeat-keeps-one-frame-per-tick", std::atoi(frames.c_str()) == kFrames, frames);
316+
expect("repeat-keeps-the-picture", std::abs(luma - 63) <= 3, "Y=" + std::to_string(luma));
317+
DeleteFileA(path.c_str());
318+
}
319+
320+
{
321+
const std::string path = std::string(tempDir) + "openscreen-mf-encoder-repeat-timing.mp4";
322+
const std::wstring widePath(path.begin(), path.end());
323+
Microsoft::WRL::ComPtr<ID3D11Texture2D> texture;
324+
MFEncoder encoder;
325+
if (!makeTexture(1920, 1080, texture) ||
326+
!encoder.initialize(widePath, 1920, 1080, 60, 18'000'000, device, context, nullptr, {})) {
327+
return;
328+
}
329+
double readbackMs = 0.0;
330+
double repeatMs = 0.0;
331+
constexpr int rounds = 60;
332+
for (int i = 0; i < rounds; i += 1) {
333+
Microsoft::WRL::ComPtr<IMFSample> sample;
334+
const auto start = std::chrono::steady_clock::now();
335+
encoder.captureVideoSample(texture.Get(), static_cast<int64_t>(2 * i) * 166'667, nullptr, sample);
336+
const auto read = std::chrono::steady_clock::now();
337+
encoder.submitVideoSample(sample.Get());
338+
const auto repeatStart = std::chrono::steady_clock::now();
339+
encoder.repeatLastVideoSample(static_cast<int64_t>(2 * i + 1) * 166'667, sample);
340+
const auto repeated = std::chrono::steady_clock::now();
341+
encoder.submitVideoSample(sample.Get());
342+
readbackMs += std::chrono::duration<double, std::milli>(read - start).count();
343+
repeatMs += std::chrono::duration<double, std::milli>(repeated - repeatStart).count();
344+
}
345+
encoder.finalize();
346+
DeleteFileA(path.c_str());
347+
std::cout << "REPEAT_RAW 1080p readback " << readbackMs / rounds << " ms, repeat " << repeatMs / rounds
348+
<< " ms per frame" << std::endl;
349+
}
350+
}
351+
261352
} // namespace
262353

263354
int main() {
@@ -285,6 +376,7 @@ int main() {
285376
checkEncoder(device.Get(), context.Get(), true);
286377
checkEncoder(device.Get(), context.Get(), false);
287378
timeFullHd(device.Get(), context.Get());
379+
checkRepeatedFrames(device.Get(), context.Get());
288380

289381
std::cout << "ran " << g_ran << " tests\n";
290382
if (g_failed != 0) {

0 commit comments

Comments
 (0)