perf(overlay): pre-allocate persistent DIB section and handle WM_SETCURSOR

1. Handled WM_SETCURSOR explicitly in the window procedure, setting IDC_HAND
   when dragging/hovering and returning 1 to prevent Windows from displaying
   an hourglass/wait cursor.
2. Pre-allocated persistent memory DC, DIB section, and buffer pointer in
   _init_gdi_resources() instead of allocating and freeing them 60 times/sec
   in draw_frame(), eliminating GDI handle churn.
3. Added cleanup of DIB/DC handles and class unregistration in destroy().
This commit is contained in:
2026-09-08 23:26:26 +02:00
parent 4d1d7cc655
commit bbc6768b78
+77 -37
View File
@@ -46,6 +46,13 @@ gdi32.DeleteObject.restype = wintypes.BOOL
user32.SetCursor.argtypes = [wintypes.HCURSOR]
user32.SetCursor.restype = wintypes.HCURSOR
user32.LoadCursorW.argtypes = [wintypes.HINSTANCE, ctypes.c_void_p]
user32.LoadCursorW.restype = wintypes.HCURSOR
user32.UnregisterClassW.argtypes = [wintypes.LPCWSTR, wintypes.HINSTANCE]
user32.UnregisterClassW.restype = wintypes.BOOL
# Window styles # Window styles
WS_POPUP = 0x80000000 WS_POPUP = 0x80000000
WS_VISIBLE = 0x10000000 WS_VISIBLE = 0x10000000
@@ -63,12 +70,17 @@ AC_SRC_ALPHA = 0x01
# Window messages # Window messages
WM_DESTROY = 0x0002 WM_DESTROY = 0x0002
WM_SETCURSOR = 0x0020
WM_PAINT = 0x000F WM_PAINT = 0x000F
WM_LBUTTONDOWN = 0x0201 WM_LBUTTONDOWN = 0x0201
WM_LBUTTONUP = 0x0202 WM_LBUTTONUP = 0x0202
WM_MOUSEMOVE = 0x0200 WM_MOUSEMOVE = 0x0200
WM_NCHITTEST = 0x0084 WM_NCHITTEST = 0x0084
# Cursors
IDC_ARROW = 32512
IDC_HAND = 32649
# Hit test return codes # Hit test return codes
HTTRANSPARENT = -1 HTTRANSPARENT = -1
HTCLIENT = 1 HTCLIENT = 1
@@ -190,7 +202,39 @@ class OverlayWindow:
self._current_image: Optional[Image.Image] = None self._current_image: Optional[Image.Image] = None
self.is_dragging = False self.is_dragging = False
# Persistent GDI handles for fast zero-allocation frame updates
self.hdc_screen: Optional[int] = None
self.hdc_mem: Optional[int] = None
self.hbitmap: Optional[int] = None
self.old_bmp: Optional[int] = None
self.bits_ptr: Optional[ctypes.c_void_p] = None
self._create_window() self._create_window()
self._init_gdi_resources()
def _init_gdi_resources(self) -> None:
"""Pre-allocates persistent DC and DIB section to avoid 60Hz GDI alloc/dealloc churn."""
self.hdc_screen = user32.GetDC(0)
self.hdc_mem = gdi32.CreateCompatibleDC(self.hdc_screen)
bmi = BITMAPINFO()
bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
bmi.bmiHeader.biWidth = self.width
bmi.bmiHeader.biHeight = -self.height # Top-down DIB
bmi.bmiHeader.biPlanes = 1
bmi.bmiHeader.biBitCount = 32
bmi.bmiHeader.biCompression = 0 # BI_RGB
self.bits_ptr = ctypes.c_void_p()
self.hbitmap = gdi32.CreateDIBSection(
self.hdc_mem,
ctypes.byref(bmi),
0,
ctypes.byref(self.bits_ptr),
0,
0,
)
self.old_bmp = gdi32.SelectObject(self.hdc_mem, self.hbitmap)
def _create_window(self) -> None: def _create_window(self) -> None:
"""Registers window class and creates the Win32 layered overlay window.""" """Registers window class and creates the Win32 layered overlay window."""
@@ -206,7 +250,7 @@ class OverlayWindow:
wndclass.cbWndExtra = 0 wndclass.cbWndExtra = 0
wndclass.hInstance = hinstance wndclass.hInstance = hinstance
wndclass.hIcon = 0 wndclass.hIcon = 0
wndclass.hCursor = user32.LoadCursorW(0, 32512) # IDC_ARROW wndclass.hCursor = user32.LoadCursorW(0, IDC_ARROW)
wndclass.hbrBackground = 0 wndclass.hbrBackground = 0
wndclass.lpszMenuName = None wndclass.lpszMenuName = None
wndclass.lpszClassName = self.CLASS_NAME wndclass.lpszClassName = self.CLASS_NAME
@@ -271,6 +315,16 @@ class OverlayWindow:
return HTTRANSPARENT return HTTRANSPARENT
elif msg == WM_SETCURSOR:
# Handle cursor explicitly so Windows never falls back to an hourglass/wait cursor
hit = lparam & 0xFFFF
if hit == HTCLIENT:
cur_id = IDC_HAND if self.is_dragging else IDC_ARROW
cursor = user32.LoadCursorW(0, cur_id)
user32.SetCursor(cursor)
return 1
return user32.DefWindowProcW(hwnd, msg, wparam, lparam)
elif msg == WM_LBUTTONDOWN: elif msg == WM_LBUTTONDOWN:
user32.SetCapture(hwnd) user32.SetCapture(hwnd)
self.is_dragging = True self.is_dragging = True
@@ -324,7 +378,7 @@ class OverlayWindow:
Renders a 32-bit RGBA PIL Image to the overlay using UpdateLayeredWindow. Renders a 32-bit RGBA PIL Image to the overlay using UpdateLayeredWindow.
Converts pixels to premultiplied BGRA for hardware-accelerated Windows compositing. Converts pixels to premultiplied BGRA for hardware-accelerated Windows compositing.
""" """
if not self.hwnd: if not self.hwnd or not self.hdc_mem or not self.bits_ptr:
return return
if x is not None: if x is not None:
@@ -339,34 +393,10 @@ class OverlayWindow:
self._current_image = image self._current_image = image
# Convert PIL RGBA to premultiplied BGRA bytes # Convert PIL RGBA to premultiplied BGRA bytes
# Windows UpdateLayeredWindow expects B, G, R, A with color channels premultiplied by alpha
bgra_bytes = self._premultiply_bgra(image) bgra_bytes = self._premultiply_bgra(image)
# Create compatible Memory DC and DIB section # Fast direct memory copy into the pre-allocated DIB section
hdc_screen = user32.GetDC(0) ctypes.memmove(self.bits_ptr, bgra_bytes, len(bgra_bytes))
hdc_mem = gdi32.CreateCompatibleDC(hdc_screen)
bmi = BITMAPINFO()
bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
bmi.bmiHeader.biWidth = self.width
bmi.bmiHeader.biHeight = -self.height # Top-down DIB
bmi.bmiHeader.biPlanes = 1
bmi.bmiHeader.biBitCount = 32
bmi.bmiHeader.biCompression = 0 # BI_RGB
bits_ptr = ctypes.c_void_p()
hbitmap = gdi32.CreateDIBSection(
hdc_mem,
ctypes.byref(bmi),
0,
ctypes.byref(bits_ptr),
0,
0,
)
# Copy BGRA bytes directly into the DIB memory buffer
ctypes.memmove(bits_ptr, bgra_bytes, len(bgra_bytes))
old_bmp = gdi32.SelectObject(hdc_mem, hbitmap)
pt_dst = POINT(self.x, self.y) pt_dst = POINT(self.x, self.y)
size = SIZE(self.width, self.height) size = SIZE(self.width, self.height)
@@ -381,22 +411,16 @@ class OverlayWindow:
# Single atomic call to update layered window position and 32-bit alpha frame # Single atomic call to update layered window position and 32-bit alpha frame
user32.UpdateLayeredWindow( user32.UpdateLayeredWindow(
self.hwnd, self.hwnd,
hdc_screen, self.hdc_screen,
ctypes.byref(pt_dst), ctypes.byref(pt_dst),
ctypes.byref(size), ctypes.byref(size),
hdc_mem, self.hdc_mem,
ctypes.byref(pt_src), ctypes.byref(pt_src),
0, 0,
ctypes.byref(blend), ctypes.byref(blend),
ULW_ALPHA, ULW_ALPHA,
) )
# Cleanup GDI handles
gdi32.SelectObject(hdc_mem, old_bmp)
gdi32.DeleteObject(hbitmap)
gdi32.DeleteDC(hdc_mem)
user32.ReleaseDC(0, hdc_screen)
@staticmethod @staticmethod
def _premultiply_bgra(img: Image.Image) -> bytes: def _premultiply_bgra(img: Image.Image) -> bytes:
""" """
@@ -446,7 +470,23 @@ class OverlayWindow:
return True return True
def destroy(self) -> None: def destroy(self) -> None:
"""Destroys the window handle and unregisters class.""" """Destroys the window handle, releases GDI resources, and unregisters class."""
if self.hwnd: if self.hwnd:
user32.DestroyWindow(self.hwnd) user32.DestroyWindow(self.hwnd)
self.hwnd = None self.hwnd = None
if self.hdc_mem:
gdi32.SelectObject(self.hdc_mem, self.old_bmp)
gdi32.DeleteObject(self.hbitmap)
gdi32.DeleteDC(self.hdc_mem)
self.hdc_mem = None
if self.hdc_screen:
user32.ReleaseDC(0, self.hdc_screen)
self.hdc_screen = None
try:
hinstance = kernel32.GetModuleHandleW(None)
user32.UnregisterClassW(self.CLASS_NAME, hinstance)
except Exception:
pass