Paint refactor — remaining work

Historical note: the HDC cleanup described in sections 1, 2, and 4 is now complete. WAT allocates semantic DC handles and state; legacy synthetic paint HDCs are adopted into canonical WAT state on use; JavaScript has no DC table, HDC decoder, or draw-target fallback. Those sections remain below as implementation history, not active tasks.

The longer-term pixel-storage and rasterization architecture is tracked in docs/software-gdi-design.md. That design incorporates the HDC cleanup below into a single surface-resolution model; do not create a second competing DC abstraction while implementing either plan.

Phase B steps 1–4 landed (commits c522119, a0d4fcf, d014c0d, d8b3810). Region-driven WM_PAINT pump is live; mspaint baseline holds. The items below are intentionally deferred — they form a coherent follow-up rather than partial work.

1. Step 2 completion: migrate synthesized-hdc sites

Why deferred. Step 2a migrated the sites with natural alloc/release pairs (GetDC/ReleaseDC, GetWindowDC, BeginPaint/EndPaint). The remaining inline (i32.add hwnd 0x40000) / 0xC0000 sites synthesize an hdc that gets handed to a wndproc which draws and discards — there is no return trip that can call host_release_dc. Migrating them under the allocator leaks records into _dcTable indefinitely.

Sites still on the legacy encoding (grep 0x40000\)|0xC0000\) in src/*.wat, post step 2a):

File Line Pattern
src/09a-handlers.wat 7666 WM_ERASEBKGND msg payload (wParam=hdc)
src/09a4-handlers-gdi.wat 263 CreateDC fake screen DC return
src/09a5-handlers-window.wat 847, 992 WM_ERASEBKGND / WM_PAINT msg payload
src/09a8-handlers-directx.wat 1906 DDraw forwarded hdc
src/09c-help.wat 436 help WM_PAINT inline hdc
src/09c4-defwndproc.wat 69 NCPAINT whole-window DC
src/09c5-menu.wat 156, 231, 261, 472 menu paint inline hdc (×4)
src/09c3-controls.wat 1032, 1783, 2049, 2298, 2457, 3127, 4045, 4800, 4838, 4867, 5901 control wndproc WM_PAINT inline hdc / DRAWITEMSTRUCT.hDC (×11)

Lifecycle options to investigate:

Gate to chase: grep -rn '0x40000\|0xC0000' src/*.wat | grep -v '0x4000000\|0xC000000' returns 0, AND _dcTable.size over a long mspaint session stays bounded.

2. Step 5: delete legacy decoders

Blocked on (1). Once every site uses _dcTable:

Also delete the dead paint-pump scaffolding in src/09a5-handlers-window.wat that step 4 obsoleted — paint_pending global setters that no longer gate dispatch, and paint_flag_take/paint_flag_first/paint_flag_any if no other consumer remains. Keep PAINT_FLAGS only if a non-pump consumer uses it.

3. gdi_fill_rect ≥80%-coverage sibling auto-invalidate

What. When gdi_fill_rect (or any primitive that erases parent background) hits a window without WS_CLIPCHILDREN, walk children and seed their _updateRgns for any rect overlap. Trigger only when the fill covers ≥80% of the client area, so child-internal draws aren't treated as background erases.

Why deferred. This is a behavior change with no obvious test that exercises it under the new pump. Better to land in isolation against a known-good baseline so a regression points at this change specifically. Tracked in project_mspaint_tool_palette_repaint.md — the bug it addresses is "tool button click wipes siblings from back-canvas." That bug is independent of the A.2 pump fix and is still latent.

Where. Inside the gdi_fill_rect host import in lib/host-imports.js (around line 2516). After resolving the target hwnd, before drawing:

if (target_hwnd_lacks_clipchildren && rect_covers_>=80%_of_client) {
  for each WS_CHILD intersecting rect:
    _invalRectHwnd(child, intersected_rect_in_child_coords);
}

Gate. Mspaint tool palette must still match baseline; clicking a tool button must not lose sibling icons. New screenshot test under test/test-mspaint-tool-click.js would lock this in.

4. Misc loose ends