@@ -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
263354int 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