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:
+77
-37
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user