# Double Buffer Architecture Research v16
## Focus: v5 失敗的真正機制 + 精準修正方案

---

## 重新審視 v5 失敗

### v5 的假設
「穩定狀態下 buffer 有效，可跳過 capture」

### 假設何時成立？
- 上一幀 writeback 後，buffer = screen = 最終畫面
- 下一幀只有少量 cell 變化（按鍵、游標移動）
- TextOut 覆蓋變化的 cell → buffer 更新 → writeback = 正確

### 假設何時不成立？
- **畫面大切換**（RestScreen、Scroll、CLS、@ CLEAR TO）
- 整個螢幕或大範圍的 cell 同時變化
- Harbour 的 Redraw 可能分多次 WM_PAINT 處理
- 第一次 WM_PAINT 的 ps.rcPaint 只覆蓋部分
- 跳過 capture → buffer 有舊內容 → writeback 閃爍

---

## 關鍵問題：ps.rcPaint 一定覆蓋所有 dirty cell 嗎？

### Harbour GT 的 Redraw 機制
```
hb_gt_wvt_Refresh()
  → HB_GTSUPER_REFRESH()
    → RedrawDiff: 比對 prevBuffer vs screenBuffer
      → 對每個變化的 cell 呼叫 Redraw(row, col, size)
        → hb_gt_wvt_Redraw: InvalidateRect(pixel_rect)
  → ProcessMessages
    → DispatchMessage → WM_PAINT
```

### InvalidateRect 的合併行為
- Windows 把所有 InvalidateRect 合併為一個 UNION rect
- 例：row 0 col 0 + row 24 col 79 → 合併為 {0,0, fullW, fullH}
- **一次 Refresh 的所有 Redraw 會在同一次 ProcessMessages 前完成**
- 所以 ps.rcPaint = 所有 dirty cell 的 UNION
- **ps.rcPaint 一定覆蓋所有 dirty cell**

### 那 v5 為什麼閃爍？

等等... 如果 ps.rcPaint 一定覆蓋所有 dirty cell，
TextOut 就會重繪所有 dirty cell，buffer 就會有正確內容。
跳過 capture 不應該造成閃爍。

### 重新分析閃爍的實際機制

閃爍發生在「離開銷貨單回主選單」。流程：

```
1. BUYSELL 結束
2. RESTORE SCREEN FROM cBuffer（主選單畫面寫回 Harbour screen buffer）
3. 但 RestScreen 內部可能分步驟：
   a. 先寫前半部 → 觸發 Redraw
   b. 再寫後半部 → 觸發 Redraw
4. Harbour Refresh → 收集所有 Redraw → 一次 WM_PAINT
5. ps.rcPaint = 所有 Redraw 的 UNION
6. PaintText TextOut → buffer（覆蓋所有 dirty cell）
7. writeback → screen → 正確
```

**如果步驟 4 的 Refresh 在步驟 3 完成之前被觸發呢？**

```
3a. RestScreen 寫前半部 → Redraw
3a'. ★ 此時 Harbour InKey/ProcessMessages 被呼叫？
     → WM_PAINT #1：ps.rcPaint = 前半部
     → TextOut 覆蓋前半部（buffer 有 BUYSELL 前半 + 主選單後半混合）
     → writeback → screen 閃爍！
3b. RestScreen 寫後半部 → Redraw
     → WM_PAINT #2：修正後半部
```

### RestScreen 會在中間觸發 ProcessMessages 嗎？

查 Harbour 原始碼：`hb_gtRestArea()` / `hb_gtRest()` 的實作。
- RestScreen 寫入所有 cell 後才 return
- Harbour GT 的 `hb_gt_def_PutScrChar` 只更新 screen buffer，不觸發 Redraw
- Redraw 在 `hb_gt_def_Refresh` 中觸發（比對 prevBuffer）
- Refresh 在 `InKey()` 或顯式呼叫時執行
- **RestScreen 內部不觸發 Refresh**

所以步驟 3 完成後，所有 cell 都已寫入。步驟 4 的 Refresh 看到所有 cell 變化。
一次 WM_PAINT 處理全部。

### ★ 那閃爍到底怎麼來的？

**可能性 A：RestScreen 之後有額外的畫面操作**

```
BUYSELL 結束：
1. RESTORE SCREEN
2. SetColor(cLastColor)        ← 可能觸發 Refresh？不會
3. 返回到 PullDownMenu
4. PullDownMenu 重繪選單列    ← 新的 @ SAY 操作
5. ★ 步驟 1 的 Refresh 在步驟 4 之前被處理
   → WM_PAINT 只包含 RestScreen 的 dirty
   → 步驟 4 的選單列變化還沒在 buffer 裡
   → writeback 的 buffer 有 RestScreen 內容但沒有新選單列
   → 使用者看到「沒有選單列」的中間狀態 → 閃爍
6. 步驟 4 的 @ SAY 觸發 Redraw → WM_PAINT #2
   → 修正選單列
```

**這就是閃爍的機制！**

RestScreen 和後續的選單重繪分成兩次 WM_PAINT。
第一次 WM_PAINT 的 buffer 有 RestScreen 內容但沒有選單列。
跳過 capture 不影響這個（因為 PaintText capture 也不會有選單列內容）。

**等等 — 這個閃爍跟 v5 跳過 capture 有什麼關係？**

有 capture 時：
- WM_PAINT #1：capture screen → buffer（screen 有 RestScreen 內容）
- TextOut → buffer（更新 dirty cell）
- writeback → screen
- screen = buffer = RestScreen 內容（沒有選單列）→ 看到中間狀態

沒有 capture 時（v5）：
- WM_PAINT #1：跳過 capture（buffer 有 BUYSELL 內容！）
- TextOut → buffer（只更新 dirty cell，其他保留 BUYSELL 內容）
- writeback → screen
- screen = 混合的 BUYSELL + RestScreen → ★★ 更明顯的閃爍！

**結論：有 capture 也會閃（但閃的是「沒選單列」），v5 閃得更嚴重（閃的是「BUYSELL 殘留」）。**

---

## v5 的精準修正

### 方案 A：偵測大面積 dirty → force capture
```c
// ps.rcPaint 覆蓋超過 50% 客戶區 → force capture
int dirtyArea = (ps.rcPaint.right - ps.rcPaint.left) * (ps.rcPaint.bottom - ps.rcPaint.top);
int totalArea = rcClient.right * rcClient.bottom;
if( dirtyArea > totalArea / 2 )
   g_bBufferValid = 0;  // force capture this frame
```

### 方案 B：偵測 dirty rows > N → force capture
```c
int dirtyRows = rcRect.bottom - rcRect.top + 1;
if( dirtyRows > 5 )
   g_bBufferValid = 0;
```

### 方案 C：每次 Refresh 後 reset g_bBufferValid
在 WndProc 的 WM_PAINT 開頭：
```c
// 如果這次 WM_PAINT 的 dirty area 很大，下次不跳過 capture
// 但這次可以跳過（因為是根據上一幀判斷的）
```
**太晚了 — 這次已經跳過了。**

### 方案 D：★ 在 Harbour Refresh 時 reset g_bBufferValid
在 GTWVT 的 hb_gt_wvt_Refresh 中：
```c
if( Harbour 有大量 dirty cell )
   g_bBufferValid = 0;
```
**這太侵入 Harbour 核心了。不可行。**

### ★ 方案 E：在 PaintText 檢查 ps.rcPaint 大小
```c
extern int g_bBufferValid, g_bSharedPaint;
if( g_bSharedPaint )
{
   int dirtyH = ps.rcPaint.bottom - ps.rcPaint.top;
   int totalH = rcClient.bottom;
   if( g_bBufferValid && dirtyH < totalH / 2 )
   {
      /* v5: small dirty area, skip capture */
      hdc = s_hdcPaintBuf;
      bUseMemDC = 2;
   }
   else
   {
      /* Large dirty area or invalid buffer: capture */
      BitBlt( s_hdcPaintBuf ← hdcScreen, ps.rcPaint );
      hdc = s_hdcPaintBuf;
      bUseMemDC = 2;
   }
}
```

### 方案 E 的問題
- 50% 的閾值是任意的
- 小面積 dirty 但 buffer 有完全不同內容的情境仍會閃爍
  （例如 RestScreen 只改了 3 行 → dirty < 50% → 跳過 capture → 閃）

---

## 結論

**v5 的核心矛盾：**
- 只用 dirty area 大小判斷是否跳過 capture 不夠精確
- 真正的判斷條件是「buffer 內容是否與當前畫面一致」
- 這需要逐 cell 比對（太慢）或依賴上層提示（太侵入）

**v5 的正確解法需要更根本的設計變化：**
1. 讓 Harbour GT 通知「發生了大面積畫面切換」
2. 或讓 PaintText 保持 capture 但優化 capture 效能（已經只 capture dirty region）
3. 或接受 v4 的 2 次螢幕操作（1 read + 1 write），足夠好

**建議：v5 列入長期研究，v4 full 為當前最終版。**
