# Plan: Keep MapWindow Responsive Under Heavy GE Load

## Context

The MapWindow DLL shares HC's main UI thread. HC's game loop in `Command.c` does: pump messages → game logic → `dllexGameTime()` → repeat. The exported `GameTime()` function runs synchronously — it walks all groups, updates every TGlobe4 object (icons, arrows, range rings, tracks, uncertainty diamonds), then returns. During this time, the form can't process mouse/keyboard/paint messages. Under heavy load (many groups, complex scenarios), the MapWindow feels frozen.

**Key insight from HC's `Command.c`:** When `DLLBusy` is TRUE, HC breaks out of game processing and runs `PeekMessage/DispatchMessage` + `Sleep(1)`. So messages ARE pumped between ticks — the problem is specifically that `GameTime()` itself blocks without yielding.

**Goal:** Make GameTime() return nearly instantly so the form stays responsive. Do all heavy work (updating TGlobe4 objects) outside of GameTime, driven by the Windows message pump.

---

## Approach: Snapshot in GameTime, Apply via PostMessage

### How it works

1. **GameTime()** copies group data from HC's linked list into a DLL-owned snapshot buffer (flat Pascal records, no HC pointers retained). This is fast — just copying struct fields.
2. **GameTime()** posts a `WM_APPLY_SNAPSHOT` message to `frmMain.Handle`, then returns immediately.
3. **The form's message handler** picks up `WM_APPLY_SNAPSHOT` on the next pump cycle and runs the existing mark/update/sweep logic against the snapshot data.
4. If a new GameTime fires before the previous snapshot is applied, the new snapshot overwrites the old (latest data wins).

### Why this works

- All TGlobe4 interaction stays on the main thread (via message handler) — no LCL threading issues
- PHCGroup pointers are only dereferenced during the snapshot copy (within GameTime, while pointers are valid)
- The form processes mouse/paint messages freely between snapshot applications
- No threading primitives needed — single-threaded, event-driven

### What about unparenting the window?

Several HC ExportDLLs (ActUnitReport, AutoSaves, lazTurncoat) already comment out `ParentWindow := hwndHC` with notes like "let it float free". This makes the form a top-level window instead of embedded in HC.

**However, unparenting alone does NOT fix responsiveness.** The form is created on HC's main thread (during `DLLInit`), so it shares HC's message queue regardless of whether it's parented. Moving the form to a separate thread isn't viable because LCL is not thread-safe and Win32 forbids cross-thread parent-child window relationships.

**Unparenting is still worth doing as a complementary change:** a floating window feels more independent to the user, and combined with the snapshot approach it means the MapWindow truly behaves as its own application — it just happens to be hosted in HC's process.

### Optional: Comment out ParentWindow (Step 0)

In `MapWindow.lpr` `DLLInit`, change:
```pascal
frmMain.ParentWindow := hwndHC;
```
to:
```pascal
// frmMain.ParentWindow := hwndHC; // CC 20260323 let MapWindow float as independent window
```

This is a one-line change with no code risk. The form becomes a separate top-level window. Combined with the snapshot approach, the MapWindow is fully decoupled from HC's UI.

### Why not other approaches

| Approach | Why rejected |
|---|---|
| Unparented window alone | Still shares HC's thread and message queue — doesn't fix blocking during GameTime |
| Separate thread for form | LCL is not thread-safe; cross-thread `ParentWindow` is forbidden by Windows |
| Background thread + Synchronize | The bottleneck is TGlobe4 API calls (ObjectByID scans, chain manipulation), which are LCL-bound and can't be offloaded |
| Chunked processing within GameTime | PHCGroup linked list is only valid during the call; PDLLBusy blocks HC but doesn't pump messages during GameTime |
| Application.ProcessMessages mid-loop | Re-entrancy risk: user events (drag, coursepoint ops) modify FGroupMap while iterating |

---

## Files Modified

### 1. `uGroupSnapshot.pas` (NEW)

Snapshot record types and capture procedure:

```pascal
type
  TTrackNodeSnapshot = record
    DestLon, DestLat: LongInt;
  end;

  TGroupSnapshot = record
    UUID: string;
    sLon, sLat: LongInt;
    course: SmallInt;
    kts: SmallInt;
    side: uchar;
    showing: uchar;
    gtype: HCMT;
    id: string;
    BestWeaponsRange: array[0..1] of SmallInt;
    TrackNodes: array of TTrackNodeSnapshot;  // deep copy of PHCPathRec chain
    HasUncertainty: Boolean;
    UncertSLon: array[0..3] of LongInt;
    UncertSLat: array[0..3] of LongInt;
  end;

  TGameTimeSnapshot = record
    GameTime: LongInt;
    GameTimeLeft: LongInt;
    Groups: array of TGroupSnapshot;
    Count: Integer;
    Ready: Boolean;
  end;

procedure CaptureGroupSnapshot(const pGroup: PHCGroup; out snap: TGroupSnapshot);
procedure CaptureGameTimeSnapshot(pStartGroup: PHCGroup;
  gameTime, gameTimeLeft: LongInt; var snap: TGameTimeSnapshot);
```

`CaptureGroupSnapshot`: copies flat fields + walks `pGroup^.track` linked list into `TrackNodes` dynamic array + copies `UncertaintyRegion` corners if non-nil.

`CaptureGameTimeSnapshot`: walks the group linked list, calls `CaptureGroupSnapshot` for each, sets `Ready := True`.

### 2. `MapWindow.lpr`

**Rewrite `GameTime()`** to snapshot-and-post:

```
procedure GameTime(GameTime, GameTimeLeft)
begin
  PDLLBusy := CTRUE;

  // Update GroupPtr on existing FGroupMap entries (for interactive use between ticks)
  walk linked list:
    if FGroupMap.Find(UUID) then entry^.GroupPtr := pGroup;

  // Capture snapshot
  CaptureGameTimeSnapshot(PStartGroup, GameTime, GameTimeLeft, frmMain.FSnapshot);

  // Trigger deferred apply
  PostMessage(frmMain.Handle, WM_APPLY_SNAPSHOT, 0, 0);

  PDLLBusy := CFALSE;
end;
```

The GroupPtr update loop is kept because interactive features (waypoint drag, coursepoint append/insert/delete) need the live pointer between ticks.

### 3. `mainform.pas`

**Add fields:**
```pascal
FSnapshot: TGameTimeSnapshot;        // latest snapshot from GameTime
```

**Add constant:**
```pascal
const WM_APPLY_SNAPSHOT = WM_USER + 100;
```

**Add message handler:**
```pascal
procedure WMApplySnapshot(var Msg: TMessage); message WM_APPLY_SNAPSHOT;
```

**`WMApplySnapshot` implementation:** This is essentially the current `GameTime` phases 1-3 (mark/update/sweep), moved from `MapWindow.lpr` into the form. The difference is it reads from `FSnapshot.Groups[i]` instead of `PHCGroup` pointers. Calls the same `AddGroup`/`UpdateGroup`/`RemoveGroupEntry` methods (see step 4 for their adaptation).

**Guard interactive operations:** Add `if not FSnapshot.Ready then Exit;` check at top of WMApplySnapshot. In `ScenLoad`, set `FSnapshot.Ready := False` before `ClearAllGroups` to discard any pending snapshot.

### 4. Adapt AddGroup / UpdateGroup to work from snapshot data

Currently `AddGroup(pGroup: PHCGroup, pShowing: uchar)` takes a live HC pointer. Two options:

**Option A (minimal change):** Add overloaded versions that take `TGroupSnapshot` and internally extract the same fields. The existing helper units (uGroupIcon, uArrows, etc.) already take individual parameters (side, position, color) rather than PHCGroup — the main form methods are the ones that unpack the record.

**Option B (refactor signatures):** Change `AddGroup`/`UpdateGroup` to take individual fields instead of PHCGroup. This is more surgical but touches more code.

**Recommendation: Option A.** Add `AddGroupFromSnapshot` and `UpdateGroupFromSnapshot` wrappers that unpack the snapshot record and call the same helper unit procedures. The existing `AddGroup`/`UpdateGroup` can be kept for reference or removed.

### 5. `uTracks.pas`

Add `BuildTrackChainFromSnapshot` that takes an array of `TTrackNodeSnapshot` + group position instead of walking a `PHCPathRec` linked list:

```pascal
procedure BuildTrackChainFromSnapshot(
  groupSLon, groupSLat: LongInt;
  const aNodes: array of TTrackNodeSnapshot;
  nodeCount: Integer;
  pLineChain, pDotChain: TPointStore);
```

Same great-circle interpolation logic, just reads from the flat array instead of a linked list.

### 6. `uUncertainty.pas`

Add an overloaded `UpdateUncertaintyZone` or a thin wrapper that takes flat `sLon[0..3]`/`sLat[0..3]` arrays instead of `PURegion`. Alternatively, the apply handler can construct a local `URegion` record on the stack from the snapshot's flat arrays and pass it to the existing function.

### 7. `MapWindow\MapWindow.txt`

Add history entry.

---

## GroupPtr Lifecycle (critical detail)

`PGroupMapEntry.GroupPtr` is used extensively for interactive features:
- **Waypoint drag** — reads/writes `GroupPtr^.track` linked list nodes
- **Coursepoint append/insert/delete** — modifies `GroupPtr^.track`
- **HitTestWaypointDot** — walks `GroupPtr^.track`
- **GroupCourseVisible** — reads `GroupPtr^.side` (now `DispSide`, but still accessed)
- **Context menu** — checks `GroupPtr^.track <> nil`

These all happen between ticks (user-initiated), when the HC pointer IS valid. The snapshot `GameTime()` still updates `GroupPtr` on existing entries during the snapshot phase. New entries created during `WMApplySnapshot` won't have a valid `GroupPtr` — but they don't need one until the next `GameTime` sets it.

**Guard:** Set `lEntry^.GroupPtr := nil` for newly added entries in `WMApplySnapshot`. Interactive operations already check for nil.

---

## Edge Cases

| Scenario | Handling |
|---|---|
| GameTime fires before previous snapshot applied | New snapshot overwrites; only latest is applied |
| ScenLoad fires with pending snapshot | `FSnapshot.Ready := False` before `ClearAllGroups` |
| User drags waypoint during snapshot apply | WMApplySnapshot is synchronous on the main thread; user events interleave at message pump boundaries, not mid-handler |
| DLL unload during pending snapshot | `FormDestroy` — snapshot is a value record on the form, freed with it |
| Zero groups in scenario | Snapshot has `Count = 0`; sweep phase removes all existing entries |

---

## Verification (Option A)

1. `lazbuild --build-all MapWindow.lpi` — compiles without errors
2. Load small scenario (< 20 groups) — groups appear and move correctly, identical to before
3. Load large scenario (100+ groups) — **form responds to mouse/zoom while groups update**
4. Waypoint drag during heavy scenario — drag remains smooth
5. Coursepoint append/insert/delete — works correctly between ticks
6. Scenario transitions — old groups clear, new ones appear
7. Toggle Player/God mode — visibility updates on next tick
8. Rapid scenario changes — no stale data or crashes

---
---

# Option B: Dedicated Form Thread

## Context

Option A assumes GameTime itself is the bottleneck. If HC's thread is saturated with its own work (game logic, other DLLs, rendering), then even between GameTime calls the message pump doesn't get enough time to service MapWindow's paint/mouse messages. In that case, the form needs its own thread with its own message loop.

**Option B builds on Option A** — the snapshot decoupling is a prerequisite. Option B then moves the snapshot consumer (the form) to its own thread.

---

## Thread Model

| Thread | Owns | Responsibilities |
|---|---|---|
| **HC's thread** | DLL exports, HC callbacks | `DLLInit` (spawn form thread), `GameTime` (snapshot + post), `ScenLoad`, `DLLFree` (join) |
| **Form thread** | `TfrmMain`, TGlobe4, all layers | Message loop, paint, mouse/keyboard, snapshot application, visual updates |

### Why this is safe

- LCL does not check `MainThreadID` — confirmed by grep of the entire LCL Win32 widgetset
- Win32 allows window creation on any thread; each thread has its own message queue
- All LCL/TGlobe4 interaction stays on the form thread — no cross-thread UI access
- HC never calls LCL — it's a pure C/Win32 application
- Communication is via critical sections + `PostMessage` (explicitly cross-thread safe in Win32)

---

## Additional Files (beyond Option A)

### `uFormThread.pas` (NEW)

```pascal
type
  TFormThread = class(TThread)
  private
    FReadyEvent: THandle;  // manual-reset event
  protected
    procedure Execute; override;
  public
    constructor Create;
    function WaitReady(timeoutMs: DWORD): Boolean;
  end;
```

`Execute`:
1. `Application.Initialize` (registers LCL window classes on this thread)
2. `frmMain := TfrmMain.Create(nil)` — floating, NOT parented to HC
3. Apply caption, show form
4. Signal `FReadyEvent`
5. Enter message loop: `while GetMessage(msg, 0, 0, 0) > 0 do begin TranslateMessage(msg); DispatchMessage(msg); end;`

If `Application.Initialize` proves problematic on a non-main thread, fallback: skip it, use raw `CreateWnd` + `GetMessage` loop. TGlobe4 just needs a window with a message pump.

### `uThreadComm.pas` (NEW)

Shared state between threads:

```pascal
var
  gSnapshotCS: TRTLCriticalSection;     // guards gSharedSnapshot
  gSharedSnapshot: TGameTimeSnapshot;    // HC thread writes, form thread reads
  gPendingCS: TRTLCriticalSection;       // guards gPendingRequests
  gPendingRequests: array of THCCallbackRequest;
  gFormHWND: HWND;                       // for PostMessage to form thread
  // Relocated globals (previously on TfrmMain or in mainform.pas):
  gPDLLBusy: Pointer;
  gPStartGroup: PHCGroup;
  gNewPtr: THCNewPtr;
  gDisposPtr: THCDisposPtr;
  gHCQueueHarpoonEvent: THCQueueHarpoonEvent;
```

---

## Changes to Option A Files

### `MapWindow.lpr`

**DLLInit** (replaces direct form creation):
1. Store HC callbacks in `uThreadComm` globals (not on `TfrmMain`)
2. `InitCriticalSection(gSnapshotCS)` / `InitCriticalSection(gPendingCS)`
3. Create `TFormThread`, wait for ready signal (5s timeout)
4. `gFormHWND := frmMain.Handle`

**GameTime** (replaces Option A's direct PostMessage):
1. `PDLLBusy^ := CTRUE`
2. `ExecutePendingHCCallbacks` — pop queued requests from form thread, execute on HC's thread using `NewPtr`/`DisposPtr`
3. `EnterCriticalSection(gSnapshotCS)` → `CaptureGameTimeSnapshot` → `LeaveCriticalSection`
4. `PostMessage(gFormHWND, WM_APPLY_SNAPSHOT, 0, 0)`
5. `PDLLBusy^ := CFALSE`

**ScenLoad:** `SendMessage(gFormHWND, WM_SCEN_LOAD, ...)` — synchronous, waits for form thread to clear.

**DLLFree:** `PostMessage(gFormHWND, WM_CLOSE, 0, 0)` → `WaitForSingleObject(thread, 5000)` → cleanup.

### `mainform.pas`

- HC callback fields (`NewPtr`, `DisposPtr`, etc.) removed from `TfrmMain` — moved to `uThreadComm` globals
- `WMApplySnapshot`: acquire `gSnapshotCS`, copy snapshot to local, release, then run mark/update/sweep
- Interactive operations **queue requests** via `gPendingCS` instead of calling `NewPtr`/`DisposPtr` directly

### `uWaypointDrag.pas`

- Replace `PathRec: PHCPathRec` in `TWaypointDragState` with `WaypointIdx: Integer` + `GroupUUID: string`
- `WaypointDragCommit`: queue request instead of writing `PathRec^.DestLon/DestLat` directly

---

## HC Callback Marshaling

HC callbacks (`NewPtr`, `DisposPtr`, `QueueHarpoonEvent`) are function pointers into HC's single-threaded code. They MUST run on HC's thread.

**Solution:** Pending request queue.

```pascal
type
  THCCallbackKind = (cbkAppendCoursepoint, cbkInsertCoursepoint,
    cbkDeleteCoursepoint, cbkClearCourse, cbkDragCommit);

  THCCallbackRequest = record
    Kind: THCCallbackKind;
    GroupUUID: string;
    DestLon, DestLat: LongInt;
    InsertBeforeIdx: Integer;
    DeleteIdx: Integer;
    DragWaypointIdx: Integer;
  end;
```

- **Form thread** (user interaction): push request onto queue under `gPendingCS`
- **HC thread** (next `GameTime`, step 2): pop all, find group by UUID, execute `NewPtr`/`DisposPtr`
- **Latency:** ~1 tick (0.1–1s). Form thread can show optimistic visual feedback.

---

## Risk Assessment (Option B specific)

| Risk | Severity | Mitigation |
|---|---|---|
| `Application.Initialize` on worker thread | HIGH | Test first. Fallback: raw Win32 `CreateWindow` + `GetMessage` loop. |
| TGlobe4 internal thread assumptions | HIGH | Third-party component may assume main thread. Must test. Fallback: Option A alone is still valuable. |
| HC callback thread safety | MEDIUM | Solved by queuing — all HC calls execute on HC's thread. |
| Interactive latency (1 tick) | LOW | Acceptable. Optimistic visual feedback possible. |
| Form not parented to HC | LOW | Already done by other HC DLLs. |
| `PostMessage` after form destroyed | MEDIUM | Set `gFormHWND := 0` in `FormDestroy`, guard all posts. |

---

## Implementation Sequence (both options)

1. **Implement Option A** (snapshot decoupling) — verify it works standalone
2. **Test responsiveness** — if GameTime was the bottleneck, Option A may be sufficient
3. If HC's thread is still the bottleneck, proceed to Option B:
   a. Create `uThreadComm.pas` — relocate globals, add critical sections
   b. Create `uFormThread.pas` — test that form appears on worker thread
   c. Wire `GameTime` snapshot under critical section + `PostMessage`
   d. Implement HC callback queue + `ExecutePendingHCCallbacks`
   e. Adapt interactive features to use queue
   f. Handle `ScenLoad` / `DLLFree` cross-thread lifecycle

## Verification (Option B)

1. `lazbuild --build-all MapWindow.lpi` — compiles
2. Small scenario — groups appear and update, form is in separate window
3. Large scenario — **form pans/zooms smoothly even while HC is under heavy load**
4. Waypoint drag — visual preview is smooth, commit takes effect next tick
5. Coursepoint append/insert/delete — queued, applied next tick
6. Scenario transitions — clean clear + reload
7. `DLLFree` — clean shutdown, no hangs or crashes
