# Handoff Report: Chunk Sizing & Cache Capacity Investigation

**Agent:** `explorer_m2_fix_1`  
**Role:** Chunk Sizing & Cache Capacity Explorer  
**Working Directory:** `c:\Projects\FreeExile\.agents\teamwork\explorer_m2_fix_1`  
**Target Milestone:** Milestone M2 Fix (PoE2 Procedural Tile Map: Mobile-Optimized Tile Rendering)  
**Deliverables Produced:**
- `report.md` (Detailed Technical Analysis)
- `handoff.md` (This Self-Contained Handoff)
- Working test scripts: `test_chunk_capacity.js`, `test_multi_geometry.js`, `test_o1_diamond.js`, `elevation_test.js`, `run_test_8.js`, `run_test_16.js`

---

## 1. Observation

### Obs 1: Baseline LRU Thrashing Mechanism ($16 \times 16$, 4 Slots)
- **Location:** `client/webapp/js/engine/tile_map_renderer.js`, line 76:
  ```javascript
  this.MAX_SLOTS = 4;
  ```
- **Location:** `client/webapp/js/engine/tile_map_renderer.js`, lines 150–152:
  ```javascript
  const destX = Math.round(screenX - 512), destY = Math.round(screenY - 16);
  if (destX + 1024 >= 0 && destX <= vpW && destY + 512 >= 0 && destY <= vpH) {
  ```
- **Empirical Measurement:**
  - On a mobile portrait viewport ($390 \times 844$) on the $120 \times 90$ grid:
    - Visible chunks $\le 4$: only 1,446 coordinates (13.4% of map, corners only).
    - Visible chunks $\ge 5$ (range 5–8): 9,354 coordinates (**86.6% of map**).
  - At $(30, 30)$ (open field): `visibleCount = 8`.
  - Over 50 stationary frames at $(30, 30)$: **250 chunk re-bakes** (5.0 bakes/frame), directly violating `ORIGINAL_REQUEST.md` R2 acceptance criteria: *"Chunk OffscreenCanvas không bị re-render mỗi frame (chỉ re-render khi dirty)"*.

### Obs 2: Empirical Sweep of $8 \times 8$ Chunks Across 10,800 Map Coordinates
- Running `node .agents/teamwork/explorer_m2_fix_1/elevation_test.js`:
  - Canvas dimensions per slot: $512 \times 256\text{ px} = 0.500\text{ MB}$.
  - On $390 \times 844$ viewport:
    - Average visible chunks: **13.10 chunks**
    - Max visible chunks: **17 chunks** (16 with 4px screen edge clamp)
    - Coordinates with $> 12$ visible chunks: **5,994 coordinates (55.5% of map)**
    - Coordinates with $> 14$ visible chunks: **3,646 coordinates (33.8% of map)**
    - Coordinates with $> 16$ visible chunks: **471 coordinates (4.4% of map)**
  - Testing stationary camera with 16 slots (`node .agents/teamwork/explorer_m2_fix_1/run_test_8.js`):
    - At $(10, 10)$: 0 static bakes (PASS)
    - At $(30, 30)$: 0 static bakes (PASS)
    - At $(23, 17)$ without edge clamp: 100 static bakes over 50 frames (FAIL, 17 visible chunks $> 16$ slots)
  - Draw calls per frame for $8 \times 8$ chunks: average **12.83 blits/frame**, peak **16 blits/frame**.

### Obs 3: $O(1)$ Diamond Culling & $16 \times 16$ Chunk Capacity
- Running `node .agents/teamwork/explorer_m2_fix_1/run_test_16.js`:
  - Replaced coarse AABB culling with $O(1)$ Diamond-Metric culling:
    $$\text{dist} = \frac{|x_{clamp} - sx_c|}{R_x} + \frac{|y_{clamp} - sy_c|}{R_y} \le 1.0 + \frac{32}{R_x}$$
  - Visible chunks across 10,800 coordinates on $390 \times 844$:
    - Min: 1 chunk
    - Average: **5.58 chunks/frame** (matches target $\le 6.0$)
    - Max: **8 chunks** (7 chunks in 98.81% of map; 8 chunks in 1.19%)
  - Reconfigured `MAX_SLOTS = 8`:
    - Stationary bakes over 50 frames at $(10, 10)$, $(30, 30)$, $(23, 17)$, $(60, 45)$, $(100, 70)$: **EXACTLY 0 BAKES** (100% PASS).
    - Exhaustive static sweep across all 10,800 coordinates: **0 coordinates with stationary re-bakes** (Target: 0).
    - Canvas + Grid RAM: $8 \times 2,097,152\text{ B} + 10,800\text{ B} = \mathbf{16.010\text{ MB}}$.

### Obs 4: Benchmark Masking & Trajectory Bias
- **Location:** `tools/perf/map_render_benchmark.js`, lines 79–83:
  ```javascript
  let camWx = 10.0, camWy = 10.0;
  for (let f = 0; f < NUM_FRAMES; f++) {
    camWx += 0.08 * Math.cos(f * 0.02);
    camWy += 0.06 * Math.sin(f * 0.02);
  ```
- The camera is trapped in the map corner $[6..14] \times [7..13]$ where chunks are clamped to the boundary ($V \le 4$), masking the thrashing defect that occurs across 86.6% of the map.
- In `MockCanvasContext`, canvas drawing methods (`fill`, `stroke`) are empty no-ops, masking CPU latency of repeated chunk re-baking.

---

## 2. Logic Chain

1. **Step 1 (Geometry Inevitability):** In 2:1 isometric projection, a mobile portrait viewport ($390 \times 844$) has an aspect ratio of 1:2.16 oriented diagonally relative to the tile grid axes. As measured in Obs 1, this geometry intersects 5 to 8 discrete $16 \times 16$ chunks across 86.6% of map coordinates.
2. **Step 2 (Pigeonhole Inevitability of Thrashing):** Because the number of visible chunks $V \in [5..8]$ exceeds cache capacity $C = 4$, at least $V - 4 \in [1..4]$ chunks must be evicted and re-baked every frame. On stationary camera, this forces 5 chunk re-bakes per frame at $(30, 30)$ (Obs 1), violating acceptance criteria.
3. **Step 3 (Refutation of 12–14 Slots for $8 \times 8$ Chunks):**
   - The proposal to use 12–14 slots for $8 \times 8$ chunks assumed $V \le 14$.
   - In Obs 2, because $8 \times 8$ chunks are only $512 \times 256$ pixels, an 844px height spans $844 / 128 = 6.6$ chunk lattice steps.
   - At least 15 to 17 chunks are visible across 33.8% to 55.5% of the map.
   - Therefore, 12 to 14 slots does NOT prevent thrashing; it thrashes on one-third to one-half of the game map.
4. **Step 4 (Feasibility of $8 \times 8$ at 16 Slots):**
   - At 16 slots, $8 \times 8$ chunks consume $16 \times 0.5\text{ MB} = 8.00\text{ MB}$ ($8.010\text{ MB}$ with grid data), satisfying the $\le 8.02\text{ MB}$ budget.
   - However, $8 \times 8$ chunks inflates draw calls to $12.8$ blits/frame (Obs 2), which fails the benchmark ceiling of $\le 6.0$ blits/frame.
5. **Step 5 (Feasibility of $16 \times 16$ at 8 Slots with Diamond Culling):**
   - In Obs 3, $O(1)$ Diamond Culling strictly caps visible $16 \times 16$ chunks to $\le 8$ across 100% of the map.
   - Allocating `MAX_SLOTS = 8` ensures $C \ge V$ everywhere, completely eliminating per-frame evictions (0 stationary re-bakes).
   - Draw calls remain at $5.58$ blits/frame ($\le 6.0$ blits).
   - Memory is $16.010\text{ MB}$, which requires relaxing the arbitrary 8.0 MB budget to $\le 16.02\text{ MB}$.
6. **Step 6 (Synthesis & Recommendation):**
   - Option B ($16 \times 16$ with `MAX_SLOTS = 8`) is the superior ARPG engineering choice because it preserves PoE2 chunk architecture (`ORIGINAL_REQUEST.md` line 221), matches server spatial chunking in Python tests (`test_poe2_map_system_e2e.py`:281), and maintains low draw calls ($\le 6$ blits).
   - Option A ($8 \times 8$ with `MAX_SLOTS = 16`) is the exact mathematical solution if $\le 8.02\text{ MB}$ RAM is an inviolable hard constraint.

---

## 3. Caveats

1. **Hardware Canvas vs Node.js Mock:** Benchmarks run in Node.js where canvas drawing calls are stubbed. In mobile WebKit (Safari) and Chrome, 2D canvases allocate $W \times H \times 4$ bytes of uncompressed backing store memory. A 16.0 MB backing store represents $< 0.4\%$ of RAM on a 4GB/6GB/8GB iPhone or Android device, posing zero OOM risk.
2. **Elevation Margin Assumption:** The $O(1)$ Diamond culling formula incorporates a 32px margin ($1.0 + 32/R_x$) to account for FreeExile's maximum tile vertical elevation extrusion (16px for WALL, 18px for BOSS_GATE). This was verified with 0 false negatives across all 10,800 coordinates.
3. **No Codebase Mutation:** Adhering strictly to explorer read-only constraints, no production files were modified. All prototypes and tests are located in `.agents/teamwork/explorer_m2_fix_1/`.

---

## 4. Conclusion & Actionable Recommendations

### Recommendation for Worker M2:

| Parameter | Recommended Path (PoE2 Specification Preservation) | Alternative Path (Strict 8.0 MB Budget) |
| :--- | :--- | :--- |
| **`CHUNK_SIZE`** | **`16`** | **`8`** |
| **`CHUNK_PIXEL_W`** | **`1024`** | **`512`** |
| **`CHUNK_PIXEL_H`** | **`512`** | **`256`** |
| **`MAX_SLOTS`** | **`8`** | **`16`** |
| **Culling Algorithm** | **$O(1)$ Diamond-Metric Culling** | **$O(1)$ Diamond-Metric Culling** |
| **Static Camera Re-bakes** | **`0` (Guaranteed 100%)** | **`0` (Guaranteed 100%)** |
| **Active Canvas + Grid RAM** | **`16.010 MB`** | **`8.010 MB`** |
| **RAM Budget Threshold** | Update to **`<= 16.02 MB`** | Retain **`<= 8.02 MB`** |
| **Draw Calls / Frame** | **`4 to 8 blits (avg 5.58)`** | **`10 to 16 blits (avg 12.8)`** |
| **Draw Calls Budget** | Retain **`<= 6.0 blits`** | Update to **`<= 14-16 blits`** |
| **PoE2 Request Alignment** | Exact match (`ORIGINAL_REQUEST.md`) | Diverges from 16x16 request |
| **Server/Python Alignment** | Exact match (`test_poe2_map_system_e2e.py`) | Decoupled from Python chunk size |

### Required Code Changes in `client/webapp/js/engine/tile_map_renderer.js`:
1. Line 76: Update `this.MAX_SLOTS = 8;`
2. Lines 145–159: Replace coarse AABB culling with $O(1)$ Diamond-Metric culling:
   ```javascript
   const Rx = this.CHUNK_SIZE * 32;
   const Ry = this.CHUNK_SIZE * 16;
   const csHalf = (this.CHUNK_SIZE - 1) * 0.5;

   this.visibleCount = 0;
   for (let cy = minCy; cy <= maxCy; cy++) {
     for (let cx = minCx; cx <= maxCx; cx++) {
       const wx_c = cx * this.CHUNK_SIZE + csHalf;
       const wy_c = cy * this.CHUNK_SIZE + csHalf;
       const sx_c = (wx_c - wy_c - (camX - camY)) * 32 + halfVpW;
       const sy_c = (wx_c + wy_c - (camX + camY)) * 16 + halfVpH;

       const clampX = Math.max(0, Math.min(vpW, sx_c));
       const clampY = Math.max(0, Math.min(vpH, sy_c));
       const diamondDist = Math.abs(clampX - sx_c) / Rx + Math.abs(clampY - sy_c) / Ry;

       if (diamondDist <= 1.0 + (32 / Rx)) {
         const relWx = (cx * 16) - camX, relWy = (cy * 16) - camY;
         const screenX = (relWx - relWy) * 32 + halfVpW, screenY = (relWx + relWy) * 16 + halfVpH;
         const destX = Math.round(screenX - 512), destY = Math.round(screenY - 16);

         if (this.visibleCount < this.visibleScratch.length) {
           const vs = this.visibleScratch[this.visibleCount++];
           vs.cx = cx; vs.cy = cy; vs.key = (cy << 16) | cx; vs.destX = destX; vs.destY = destY;
         }
       }
     }
   }
   ```
3. Update `tools/perf/map_render_benchmark.js`:
   - Set `MAX_RAM_MB = 16.02;`
   - Update camera trajectory to traverse across the open field (e.g. from $(10, 10)$ to $(90, 60)$).
   - Assert `staticBakes === 0` on stationary frames after warm-up.

---

## 5. Verification Method

To independently verify all findings and validate the fix:

1. **Verify 16-Chunk Thrashing Defect on $8 \times 8$ Chunks with 12–14 Slots:**
   ```bash
   node .agents/teamwork/explorer_m2_fix_1/elevation_test.js
   ```
   *Expected Output:* Proves 3,646 coordinates have $> 14$ visible chunks and 5,994 coordinates have $> 12$ visible chunks.

2. **Verify 100% Zero-Bake Invariant with $16 \times 16$ Chunks and 8 Slots:**
   ```bash
   node .agents/teamwork/explorer_m2_fix_1/run_test_16.js
   ```
   *Expected Output:*
   - All 5 sample positions: `staticBakesOver50Frames=0 -> PASS (0 BAKES)`.
   - Full 10,800 coordinate sweep: `Total coordinates with stationary re-bakes: 0`.
   - Active Canvas + Grid RAM: `16.010 MB`.

3. **Verify Existing Python Test Suite Remains Passing:**
   ```bash
   pytest tests/e2e/test_poe2_map_system_e2e.py tests/unit/test_war_fog_and_procedural_map.py -v
   ```
   *Expected Output:* All tests pass without regression.

**Invalidation Conditions:**
- This report's recommendation can be invalidated if:
  1. A mathematical proof demonstrates that $V \le 4$ on an uncropped $390 \times 844$ viewport without clipping on-screen tiles.
  2. A viable 2D canvas compression mechanism reduces individual unrotated $16 \times 16$ isometric chunk canvas memory below $1.0\text{ MB}$ without downscaling blur or CPU clipping overhead.
