Agent skill

review-paint

Audit the D2D paint pipeline for per-frame waste — allocations, recomputation, redundant API calls at 120-240fps

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/review-paint

SKILL.md

Enter planning mode. Deep-audit the D2D paint pipeline for anything that wastes cycles per frame — from per-frame allocations to redundant D2D API calls. Use maximum parallelism — spawn explore agents for independent subsystems.

Context

The overlay renders at 120-240fps via Direct2D 1.1. At these frame rates, per-frame waste compounds fast: a 5μs allocation ×30/frame ×240fps = 36ms/s of pure garbage. The AHK runtime allocates objects on the heap — every Buffer(), Array(), Object(), and Map() lookup is a real cost.

This review covers the D2D rendering pipeline — from pre-render through BeginDraw to EndDraw, including the 8-layer compositor stack, effect chains, and resource management. It does NOT cover:

  • Shader compilation or D3D11 interop (use review-d3d for that)
  • Window data / store operations (use review-latency Path 1)
  • Keyboard hook latency (use review-latency Path 2 preamble)

Post-#177 architecture note: The compositor is an 8-layer stack: DWM backdrop → background image → shader layers 1-N → background image (configurable order) → mouse effect → selection effect → hover effect → window list + text. Shader layers render to independent D3D11 intermediate textures, composited via DrawImage with premultiplied alpha srcOver blending. PushLayer/PopLayer is eliminated for shader opacity (baked into HLSL via AT_PostProcess). However, PushAxisAlignedClip/PopLayer is still used for BG-shader-as-selection clipping (rounded rect clip to selection row). The pre-render phase (FX_PreRenderShaderLayers, FX_PreRenderMouseEffect, FX_PreRenderSelectionEffect) runs D3D11 dispatches BEFORE BeginDraw — this is part of the paint-path cost even though it's outside the D2D draw calls.

What to Look For

1. Per-Frame Allocations (Highest Priority)

Any Buffer(), [] array literal, {} object literal, or Map() creation inside a function called every frame.

The fix pattern is established (from ahk-patterns.md):

ahk
; WRONG — allocates every call
MyHotFunc() {
    buf := Buffer(72)
    NumPut("float", x, buf, 0)
    DllCall(..., "Ptr", buf, ...)
}

; CORRECT — static buffer, repopulated (ONLY if not reachable during STA pump)
MyHotFunc() {
    static buf := Buffer(72)
    NumPut("float", x, buf, 0)
    DllCall(..., "Ptr", buf, ...)
}

Scan for: Buffer(, Array(, literal [, literal { inside any function reachable from the paint path.

Exceptions:

  • Buffers whose size varies per call cannot be made static. Flag them but note the constraint.
  • Buffers in functions reachable during STA pump reentrancy MUST NOT be static — COM calls (DrawText, DrawBitmap, FillRoundedRectangle, EndDraw, DwmFlush) pump the STA message loop and can re-enter the same function through timer/callback dispatch, corrupting the shared buffer. Critical "On" does NOT prevent this. See ahk-patterns.md Hot Path Resource Rules.

2. Redundant D2D API Calls

D2D operations that produce the same result every frame:

  • Setting the same effect property to the same value (e.g., blur radius that hasn't changed)
  • Creating/releasing resources that could be cached (brushes, geometries, layers)
  • SetTransform / GetTransform pairs that could be avoided
  • Effect chain SetInput / GetOutput calls that don't change between frames

3. Cacheable Computation

Values computed every frame that only change on config change, resize, or state transition:

  • Layout math (positions, rects) recomputed on every paint when overlay size hasn't changed
  • Color conversions (hex → ARGB) done per-frame instead of on config load
  • Gradient stop arrays rebuilt identically every frame
  • Shadow/glow parameters recomputed from config values that haven't changed

4. Map Lookup Overhead

gFX_GPU["effectName"] string-keyed Map lookups on the hot path. Each lookup hashes the string and walks the bucket. When the same key is accessed multiple times per frame, cache it in a local variable.

5. Unnecessary DrawImage / DrawBitmap Calls

  • Drawing layers with zero opacity (should early-exit)
  • Drawing effects whose inputs haven't changed (could cache output bitmap like gui_bgimage.ahk does)
  • Drawing off-screen content that will be fully occluded by later layers

6. PushLayer / PopLayer / PushAxisAlignedClip Overhead

Phase 5b eliminated PushLayer/PopLayer for shader opacity (now baked into HLSL premultiplied alpha). However, PushAxisAlignedClip + PopLayer is still used for BG-shader-as-selection and BG-shader-as-hover (clipping background shaders to the selection/hover row rounded rect). Check:

  • Can the clip be avoided when the shader already respects selRect bounds via cbuffer?
  • Are clips applied when the selection shader isn't using BG-as-selection mode (wasted clip/pop)?
  • Is clip setup per-frame even when selection hasn't moved?

7. Any other detected optimizations.

Files to Audit

Primary (called every frame):

  • src/gui/gui_paint.ahk — main paint orchestrator (_GUI_PaintOverlay)
  • src/gui/gui_effects.ahk — 8-layer compositor: FX_PreRenderShaderLayers, FX_DrawShaderLayers, FX_PreRenderMouseEffect, FX_DrawMouseEffect, FX_PreRenderSelectionEffect, FX_DrawSelectionEffect, FX_PreRenderHoverEffect, FX_DrawHoverEffect, FX_DrawSoftRect, inner shadow chains
  • src/gui/gui_bgimage.ahk — background image layer
  • src/gui/gui_animation.ahk — animation tick + frame pacing

Supporting (called from paint path):

  • src/gui/gui_gdip.ahk — D2D resource management, bitmap creation, cached brushes
  • src/gui/gui_math.ahk — layout calculations
  • src/gui/d2d_shader.ahkShader_GetBitmap returns intermediate textures for DrawImage (D3D11 dispatch internals are out of scope — use review-d3d)

Init/config (not per-frame, but relevant for cache invalidation):

  • src/gui/gui_effects.ahkFX_Init(), shader layer registration, effect disposal
  • src/shared/config_registry.ahk — which values feed into paint-path computations

Explore Strategy

Split by subsystem (run in parallel):

  • Paint orchestrator agent: gui_paint.ahk — trace _GUI_PaintOverlay from pre-render through BeginDraw to EndDraw. Map every function call, every Buffer allocation, every D2D API call. Include the pre-render phase (shader/mouse/selection pre-render calls happen before BeginDraw but are per-frame cost). Count per-frame frequency.
  • Effects agent: gui_effects.ahk — the 8-layer compositor. For each layer: pre-render cost, draw cost, allocations, recomputed invariants. Key functions: FX_PreRenderShaderLayers (loops N shader layers, calls Shader_PreRender each), FX_DrawShaderLayers (loops N layers, DrawImage each), FX_PreRenderMouseEffect (compute dispatch + adaptive FPS skip), FX_PreRenderSelectionEffect/FX_PreRenderHoverEffect (selection/hover shader dispatch), FX_DrawSelectionEffect/FX_DrawHoverEffect (clip + DrawImage), FX_DrawSoftRect (inner shadow chains). Note: mouse effect has QPC-based adaptive framerate skip — check if similar skip logic would benefit background shader layers.
  • Resource agent: gui_gdip.ahk, gui_math.ahk, gui_animation.ahk — resource caching effectiveness, layout recomputation, animation state updates.

Tools

  • query_function.ps1 <func> — extract function bodies without loading full files
  • query_interface.ps1 <file> — public API surface of paint path files
  • query_timers.ps1 — find animation/repaint timers and their frequencies
  • query_global_ownership.ps1 <global> — trace who writes paint-path globals

Assessment Format

Surface everything — do not auto-exclude findings based on estimated size. At 240fps, a 2μs saving ×240 = 480μs/s. That matters.

For each finding:

Finding File:Lines Per-Call Cost Calls/Frame Per-Frame Cost Complexity Fix
FX_LayerParams allocates 72B Buffer gui_effects.ahk:123 ~3μs 4 ~12μs One-line static static buf := Buffer(72)

Columns:

  • Per-Call Cost: Estimated cost of one invocation (allocation + computation)
  • Calls/Frame: How many times this runs per frame (trace the paint path, don't guess)
  • Per-Frame Cost: Per-Call × Calls/Frame — the actual per-frame waste
  • Complexity: One-line, small refactor, medium refactor, architectural
  • Fix: Concrete fix description

Do not filter. A 1μs saving ×30/frame is 30μs/frame = 7.2ms/s at 240fps. List everything, ordered by per-frame cost (highest first).

Validation

After explore agents report back, validate every finding yourself:

  1. Trace the call path: Confirm the function is actually called per-frame by tracing from _GUI_PaintOverlay → ... → the function. Don't flag init-time code as per-frame.
  2. Check for existing optimization: This codebase has been through optimization passes. Some functions already use static buffers, cached brushes, or early-exits. Verify the waste still exists.
  3. Verify mutability: Before suggesting static, confirm the buffer content actually changes between calls (otherwise it could be a one-time init). If content is frame-invariant, the fix might be "compute once, cache" rather than "static buffer."
  4. Check AHK semantics: static in AHK v2 persists across calls to the same function. Critical "On" prevents timer/hotkey interruption but does NOT prevent STA pump reentrancy — any D2D/COM draw call can dispatch callbacks that re-enter the same function, corrupting the static buffer. Only use static when the buffer is fully consumed before any COM/D2D call, or when the function is provably unreachable from STA pump paths.
  5. Preserve Float() on GPU buffers: Never remove Float() wrappers from NumPut("float", ...) calls that feed D2D/D3D geometry buffers (rects, points, ellipses, viewports). These ensure IEEE 754 bit patterns — AHK v2 integer-to-float coercion in NumPut is not guaranteed safe. Removing them is not an optimization.
  6. Distinguish from shader path: If a finding is in d2d_shader.ahk and relates to D3D11 operations (texture creation, SRV binding, shader dispatch), it's out of scope. Only flag the D2D-side draw call.

Plan Format

Section 1 — Per-Frame Allocations:

Finding File:Lines Per-Call Cost Calls/Frame Per-Frame Cost Complexity Fix

Section 2 — Redundant D2D API Calls:

Finding File:Lines Per-Call Cost Calls/Frame Per-Frame Cost Complexity Fix

Section 3 — Cacheable Computation:

Finding File:Lines Per-Call Cost Calls/Frame Per-Frame Cost Complexity Fix

Section 4 — Map Lookups & Misc:

Finding File:Lines Per-Call Cost Calls/Frame Per-Frame Cost Complexity Fix

Order within each section by per-frame cost (highest first).

Ignore any existing plans — create a fresh one.

Expand your agent's capabilities with these related and highly-rated skills.

Didn't find tool you were looking for?

Be as detailed as possible for better results