10

The problem

I am capturing a screenshot from an obstructed window on Windows 11 OS using windll.user32.PrintWindow (which in turn is calling WM_PRINT according to the docs). Everything works as expected however after moving from Windows 10 to Windows 11 the performance has been very unstable. On Win10 it usually took under 30ms for the capture, on Win11 it's sometimes close to that but every now and then the screencapture will take close to 300ms repeatedly for hours (the screencapture is running in a loop). This has never occured during many months of the screen capturing running on Win10. The single line of code responsible for this slow performance is the call to PrintWindow.

The interesting part is that the slow performance occurs only when capturing a particular application. It's a third party application and I don't have its source code, I only know it is using Java. When trying to capture other applications using the same code for screen capture, the performance is in line with expectations - around 20-30 ms.

Additional info

  • The time is roughly the same for both printing the whole window and only the clientarea (WM_PRINT or WM_PRINTCLIENT).

  • The time is scaling pretty linearly with the size of the captured window. This is NOT the case for other applications - PrintWindow takes roughly the same (around 30ms) regardless if the captured window takes up the full screen or the size is very strongly reduced.

  • The slow performance happens on both a slower machine (i5 9600 12 GB RAM) as well as on a quicker one (i7 10700 32 GB RAM). The slower PC was used to run the screencapture on Win10 (capturing in under 30ms).

  • The CPU and GPU are not overburdened when the performance get slower (looking at task manager they are using less than 3%). I didn't notice any pattern as to when does it become slower.

  • The OS settings for animations are switched off. Also, The window is not minimized and restored, so as far as I understand, the animations should not be a factor.

  • The screencapture becomes slightly slower when adding controls in the captured app. However, reducing the controls to a bare minimum still doesn't get me close to the desired 30ms capture time.

My guesses initially:

  • OS overly demanding for CPU/GPU. I think testing on i7 10700 with the same results as on i5 proves otherwise.

  • Application's messagequeue might be heavily loaded and my PrintWindow call is waiting in line. I would assume the performance scaling linearly with the window size suggests otherwise. I also tried calling RedrawWindow before calling PrintWindow - no effect.

Possible solutions/workarounds:

  • make the window as small as possible without sacrificing the information needed

  • capturing a couple of regions concurrently and then putting them together

  • Capturing the desktopscreen using BitBlt (the window has to be visible)

All of these don't address the core problem - why this particular window draws much slower than all the other windows. Any ideas much appreciated.

The code: the code is in python however the one crucial line with PrintWindow is as far as I know calling Windows dll directly. Please feel free to add ideas/workarounds regardless of the programming language.

def capture_screen_from_DC(hwnd):
    l, t, r, b = win32gui.GetWindowRect(hwnd)
    w = r - l
    h = b - t

    hwndDC = win32gui.GetWindowDC(hwnd)
    mfcDC = win32ui.CreateDCFromHandle(hwndDC)
    destDC = mfcDC.CreateCompatibleDC()

    saveBitMap = win32ui.CreateBitmap()
    saveBitMap.CreateCompatibleBitmap(mfcDC, w, h)

    destDC.SelectObject(saveBitMap)

    windll.user32.PrintWindow(hwnd, destDC.GetSafeHdc(), 2)

    bmpinfo = saveBitMap.GetInfo()
    bmpstr = saveBitMap.GetBitmapBits(True)

    im = Image.frombuffer('RGB', (bmpinfo['bmWidth'], bmpinfo['bmHeight']), bmpstr, 'raw', 'BGRX', 0, 1)

    win32gui.DeleteObject(saveBitMap.GetHandle())
    destDC.DeleteDC()
    mfcDC.DeleteDC()
    win32gui.ReleaseDC(hwnd, hwndDC)

    return im
Wojciech
  • 332
  • 1
  • 7
  • It's possible that Windows sometimes is able to bypass the app by using a cached copy of the screen with [DWM](https://en.wikipedia.org/wiki/Desktop_Window_Manager) which would be very fast. If Windows 11 introduces new conditions where that optimization can't be applied, the app itself would need to redraw the screen which could account for your slowdown. – Mark Ransom Sep 19 '22 at 02:06
  • Not related to slowness: [\[SO\]: C function called from Python via ctypes returns incorrect value (@CristiFati's answer)](https://stackoverflow.com/a/58611011/4788546). *PrintWindow* line yelds ***U**ndefined **B**ehavior* (if settings from above *URL*, not specified elsewhere in the code). Regarding performance, the most time is spent **inside** the function? – CristiFati Sep 23 '22 at 00:34
  • Also is it reproducible with both *064bit* and *032bit* *Python*? – CristiFati Sep 23 '22 at 05:43
  • Is that app frequently update its window? Is the behavior reproducible on another app (let's say a game) that updates its window at high FPS rate (>100)? I don't know what happens if *PrintWindow* is called just when the window is refreshed, but this sounds strange: "*How quickly this function returns depends on run-time factors such as **network status***". – CristiFati Sep 23 '22 at 09:27
  • 1
    From what you say, it seems reasonable to think the problem is caused by that 3rd party application when running under Windows 11, not by Windows itself nor PrintWindow API. For some reason, it behaves differently and causes what you see. Difficult to say more w/o more information. – Simon Mourier Sep 24 '22 at 17:13
  • This is a system call. There is nothing much to do. The line of code `windll.user32.PrintWindow(hwnd, destDC.GetSafeHdc(), PW_CLIENT_ONLY | PW_RENDERFULLCONTENT)`. (In your case, you have used `PW_RENDERFULLCONTENT` = 2). Try using the flags 0 and 1. I've seen some examples using 3, too. Does that help in your case? – Bilal Qandeel Sep 26 '22 at 05:19
  • You can guess or you can profile. Windows 10/11 comes with wpr which is a built-in profiling tool. Do wpr -start cpu -start diskio, let it run for 30-60s and then stop profilng with wpr -stop c:\temp\SlowScreenCapture.etl. Then you can analyze further with WPA or other tools like ETWAnalyzer. – Alois Kraus Sep 26 '22 at 21:52
  • 1
    Please edit the question, adding answers to questions in comments. -1. – CristiFati Sep 27 '22 at 07:18
  • Why don't you try to make the screenshot another way? Maybe use PIL.ImageGrab or https://pypi.org/project/mss/? – David Serero Sep 27 '22 at 16:28

2 Answers2

2

To improve the performance of PrintWindow in a Java application, You can try to use the following code:

def capture_screen_from_DC(hwnd):
  l, t, r, b = win32gui.GetWindowRect(hwnd)
  w = r - l
  h = b - t

  hwndDC = win32gui.GetWindowDC(hwnd)
  mfcDC = win32ui.CreateDCFromHandle(hwndDC)
  destDC = mfcDC.CreateCompatibleDC()

  saveBitMap = win32ui.CreateBitmap()
  saveBitMap.CreateCompatibleBitmap(mfcDC, w, h)

  destDC.SelectObject(saveBitMap)

  # Set the window to be topmost
  win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0, win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
  # Call PrintWindow
  windll.user32.PrintWindow(hwnd, destDC.GetSafeHdc(), 2)
  # Set the window back to normal
  win32gui.SetWindowPos(hwnd, win32con.HWND_NOTOPMOST, 0, 0, 0, 0, win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)

  bmpinfo = saveBitMap.GetInfo()
  bmpstr = saveBitMap.GetBitmapBits(True)

  im = Image.frombuffer('RGB', (bmpinfo['bmWidth'], bmpinfo['bmHeight']), bmpstr, 'raw', 'BGRX', 0, 1)

  win32gui.DeleteObject(saveBitMap.GetHandle())
  destDC.DeleteDC()
  mfcDC.DeleteDC()
  win32gui.ReleaseDC(hwnd, hwndDC)

  return im 

Explanation: The PrintWindow function is a synchronous function. When the window is not topmost, the window manager may need to wait for other windows to be painted before it can paint the window. This may cause the PrintWindow function to be slow. When the window is topmost, the window manager will not wait for other windows to be painted before it can paint the window. This may cause the PrintWindow function to be fast.

Tibic4
  • 3,709
  • 1
  • 13
1

I really like @Tibic4's explanation, however, pushing the window to the top may not be desirable if you're trying to hide the screen-capture behavior from the user.

If the background window is a Java application using Swing for rendering the UI, the repaint call runs in the event dispatch thread (if I remember right.) When you call windll.user32.PrintWindow, the thread your process is running in has to wait for the Java process' event dispatch thread to get prioritized by the CPU, which is when it can paint and return the image for you. That sequence of events may be slowed down if the foreground application has spawned multiple threads that are tying up the CPU. However, you may be able to explicitly give the Java process priority, which might accelerate things.

Bill Horvath
  • 1,336
  • 9
  • 24
  • Not many details were given. I assumed it might be a valid option. your contribution was excellent. – Tibic4 Sep 29 '22 at 16:56