diff --git a/CLAUDE.md b/CLAUDE.md index 0438fb8b..1073d391 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,14 +67,28 @@ offset, so both axes scroll pixel-smooth with no per-pixel bit decode in the hot path. **Any new routine that writes VRAM tile data must set `g_tilecache_dirty`** (the existing VRAM loaders already do). -**Temporary scaffold — `DrawTileBlock` out-of-range block clamp:** the extended -40×25-tile viewport draws a larger area than the original 20×18, so the camera -can reach into uninitialized `wOverworldMap` padding and read block IDs past the -embedded blockset. `DrawTileBlock` (`src/overworld/overworld.asm`) currently -clamps such IDs to block 0 to avoid painting garbage. This is a stopgap: the -plan is to **extend the map data** so those regions hold real blocks (no blank -area), after which the clamp is dead code and should be deleted. See TODO.md -(Phase 2). +**Temporary scaffold — two out-of-map clamps (`src/overworld/overworld.asm`):** +the extended 40×25-tile viewport draws a larger area than the original 20×18 and +the player is pinned at screen-center, so a player-centered camera near a map +edge reaches past the populated `wOverworldMap` data. Two complementary stopgaps +keep that from painting garbage: +1. **Block-ID clamp** in `DrawTileBlock`: a block ID past the embedded blockset + is clamped to block 0. +2. **Block-map address clamp** in `LoadCurrentMapView`: `wOverworldMap` ($E580) + sits directly above `wSurroundingTiles` ($E000) in WRAM, so a view pointer + that reaches above the map's top border reads *tile* IDs from the surrounding + buffer and decodes them as *block* IDs → a garbage band (seen at map edges and + the instant a connection is crossed). Any read outside + `[wOverworldMap, wOverworldMapEnd)` instead yields the map's border block + (`wMapBackgroundTile`), so the out-of-map area renders as clean dummy tiles + matching the in-bounds border. + +Both are stopgaps: the real fix is to **extend the map data** so those regions +hold real blocks (no blank area), after which both clamps are dead code and +should be deleted. The address clamp removes the garbage *now* (verified via +`FRAME.BIN` for baseline / north-transition / walk-to-edge); it does **not** yet +give editable map cells for that extended area — that still needs the map-data +extension (enlarged border / bigger block grid). See TODO.md (Phase 2). --- @@ -301,11 +315,31 @@ DOSBox-X (2026.06.02, SDL1) can also be built/run with the heavy debugger offset = `[ds_base] + EBP + offset`; both are runtime values, so the file-dump route above is usually faster than chasing them in the debugger. +### Back-buffer dump to PNG (preferred over screenshots) + +`src/debug/debug_dump.asm:DumpBackbuffer` writes the full software-PPU back +buffer (`GB_BACKBUF`, 320×200 = 64000 raw palette-indexed bytes) to `FRAME.BIN`, +then exits — the **exact pixels DOSBox-X rendered**, with no compositor in the +loop (host Wayland/XWayland screenshot tools are unreliable across displays). +Render `FRAME.BIN` on the host with `tools/render_frame.py FRAME.BIN out.png` +(values 0–3 = DMG shades, 4–11 = sprite pixels), then view the PNG. +Driven by deterministic, input-free `%ifdef` harnesses in `EnterMap`: +`DEBUG_TRANSITION` (force a north crossing; add `DEBUG_BASELINE=1` — both via the +Makefile — for pristine Pallet Town) and `DEBUG_WALK_NORTH` (drive the real +movement primitives north `DEBUG_WALK_STEPS` steps, dumping at the crossing). +Typical loop: `make clean && make SKIP_TITLE=1 DEBUG_TRANSITION=1` → +`dosbox-x -defaultdir "$PWD" -c 'mount c "'"$PWD"'"' -c c: -c PKMN.EXE -c exit` → +`python3 ../tools/render_frame.py FRAME.BIN /tmp/f.png`. This is how the +2026-06-15 viewport diagnosis and the 2026-06-16 out-of-map clamp fix were made +(see docs/loadmapheader_handoff.md). Prefer this to screenshots for ground truth. + ### Visual capture `./test_render.sh [out.png]` does a clean `SKIP_TITLE=1` build, launches DOSBox-X, waits, screenshots (spectacle → import fallback), and force-kills. -Good for confirming a final render once the data is known-correct. +Good for confirming a final render once the data is known-correct. Note: under a +Wayland session the compositor screenshot may grab the wrong window — the +`FRAME.BIN` route above is more reliable. --- diff --git a/Research.md b/Research.md deleted file mode 100644 index b6c41061..00000000 --- a/Research.md +++ /dev/null @@ -1,34 +0,0 @@ -# Memory Model Optimization Research - -## Proposed Plan Change: Static Offset Simulation - -Instead of using `EBP` as a dynamic base pointer for the emulated Game Boy memory map, we can declare the entire 72 KB Game Boy address space as a single contiguous array directly in the native `.bss` section (e.g., `gb_memory: resb 72 * 1024`), and use **absolute 32-bit addressing** (e.g., `[gb_memory + 0xC000]`). - -### Why this is faster: - -1. **Frees `EBP` (Massive Win):** The x86 architecture is famously register-starved. Freeing `EBP` from its base-pointer duties gives you a 7th general-purpose register. This perfectly aligns with `docs/386_optimization_strategy.md`, which states: *"Cache intermediate values in registers (like EAX, ECX, EDX, EBX, ESI, EDI) during complex calculations."* Now we can add `EBP` to that list. -2. **Smaller Instruction Sizes:** Accessing memory via `[EBP + disp32]` takes 6 bytes to encode (opcode + ModR/M + 32-bit displacement). Using absolute addressing `[gb_memory + 0xC000]` only takes 5 bytes. Smaller machine code means better instruction cache density, which is critical for 386 performance. -3. **Hardware-Accelerated Pointer Math:** If a GB pointer is held in a register (like `EBX`), translating it currently requires `[EBP + EBX]`. With the new model, we can use `[EBX + gb_memory]`, which the 386 ALU handles natively as a standard index+displacement operation with zero extra overhead. - -### Why this preserves accuracy (Bug Preservation): - -Because we are still allocating a single contiguous 72 KB block in `.bss`, the **relative distances** between all memory structures remain identical to the original hardware. If a critical glitch causes an out-of-bounds write that overruns `wSpriteStateData1` by 150 bytes, it will still land exactly inside `wSpriteStateData2`, just as it would on the original Game Boy. ACE (Arbitrary Code Execution) will also function identically because the raw binary payload will still land at the expected offsets. - ---- - -## Future Implementation Tasks - -When the codebase is ready for this refactor, the following documents and systems will need to be updated: - -1. **`README.md` (Hard Conventions):** - * Remove `EBP` from the reserved register list and mark it as a general-purpose register. - * Update the "Memory Model" section to specify a static `.bss` flat allocation rather than a dynamic DPMI EBP-relative allocation. - -2. **`ROADMAP.md` (Phase 1):** - * Change *"GB memory model live: 72 KB DPMI allocation, EBP-relative access working"* to *"GB memory model live: 72 KB static .bss allocation, absolute 32-bit addressing working"*. - -3. **`dos_port/include/gb_memmap.inc`:** - * Modify the comments and macros so that variables resolve to `gb_memory + offset` rather than relying on `EBP`. - -4. **`docs/glitch_safety.md`:** - * Update the DPMI Protection Details to clarify that glitch safety is enforced by CWSDPMI's flat data selector segment limits over the `.bss` section, rather than bounded `EBP` offsets. diff --git a/TODO.md b/TODO.md index 3d6b3ddb..5d95cedc 100644 --- a/TODO.md +++ b/TODO.md @@ -124,14 +124,23 @@ Prioritized task list. Check off items as they complete; add new items with phas Awaiting: first session to start with Step 1 (geometry constants). -- [ ] **Extend map data to cover the extended-draw region, then remove the - `DrawTileBlock` out-of-range block clamp.** The 40×25-tile viewport draws - a larger area than the original 20×18, so the camera can reach into - uninitialized `wOverworldMap` padding and read block IDs past the embedded - blockset. A temporary clamp (block ID → 0 when out of range) in - `DrawTileBlock` (src/overworld/overworld.asm) stops the garbage. The real - fix is to extend the maps so those regions hold real blocks (no blank - area); once done, delete the clamp — it becomes dead code. +- [ ] **Extend map data to cover the extended-draw region, then remove the two + out-of-map clamps.** The 40×25-tile viewport draws a larger area than the + original 20×18 and the player is screen-centered, so the camera can reach + past the populated `wOverworldMap` data near map edges. Two temporary + clamps in `src/overworld/overworld.asm` stop the garbage: + (1) `DrawTileBlock` clamps a block ID past the blockset → block 0; + (2) `LoadCurrentMapView` (added 2026-06-16) substitutes `wMapBackgroundTile` + for any block-map read outside `[wOverworldMap, wOverworldMapEnd)` — these + reads otherwise fall into `wSurroundingTiles` ($E000, directly below + `wOverworldMap` at $E580) and decode tile IDs as block IDs. + The address clamp removes the garbage now (dummy/border tiles in the + out-of-map area) but does NOT create editable cells there. The real fix is + to extend the maps so those regions hold real blocks: enlarge `MAP_BORDER` + / the block grid and regenerate `assets/map_headers.inc` with per-direction + `_win`/`_y`/`_x` re-tuned for the larger border (the formulas in + `tools/gen_map_headers.py` are hand-tuned per direction — re-verify each + empirically with the FRAME.BIN loop). Once done, delete both clamps. - [ ] **BUG — collision: facing DOWN lets the player penetrate 1 tile into objects** (signs, building roofs/fronts) when approached from the top. Confirmed NOT a graphical artifact (user, 2026-06-15). Root cause: diff --git a/docs/Native-width BG renderer (retire the 256px torus)_current.md b/docs/Native-width BG renderer (retire the 256px torus)_current.md new file mode 100644 index 00000000..53e1a143 --- /dev/null +++ b/docs/Native-width BG renderer (retire the 256px torus)_current.md @@ -0,0 +1,204 @@ +# Plan: Native-width BG renderer (retire the 256px torus), then border + connections + +## Context + +The DOS port renders the overworld at a **40×25-tile / 320×200** viewport, but +the BG renderer (`src/ppu/ppu.asm:render_bg`) still emulates the Game Boy's +**32×32-tile / 256px** VRAM tilemap as a 256×256 torus surface and blits a +320px-wide window from it **with 256px wrap**. Because 320 > 256, every scanline +wraps the seam, forcing screen columns 256–319 to mirror columns 0–63. + +**Verified empirically (2026-06-16):** on a baseline frame, columns 256–319 were +**98.6% pixel-identical** to columns 0–63 (residual = the player sprite drawn on +top); a 100–163 vs 0–63 control was 46% different. Deterministic +`DEBUG_WALK_NORTH DEBUG_WALK_STEPS=1/2/3` captures show top-row garbage after a +**single** step — while the camera is still well inside the map, so it is **not** +an out-of-bounds map read. Root cause is purely the renderer's 256px torus plus +the `CopyMapViewToVRAM`/`FillExtraVRAMRows` "32 tiles + 8 wrapped onto cols 0–7" +fold and the SCY-wrap that reads stale VRAM rows 25–31. + +The earlier 2026-06-16 out-of-map clamp in `LoadCurrentMapView` +(read outside `[wOverworldMap,wOverworldMapEnd)` → `wMapBackgroundTile`) fixed a +**separate** real bug (edge/transition OOB block reads) and **stays**. + +**Intended outcome:** a port-native BG renderer that shows 40 distinct tile +columns with smooth 2px scrolling and no wrap duplication, then (Stage B/C) map +data + connection geometry sized so the centered viewport never reads past the +map and transitions place the player correctly. + +**Decisions locked with the user:** +- Architecture: **native oversized surface + plain pixel-offset blit** (no torus). +- Scope: renderer **and** border **and** connections, executed as 3 sequential, + independently-verifiable stages; **Stage A detailed here**, B/C specced. +- Scroll: **per-pixel smooth** (preserve the 2px/frame glide). + +--- + +## Stage A — Native-width BG renderer (execute now) + +### Key insight that makes this clean +`LoadCurrentMapView` (`src/overworld/overworld.asm`) already maintains +**`wSurroundingTiles`** = a **44×32-tile** decoded view (`SURROUNDING_WIDTH=44`, +`SURROUNDING_HEIGHT=32`) with a 2-tile (16px) margin on each side of the 40×25 +viewport, rebuilt **once per 16px walk-step** (on `wWalkCounter==7`, the first +frame of each step; intermediate frames only accumulate `H_SCX/H_SCY += 2`). So +the surrounding buffer already has exactly the margin the fine scroll needs. + +The native renderer mirrors **`wSurroundingTiles` (352×256 px)** and blits a +320×200 window at a signed pixel offset = (half-block coarse) + (fine 0..14px), +with **no wrap**. This removes the 40-into-32 fold entirely. + +### New renderer (`src/ppu/ppu.asm`) +1. **Replace `bg_surface`** (`resb 256*256`) with a native surface + `resb 352*256` (`SURROUNDING_WIDTH*8` × `SURROUNDING_HEIGHT*8` = 88 KB BSS in + the program image, not the GB alloc — fine). +2. **Rewrite `render_bg`** to: + - Decode `wSurroundingTiles` (44×32 tile IDs) → native surface using the + existing `tile_cache` (keep `rebuild_tile_cache` and `g_tilecache_dirty` + unchanged; OBJ tiles still need the cache). Reuse the per-tile decode body + of `surf_decode_tile` but with **stride 352 and no `&31`/`&1023` torus + masking** — surface tile (col,row) at `row*8*352 + col*8`. + - Source tile IDs from `wSurroundingTiles` (`W_SURROUNDING_TILES`, EBP-rel), + not VRAM `$9800`. Tile-data addressing mode (LCDC bit 4 → `tiledata_mode`) + is unchanged. + - **Blit** a 320×200 window at origin `(Xoff, Yoff)` into the 352×256 surface + with a plain per-row `rep movsb` (no seam branch). `Xoff/Yoff` derived in §3. + - Sync strategy: simplest correct first — **decode all 44×32 each frame** + (1408 tiles × 64B ≈ 90 KB memcpy/frame, trivially within 60 Hz budget on + 386+). The shadow/diff optimization (`sync_surface_diff`/`bg_tilemap_shadow`) + can return later keyed on `wSurroundingTiles` instead of VRAM; not needed + for correctness. Drop `surf_last_base`/torus-base logic. +3. **Blit offset (the smooth-scroll math).** The window's top-left within the + surface = the 2-tile margin minus the partial scroll already consumed: + - Coarse: `wSurroundingTiles` is built so the 40×25 view begins at + surface tile `(2,2)` only when `wXBlockCoord/wYBlockCoord == 0`; when a + half-block coord is set, `LoadCurrentMapView` currently bakes a +2-tile + offset into the `wTileMap` copy (see its `.adjust_x_coord`/`.adjust_y_coord`). + In the native model we **drop that copy** and instead fold the half-block + into the blit origin: `baseX = (2 - wXBlockCoord*2) tiles`, + `baseY = (2 - wYBlockCoord*2) tiles` … (exact sign to be confirmed against + a capture — see Verification; this is the one spot to calibrate). + - Fine: the 0..14px within a step. Reuse `H_SCX`/`H_SCY`, but interpret them + as a **signed pixel offset into the surface**, not a torus SCX/SCY. The + accumulation in `AdvancePlayerSprite.scroll` (`H_SCX/H_SCY += 2*stepvec`) + stays; only the renderer's interpretation changes. + - `Xoff = baseX_px - H_SCX_signed`, `Yoff = baseY_px - H_SCY_signed` + (sign calibrated empirically). Surface margin (16px) ≥ max fine (14px), so + `Xoff/Yoff` stay in `[0, 352-320]`/`[0, 256-200]` — assert/clamp during bring-up. + +### Retire the GB-VRAM BG scroll pipeline +These exist only to feed the torus; with the native surface they are dead: +- `CopyMapViewToVRAM` / `CopyMapViewToVRAM2` (`overworld.asm`) — BG path. +- `FillExtraVRAMRows` (`overworld.asm`) — stale-row hack; delete. +- `RedrawRowOrColumn` + `Schedule{North,South}RowRedraw` / + `Schedule{East,West}ColumnRedraw` + `CopyToRedrawRowOrColumnSrcTiles` + (`overworld.asm`) and its `DelayFrame` call site (`frame.asm:58`). +- `wMapViewVRAMPointer` slide block in `AdvancePlayerSprite` (`overworld.asm` + ~1087–1141, the `.checkWest/.checkSouth/.checkNorth` `&0x03|0x98` ring math) + and its resets (`ResetMapVariables`, `.mapTransition`, DEBUG harness). +- `do_bg_transfer` BG copy in `frame.asm` is gated on `H_AUTO_BG_TRANSFER_EN`, + which the overworld sets to 0 (`overworld.asm:403`); leave the routine for + now (text/menu may use it later) but it is inert in the overworld. + +**Keep:** `LoadCurrentMapView` (now the sole producer of the displayed BG, via +`wSurroundingTiles`), `AdvancePlayerSprite`'s coord/block bookkeeping +(`wXCoord/wYCoord/wXBlockCoord/wYBlockCoord`, `MoveTileBlockMapPointer*`, +`CheckMapConnections`), `wTileMap` (still written by text/menu code — see Risk), +`rebuild_tile_cache`, `render_window`, `render_sprites`, `present`. + +### Coupling to verify, not assume +- **Text boxes / menus** write to `wTileMap`, not `wSurroundingTiles`. They are + drawn while the player is **stationary** (fine offset 0, half-block aligned), + where the visible window equals `wTileMap`. Overworld menus are not wired yet + (Phase 2), so this is a **verification checkpoint**, not a blocker. Contingency + if/when text lands: mirror `wTileMap` text writes into `wSurroundingTiles` at + the current view offset, or composite `wTileMap` over the surface when + stationary. Note in the handoff doc; do not solve speculatively now. + +### Files touched (Stage A) +- `dos_port/src/ppu/ppu.asm` — surface size, `render_bg`, decode helper, drop + torus/diff/base state. +- `dos_port/src/overworld/overworld.asm` — delete the VRAM-ring scroll routines + and the `wMapViewVRAMPointer` slide; simplify `AdvancePlayerSprite` to: update + coords/blocks → `LoadCurrentMapView` → accumulate `H_SCX/H_SCY`. Drop + `CopyMapViewToVRAM`/`FillExtraVRAMRows` calls in `LoadMapData`/`.mapTransition` + and the DEBUG harness. +- `dos_port/src/video/frame.asm` — remove the `RedrawRowOrColumn` call (line 58). +- `dos_port/include/gb_memmap.inc` — comment updates only (no layout change in A). + +### Verification (Stage A) — FRAME.BIN ground truth, per project convention +Build/capture loop (already wired): +``` +make clean && make SKIP_TITLE=1 DEBUG_WALK_NORTH=1 DEBUG_WALK_STEPS=N +dosbox-x -defaultdir "$PWD" -c 'mount c "'"$PWD"'"' -c c: -c PKMN.EXE -c exit +python3 ../tools/render_frame.py FRAME.BIN /tmp/f.png +``` +Checks: +1. **Wrap gone:** re-run the cols-256–319-vs-0–63 comparison (the script in the + 2026-06-16 session); expect it to now diverge like a normal control + (~40–50% different), proving 40 distinct columns. +2. **No top-row garbage** at `DEBUG_WALK_STEPS=1,2,3` (the reproducer). +3. **Baseline unchanged:** `DEBUG_TRANSITION=1 DEBUG_BASELINE=1` still renders + clean Pallet Town. +4. **Smoothness:** capture mid-step (odd `DEBUG_WALK_STEPS`) shows a 2px-offset + glide, not a tile snap. Calibrate the `Xoff/Yoff` sign here. +5. **Interactive:** `make SKIP_TITLE=1`, launch DOSBox-X, walk all four + directions and across the north transition; confirm coherent scroll + edges + (edges may still show border-block fill until Stage B — that's expected). + +--- + +## Stage B — Enlarge MAP_BORDER so the centered viewport never reads past the map (SPEC) + +**Problem:** player is pinned at screen tile (20,12) = block (5,3). The 40×25 +viewport spans 10×6.25 blocks, so centering needs ~5 blocks left/right and ~3 +up/down of real data around the player. `MAP_BORDER=3` is too small near edges → +the out-of-map clamp paints border tiles (clean, but not real map). + +**Approach (to detail next session):** +- Raise `MAP_BORDER` (`include/gb_memmap.inc`) from 3 to **~6** (covers the + 5-block half-width + margin). Verify `wOverworldMap` still fits `0x514` bytes: + worst case Route21 (w10,h45) at border 6 → stride 22 × 57 rows = 1254 < 1300 ✓. +- `SCREEN_BLOCK_WIDTH/HEIGHT` and `SURROUNDING_WIDTH/HEIGHT` already cover the + viewport; re-confirm they still bound the surface after the border change. +- `LoadTileBlockMap` and the `PALLET_TOWN_VIEW_PTR` start use + `MAP_BORDER`/`stride` symbolically — should follow automatically; audit the + few hardcoded `MAP_BORDER*2`/`+6` sites. +- The out-of-map clamp stays as a safety net; with a big enough border it should + stop firing in normal play (confirm via a debug counter or capture). + +**Verify:** baseline + walk-to-edge in all four directions show **real map data** +to the screen edge (no border-block band) via the FRAME.BIN loop. + +--- + +## Stage C — Re-derive connection geometry for the new border/viewport (SPEC) + +**Problem:** `tools/gen_map_headers.py` computes per-direction connection fields +(`_blk/_map/_win/_y/_x/_len`) with hardcoded `+6`/`+3`/`*2` that assume +`MAP_BORDER=3` and the GB's player-at-tile-(8,8). After Stage B these are wrong, +so transitions place the player/camera incorrectly. + +**Approach (to detail next session):** +- Parameterize `gen_map_headers.py` formulas by `MAP_BORDER` and by the DOS + player tile (20,12) instead of the GB (8,8). Regenerate + `dos_port/assets/map_headers.inc`. +- The `connection`-macro formulas are hand-tuned per direction; **re-derive and + verify each direction empirically** with `DEBUG_TRANSITION` (north today; + add south/east/west harness variants) + FRAME.BIN, not by trusting the formula. +- Once map data fully covers the viewport, **delete both stopgaps**: the + `DrawTileBlock` block-ID clamp and the `LoadCurrentMapView` address clamp. + +**Verify:** cross each connection; player stays at screen-center (20,12), new +map is correctly framed, no garbage, return crossing works. + +--- + +## Out of scope (this whole pass) +- Audio, window/menu mid-scroll compositing, NPC sprite engine, save system. +- The `sync_surface_diff` perf optimization (full re-decode is fine at 60 Hz). + +## Rollback +Each stage is a separable commit. Stage A is self-contained: if scroll +calibration regresses, the prior torus `render_bg` + VRAM-ring routines are in +git history. Keep the FRAME.BIN comparison script handy as the objective gate. diff --git a/docs/loadmapheader_handoff.md b/docs/loadmapheader_handoff.md deleted file mode 100644 index 7098429d..00000000 --- a/docs/loadmapheader_handoff.md +++ /dev/null @@ -1,259 +0,0 @@ -# Handoff — faithful LoadMapHeader + map transitions - -Status: **planned, not started** (2026-06-15). For a fresh session. This is the -"rest of the overworld map + transitions" work. The user chose the **faithful -`LoadMapHeader`** path (real ROM map-header format) over a hardcoded dispatcher. - -Cross-refs: CLAUDE.md (faithfulness conventions), docs/session_handoff.md (open -problems), docs/render_opt_handoff.md (renderer is done + decoupled — see -"Renderer impact" below). pret source is the repo root (read-only spec). - ---- - -## Goal - -Make Pallet Town ↔ Route 1 ↔ Route 21 actually walkable: when the player walks -off a map edge into a connection, the game switches `wCurMap`, loads the new -map's header faithfully, and continues — the real Gen 1 behavior. Build it as -real infrastructure (faithful `LoadMapHeader`), not a per-map hardcode, so future -maps drop in by adding data. - ---- - -## Current state (what exists) - -- **Connection STRIPS already load** (done 2026-06-15, see translation_log.md): - `LoadTileBlockMap` calls translated `LoadNorthSouthConnectionsTileMap` / - `LoadEastWestConnectionsTileMap`; the 3-block border of `wOverworldMap` is - filled from the connected map's edge. **But** the connection-struct fields are - currently set by the **`SetupPalletTown` scaffold** with precomputed constants - (NORTH_STRIP_SRC etc. in src/overworld/overworld.asm), and Route1/Route21 - `.blk` are copied to `OW_ROUTE1_BLK_GBADDR` ($5000) / `OW_ROUTE21_BLK_GBADDR` - ($5200). This scaffold is what LoadMapHeader must REPLACE. -- **Map setup is the `SetupPalletTown` scaffold** (src/overworld/overworld.asm): - hardcodes wCurMap, dims, tileset pointers, border block, connections, player - start (center, wYCoord=wXCoord=8 — deliberately away from edges), and copies - all assets to the ROM window. `LoadMapData` calls - DisableLCD → ResetMapVariables → LoadTextBoxTilePatterns → LoadScreenRelatedData - (= LoadTileBlockMap + LoadTilesetTilePatternData + LoadCurrentMapView) → - CopyMapViewToVRAM. `LoadMapHeader` is explicitly skipped ("setup done by - SetupPalletTown scaffold"). -- **Renderer impact: NONE.** The Tier-2 surface renderer (render_bg) mirrors the - VRAM tilemap via a per-frame diff and is fully decoupled — when LoadTileBlockMap - / CopyMapViewToVRAM rewrite the map, the surface picks it up automatically. No - renderer changes are needed for transitions. -- `wYCoord`/`wXCoord` are committed by `AdvancePlayerSprite` (W_Y_COORD/W_X_COORD - += step vector at end of a step). Player can in principle reach the edge values. - ---- - -## The map-header binary format (what to embed + parse) - -From `macros/scripts/maps.asm`: - -`map_header` emits the **fixed header** (copied to `wCurMapHeader`): -``` -db tileset_id -db height, width ; in 4x4-tile blocks -dw Blocks ; -> map .blk data -dw TextPointers -dw Script -db connections ; NORTH|SOUTH|WEST|EAST bits (8/4/2/1) -``` -Then, **in order N,S,W,E for each set connection bit**, an 11-byte -`map_connection_struct` (see macros/ram.asm): -``` -db ConnectedMap -dw ConnectionStripSrc ; into connected map's Blocks -dw ConnectionStripDest ; into wOverworldMap border -db ConnectionStripLength -db ConnectedMapWidth -db ConnectedMapYAlignment -db ConnectedMapXAlignment -dw ConnectedMapViewPointer ; into wOverworldMap -``` -Then `end_map_header` emits `dw _Object`. - -**Object data** (`data/maps/objects/.asm`, pointed to by the `dw Object`): -``` -db border_block -def_warp_events -> N warps; each warp_event = db y,x,destWarpID,destMap (4 bytes, preceded by count) -def_bg_events -> N signs; each = db y,x,textID (3 bytes, preceded by count) -def_object_events-> N sprites; each = picture/movement/etc (see object_event macro), preceded by count -def_warps_to -> warp-to data (for arriving) -``` - -Pallet Town examples (already read): -- header: `map_header PalletTown, PALLET_TOWN, OVERWORLD, NORTH | SOUTH` + - `connection north, Route1, ROUTE_1, 0` + `connection south, Route21, ROUTE_21, 0`. -- object: border `$0b`; 3 warps; 4 signs; 3 objects (Oak/Girl/Fisher). - -Map dims (constants/map_constants.asm): PALLET_TOWN 10×9, ROUTE_1 10×18, -ROUTE_21 10×45. Connection bits (map_data_constants.asm): EAST=1,WEST=2,SOUTH=4, -NORTH=8. - ---- - -## What `LoadMapHeader` does (home/overworld.asm:1793, enumerate to translate) - -1. (skip MarkTownVisited / toggleable objects — defer/stub) -2. `SwitchToMapRomBank` — **no-op in flat model**. -3. tileset bookkeeping (wUnusedCurMapTilesetCopy, hPreviousTileset, the - NO_PREVIOUS_MAP bit) — mostly bookkeeping; keep minimal. -4. `GetMapHeaderPointer` — `MapHeaderPointers[wCurMap]` (+bank). **We need a flat - table** mapping map id → flat addr of that map's embedded header. -5. Copy fixed header → `wCurMapHeader` (`wCurMapHeaderEnd - wCurMapHeader` bytes). -6. Init all 4 wXxxConnectedMap = $FF, then `CopyMapConnectionHeader` (11 bytes - each) for each connection bit set, into wNorth/South/West/EastConnectionHeader. -7. Read object-data pointer (2 bytes), then parse object data: border block → - wMapBackgroundTile, warps (→wNumberOfWarps + wWarpEntries), signs - (CopySignData), sprites (InitSprites). Several of these can be **stubbed** - initially (see "scope" below). -8. `predef LoadTilesetHeader` — loads the tileset's gfx/block/coll pointers + - grass tile + counts into wTileset* from a tileset-header table. We currently - set wTilesetGfxPtr/BlocksPtr/CollisionPtr by hand in the scaffold; this must - become the faithful LoadTilesetHeader reading an embedded OVERWORLD tileset - header. -9. `wCurrentMapHeight2 = wCurMapHeight*2`, `wCurrentMapWidth2 = wCurMapWidth*2` - (the east/south edge thresholds CheckMapConnections compares against). -10. music setup (defer — audio is Phase 3). - ---- - -## Transition flow (home/overworld.asm) — `CheckMapConnections` (~line 525) - -Reached after a step via `CheckWarpsNoCollision` (line 360): if no warp matches, -`jp CheckMapConnections`. Logic per direction: -- **West:** `wXCoord == $FF`? → wCurMap = wWestConnectedMap; wXCoord = - WestXAlignment; wYCoord += WestYAlignment; view ptr = WestViewPointer adjusted - by (wYCoord>>1) rows of (WestConnectedMapWidth + 2*MAP_BORDER); → `.loadNewMap`. -- **East:** `wXCoord == wCurrentMapWidth2`? → analogous with East fields. -- **North:** `wYCoord == $FF`? → wCurMap = wNorthConnectedMap; wYCoord = - NorthYAlignment; wXCoord += NorthXAlignment; view ptr = NorthViewPointer + - (wXCoord>>1); → `.loadNewMap`. -- **South:** `wYCoord == wCurrentMapHeight2`? → analogous with South fields. -- else `.didNotEnterConnectedMap` → `jp OverworldLoop`. - -`.loadNewMap`: (pikachu flags — skip) → **`LoadMapHeader`** → -PlayDefaultMusicFadeOutCurrent (skip) → RunPaletteCommand SET_PAL_OVERWORLD -(skip/Phase 5) → `InitMapSprites` (sprite VRAM slots — can keep NPC scaffold or -stub) → **`LoadTileBlockMap`** → `jp OverworldLoopLessDelay`. - -(Note: after LoadTileBlockMap the view must be rebuilt — pret relies on the -subsequent loop + RedrawRowOrColumn; verify our LoadCurrentMapView + -CopyMapViewToVRAM are invoked so the surface repopulates. May need to call -LoadCurrentMapView + CopyMapViewToVRAM in .loadNewMap like LoadMapData does.) - ---- - -## THE KEY DESIGN CHALLENGE — pointer relocation - -The embedded map headers' pointers (`Blocks`, `Object`, and every connection -`StripSrc`/`StripDest`/`ViewPointer`) are **GB-absolute addresses from the -original ROM/WRAM layout**. Our flat port puts map data at different ROM-window -addresses (OW_*_GBADDR) and wOverworldMap at $E580 (not the GB $C6E8). So the -embedded pointers will be wrong unless handled. Two options: - -- **(Recommended) Asset-gen relocation:** extend `tools/gen_overworld_assets.py` - (or a new tool) to emit each map's header bytes WITH our addresses substituted: - Blocks → OW__BLK_GBADDR, Object → our object-data addr, and recompute the - connection struct pointers (StripSrc = our connected-map blocks + _blk; - StripDest/ViewPointer = W_OVERWORLD_MAP + offset) using the same formulas as the - pret `connection` macro (macros/scripts/maps.asm — already decoded in - src/overworld/overworld.asm's NORTH_*/SOUTH_* constant comments). Then - LoadMapHeader parses them faithfully with no runtime fixup. -- **(Alt) Runtime fixup:** parse the original pointers and relocate in - LoadMapHeader. More runtime code, more error-prone. - -Either way, you need a **memory map** for the embedded data of all three maps: -headers, object data, and blocks at distinct ROM-window addresses (extend the -OW_*_GBADDR block; there's free space above $5400). Define a `MapHeaderPointers` -flat table (map id → header addr) for at least PALLET_TOWN(00), ROUTE_1(0C), -ROUTE_21(20). - ---- - -## Suggested phased plan (keep the per-step-handoff cadence) - -**Step A — data infra:** decide the ROM-window memory map for 3 maps' headers + -object data + blocks; build the asset-gen that emits relocated header/object -binaries + a MapHeaderPointers table; embed OVERWORLD tileset header data. Verify -by DEBUG_DUMP that the embedded header bytes + relocated pointers are correct. - -**Step B — LoadMapHeader + LoadTilesetHeader + CopyMapConnectionHeader:** -translate them; add the `wCurMapHeader`/connection-header WRAM region + -wCurrentMapWidth2/Height2 to gb_memmap.inc (see addresses below). Replace the -`SetupPalletTown` scaffold: `EnterMap` sets wCurMap = PALLET_TOWN and calls -LoadMapHeader; LoadMapData stops skipping it. Stub deferrable parts (warps/signs/ -wild/music/palette) with clear `; TODO` markers. Verify Pallet Town still boots -identically (DEBUG_DUMP header + render). - -**Step C — CheckMapConnections + overworld wiring:** translate -CheckMapConnections; wire the post-step warp/connection check into OverworldLoop -(pret does CheckWarpsNoCollision→CheckMapConnections after a completed step); -ensure the player can reach the edge (collision + coord commit). `.loadNewMap` -calls LoadMapHeader + LoadTileBlockMap + LoadCurrentMapView + CopyMapViewToVRAM. -Verify by walking north into Route 1 and south into Route 21 (user-driven; or -DEBUG_DUMP wCurMap + wOverworldMap after a scripted edge cross). - -**Step D — polish:** Route 1's own north (Viridian) / Route 21's south -(Cinnabar) stay $FF until those maps are added; the DrawTileBlock clamp stays -until all reachable regions have data. Update docs + handoffs. - ---- - -## WRAM addresses (verify/add in dos_port/include/gb_memmap.inc) - -Already present: W_CUR_MAP (0xD35E? check), W_CUR_MAP_TILESET, W_CUR_MAP_HEIGHT, -W_CUR_MAP_WIDTH, W_CUR_MAP_DATA_PTR (0xD369), W_CUR_MAP_CONNECTIONS (0xD36F), -W_{NORTH,SOUTH,WEST,EAST}_CONNECTED_MAP (0xD370/7B/86/91, 11-byte structs), -W_OBJECT_DATA_PTR_TEMP (0xD3A8), W_MAP_BACKGROUND_TILE (0xD3AC), -W_NUMBER_OF_WARPS (0xD3AD), W_WARP_ENTRIES (0xD3AE), W_CURRENT_TILE_BLOCK_MAP_VIEW_PTR -(0xD35E? check — comment says 0xD35E), W_Y_COORD (0xD360), W_X_COORD (0xD361), -CONN_* struct field offsets (added 2026-06-15). - -**Need to add (check pret ram/wram.asm for exact addresses):** -- `wCurMapHeader` region + `wCurMapHeaderEnd` (the fixed-header copy target). In - pret it's the block starting at wCurMapTileset through the connections byte; - our W_CUR_MAP_* vars ARE that region — confirm they're contiguous & ordered to - match the header byte layout, or define a dedicated copy buffer. -- `wCurrentMapHeight2` / `wCurrentMapWidth2` (ram/wram.asm:2108/2111). -- `wNumSigns`, sign/sprite data regions (if not stubbing). -- The connection-header sub-fields are the CONN_* offsets already defined. - -NOTE: our W_CUR_MAP_* layout was hand-assigned and may NOT be byte-contiguous in -header order. LoadMapHeader copies a contiguous header blob — either (a) make the -WRAM vars contiguous in the right order, or (b) copy into a dedicated wCurMapHeader -buffer and have the code read fields from there. Decide early in Step B. - ---- - -## Gotchas / faithfulness notes - -- SwitchToMapRomBank, hLoadedROMBank, BANK() → no-ops in the flat model. -- Keep `; TODO-HW:` for music/palette; `; TODO:` for deferred warp/sign/sprite - parsing. Don't block the transition milestone on them. -- The connection-struct precomputed constants currently in overworld.asm - (NORTH_STRIP_SRC etc.) become DEAD once headers are embedded+parsed — delete - them with the SetupPalletTown scaffold, but keep their derivation comments - (they document the `connection` macro math) somewhere. -- DrawTileBlock out-of-range clamp: still needed (E/W borders, past-map-end). -- Verify via DEBUG_DUMP (src/debug/debug_dump.asm — edit the `windows` table), - not screenshots, per project debugging policy. Build: `make SKIP_TITLE=1 DEBUG_DUMP=1`. -- Renderer needs no changes (surface mirror is decoupled). - ---- - -## Key references - -- pret: `home/overworld.asm` — LoadMapHeader (1793), CheckMapConnections (525), - CheckWarpsNoCollision (360), LoadNorth/SouthConnectionsTileMap, LoadTileBlockMap. -- pret macros: `macros/scripts/maps.asm` (map_header/connection/object_event), - `macros/ram.asm` (map_connection_struct). -- pret data: `data/maps/headers/{PalletTown,Route1,Route21}.asm`, - `data/maps/objects/*.asm`, `data/maps/map_header_pointers.asm`, - `constants/map_constants.asm`, `constants/map_data_constants.asm`. -- ours: `src/overworld/overworld.asm` (SetupPalletTown scaffold, LoadMapData, - LoadTileBlockMap + connection loaders, AdvancePlayerSprite coord commit, - OverworldLoop), `tools/gen_overworld_assets.py`, `dos_port/assets/*.inc`, - `dos_port/include/gb_memmap.inc`. diff --git a/docs/render_opt_handoff.md b/docs/render_opt_handoff.md deleted file mode 100644 index d980a5d4..00000000 --- a/docs/render_opt_handoff.md +++ /dev/null @@ -1,180 +0,0 @@ -# Render Optimization Journey — running handoff - -Living progress log for the render-speed work. **One section per step**, revised -as each lands. Read top-to-bottom for current state, then "Next step" for what to -do. Plan of record: docs/render_tier2_plan.md. Background: docs/session_handoff.md, -docs/386_optimization_strategy.md. - -Decisions locked with the user: -- Keep the GB tile system (tilemap + 2bpp tiles) as source of truth. -- **Chunky 256-color output, raw palette indices** (NOT planar 16-color) — keeps - the Phase 5 GBC-color goal open. We let the VGA DAC do GB palette mapping - (idea borrowed from GaelCathelin/Game-Boy-DOS; that emulator uses planar mode - 0xD, which we reject because 16 colors can't represent GBC). -- Target a slow 386; be aggressive. Each step independently shippable + pixel- - verifiable against the known-good Pallet Town (`./test_render.sh`). - ---- - -## Status board - -| Step | What | State | -|------|------|-------| -| 0 | Decoded-tile cache (raw bit-decode out of hot path) | ✅ done (earlier) | -| 1 | Raw color indices + DAC palette mapping | ✅ done 2026-06-15 | -| 2 | Kill the double full-screen copy | ✅ done 2026-06-15 | -| 3 | Offscreen chunky surface + viewport blit | ✅ done 2026-06-15 | -| 4 | (optional) Mode X / hardware scroll | ⬜ maybe | - -User feedback after step 1: "already way faster." (Some hitching walking *down* -is from the missing-map-connector junk data — already tracked separately, not a -renderer issue.) - ---- - -## Step 1 — raw color indices + DAC palette mapping ✅ (2026-06-15) - -**Goal:** stop baking BGP/OBP shades into framebuffer pixels. Write the *raw* GB -color and let the VGA DAC map color→shade, set once per frame. Cheaper (palette -change = a few register writes, not a tile re-decode) and more faithful to -palette fades/effects. - -**Framebuffer index convention now:** -- BG / window: raw color `0-3` → DAC entries `0-3` -- Sprite via OBP0: `4 + color` → DAC `4-7` -- Sprite via OBP1: `8 + color` → DAC `8-11` -- Sprite color 0 still transparent (not written). BG-priority check now tests BG - *color* 0 (`cmp [backbuf],0`), which is more correct than the old "shade 0". - -**Changes:** -- `src/ppu/ppu.asm`: - - `rebuild_tile_cache` stores raw color (dropped the `bgp_tab` lookup + BGP - unpack). `tile_cache` no longer depends on BGP, so `render_bg`'s rebuild - trigger is `g_tilecache_dirty` only — the BGP-compare and `g_tilecache_bgp` - are gone. - - `decode_win_row` / `render_window` write raw color (dropped BGP unpack). - - `render_sprites` writes `4|color` / `8|color` (dropped the OBP0/OBP1 unpack - and `obp_tab`). `bgp_tab` and `obp_tab` BSS removed. -- `boot/video.asm`: new `commit_palette` — reads BGP/OBP0/OBP1 ($FF47-49, three - consecutive regs), programs DAC entries 0-11 from `dmg_palette` - (`DAC[base+c] = dmg_palette[(reg>>2c)&3]`). Skips when the three regs are - unchanged (`pal_shadow`, init 0xFF). Note: it uses EBP as scratch *after* - deriving `esi = ebp+IO_BGP` (pushad/popad save EBP). -- `src/video/frame.asm`: `DelayFrame` calls `commit_palette` right after - `commit_shadow_regs` (the per-frame VBlank-style register commit). - -**Why identical on screen:** at the normal BGP/OBP ($E4 etc., identity mapping) -raw color == old shade and `DAC[c] = dmg_palette[c]`, so output is byte-identical. -The win shows on palette changes (no rebuild) and unblocks steps 2-3. - -**Verified:** `./test_render.sh` — Pallet Town BG + player/NPC sprites render -correctly in DMG green (sprites confirm the DAC 4-11 path). Build clean (only the -pre-existing benign BSS `align` warnings). - -**Gotchas for later:** -- Anything that writes the back buffer directly must use the raw-index convention - now (e.g. a future textbox/HUD), not shade values. -- `draw_player_marker` (gated off) writes "shade" 0/3 — harmless at normal BGP, - but revisit if re-enabled. -- `video_init` still loads the boot test palette + `dmg_palette` into DAC 0-3; - `commit_palette` overrides 0-11 from frame 1. Fine, but that init block is now - mostly boot-diagnostic. - ---- - -## Step 2 — kill the double full-screen copy ✅ (2026-06-15) - -**Was:** a scroll frame moved ~192 KB — `render_bg` decoded 41 tiles into -`bg_scanline_buf` (~64 KB write), then `rep movsb` copied 320 px/line into the -back buffer at the `SCX&7` offset (64 KB), then `present` copied the back buffer -to VGA (64 KB). - -**Now:** `render_bg` assembles each scanline **directly into the back buffer** in -one write pass — `bg_scanline_buf` and the per-line `rep movsb` copy are gone. -~64 KB/frame + the copy-loop overhead removed (192 KB → ~128 KB traffic). - -**How the `SCX&7` fine offset is handled without the buffer:** each tile's 8 -decoded bytes are written at `dest_pos = tile_col*8 - fine_x`, with per-tile -left/right clipping (`bg_row_ptr` = row start). Tile 0 left-clips by `fine_x` -(writes `8-fine_x` bytes at row start); the last tile right-clips to the -remaining room. With `fine_x = 0` the 41st tile clips to zero → 40 full tiles; -with `fine_x > 0` tiles 0 and 40 are partial → exactly 320 px. The `rep movsb` -count is the (possibly clipped) tile width. - -**Why keep the back buffer + `present`:** window (text boxes — potentially large) -and sprites composite in fast RAM, and sprite BG-priority reads the composited -pixel. Doing that on VGA would mean slow VGA reads + scattered VGA writes. -`present` is a single streaming `rep movsd` (cheap). So we cut the *redundant RAM -copy*, not `present`. - -**Changes:** `src/ppu/ppu.asm` `render_bg` scan loop rewritten; BSS `bg_fine_y2` -and `bg_scanline_buf` removed, `bg_row_ptr` added. - -**Verified:** `./test_render.sh` — Pallet Town pixel-correct, sub-tile fine -offset intact (no horizontal seam), sprites correct. - ---- - -## Step 3 — offscreen chunky surface + viewport blit ✅ (2026-06-15) - -The structural win. `render_bg` no longer re-resolves 41 tiles × 200 scanlines or -walks the tilemap torus per frame. - -**Design (decoupled — NO changes to the faithful scroll/redraw logic):** a -`bg_surface` (256×256 chunky raw-color, in BSS) mirrors the *decoded* BG tilemap -torus. Each frame `render_bg`: -1. Picks the BG tilemap base (LCDC bit 3) and tile-data mode (bit 4). -2. **Syncs** the surface: if `g_tilecache_dirty` (tile data changed) or the - tilemap base switched → full rebuild (`rebuild_tile_cache` + - `rebuild_surface_full`, refreshing `bg_tilemap_shadow`). Otherwise - `sync_surface_diff` compares the live VRAM tilemap to `bg_tilemap_shadow` and - re-decodes only the changed tiles via `surf_decode_tile` (while walking that - is just the edge strip `RedrawRowOrColumn` wrote — cheap, no spikes). -3. **Blits** a 320×200 window from the surface at `(SCX,SCY)` with 256-px torus - wrap on both axes (1–2 `rep movsb` per row). No per-tile addressing, no - 40-into-32 fold. Sampling matches the old renderer exactly: BG pixel (x,y) = - surface pixel `((SCX+x)&255,(SCY+y)&255)`. - -This is the chosen "screen-area torus surface" option (the 256×256 torus *is* the -GB's BG map space; we mirror it rather than maintaining a separate wider surface, -so the existing sliding-window scroll machinery is untouched). `tile_cache` is -kept as the decoded-tile-data store the surface copies from (so a future -animated-tile `g_tilecache_dirty` tick costs a cache rebuild + surface copy, not -a full fresh 2bpp decode). - -**Changes:** `src/ppu/ppu.asm` — `render_bg` rewritten as sync+blit; new -`surf_decode_tile` / `rebuild_surface_full` / `sync_surface_diff`; BSS gained -`bg_surface` (64 KB), `bg_tilemap_shadow` (1 KB), `surf_last_base`; removed the -per-scanline scratch (`bg_fine_x`, `bg_tile_col0`, `bg_fine_y8`, -`bg_tilemap_row`, `bg_row_ptr`, `bg_y`). `rebuild_tile_cache` unchanged. - -**Cost:** static frame = ~1024-byte tilemap diff + one 64 KB windowed blit (no -addressing tax). Walking frame = + re-decode of the changed edge tiles (~tens of -tiles). Full rebuild only on map load / tile-data change. - -**Verified (2026-06-15):** clean-boot capture matches the known-good Pallet Town -exactly; user-driven scrolling in all directions renders clean, correctly-aligned -tiles with no stale strips or seams at the 256-px torus boundary. - -**Known issue (separate, NOT this step's regression):** a **red strip** has been -observed that is not background data — under the lazy pea-soup palette the BG is -always green (0–3) and sprites 4–11, so a red strip implies a pixel value ≥12 -indexing the un-initialized DAC ramps. Tracked as a NEEDS-FIXED item in TODO.md -(distinct from the missing-connector junk). Not addressed here. - -**Possible follow-ups:** -- `sync_surface_diff` uses a byte compare over 1024 entries; could be dword-batched - (negligible vs. what was removed, so not done). -- Step 4 (Mode X / hardware scroll) remains available if a slower target needs it - — would let the CRTC scroll and cut the per-frame 64 KB blit too, drawing only - exposed strips. Big change; only if required. - ---- - -## Next step — none mandatory - -Tier 2 (steps 1–3) is complete: the per-frame tile-decode, double copy, and -per-scanline tile addressing are all gone. Step 4 (Mode X) is optional and only -worth it if the real target CPU still struggles. Otherwise the render-opt quest -is done; remaining overworld work (map connectors, facing-down collision) is in -docs/session_handoff.md. diff --git a/docs/render_tier2_plan.md b/docs/render_tier2_plan.md deleted file mode 100644 index 35387b17..00000000 --- a/docs/render_tier2_plan.md +++ /dev/null @@ -1,162 +0,0 @@ -# Render Tier 2 Plan — chunky raw-index surface + hardware palette - -Status: **planned, not yet implemented** (2026-06-15). Supersedes the "render -speed" follow-on noted in session_handoff.md. Cross-refs: -docs/386_optimization_strategy.md, session_handoff.md, the decoded-tile-cache -work already landed in src/ppu/ppu.asm. - ---- - -## Goal & constraints - -`render_bg` still spends most of a scroll frame on (1) ~120k instructions of -per-scanline tile **addressing** (re-walking the 32-wide tilemap torus, mode -branch, VRAM→cache offset) and (2) **triple full-screen memory traffic** -(scanline buf 64 KB → back buffer 64 KB → VGA 64 KB = 192 KB/frame). The -decoded-tile cache removed the per-pixel bit decode but not these. - -Decisions locked in with the user: -- **Keep the GB tile system** (wOverworldMap blocks + 2bpp tile data + tilemap) - as the source of truth — memory-efficient, preserves glitch/bug behavior. -- **Chunky 256-color output (mode 13h / Mode X), raw palette indices** — NOT a - planar 16-color mode. Keeps the Phase 5 **GBC color** goal open (GBC needs up - to ~56 on-screen colors; 4 bitplanes can only do 16). -- Target a **slow 386**; be aggressive. - ---- - -## What we took from Game-Boy-DOS (GaelCathelin/Game-Boy-DOS, SCREEN.c) - -That emulator uses planar VGA **mode 0xD** and writes GB 2bpp tile bytes -**directly** into VGA bitplanes, with the **VGA palette hardware** doing GB -shade mapping (attribute controller / DAC, reprogrammed once per frame). It runs -full-speed on a 486 **even though it redraws every frame with no hardware -scrolling**. - -Transferable lessons (the planar mode itself is rejected — DMG-only, see above): - -1. **Let the DAC do GB palette mapping; never bake shades into pixels.** Write - the **raw color index** to the framebuffer and map indices→colors via DAC - registers. A BGP/OBP change becomes a handful of register writes instead of a - tile re-decode — cheaper *and* more faithful to palette fades/effects. (Our - current tile cache bakes BGP in and rebuilds on BGP change; this removes that - coupling entirely.) -2. **The redraw is not the enemy — the per-pixel cost is.** Their per-pixel work - is ~nil (planar copy + HW palette), so a full per-frame redraw is fine. Our - enemies are the 8bpp baking, the double copy, and software palette/compositing - — not the act of redrawing. This means Tier 2 should first attack *cost per - pixel and copy count*, and the offscreen surface is a second lever, not the - only one. -3. (Later) **Hardware/region sprite compositing** is possible, but in chunky mode - we keep software sprite compositing (cheap: few sprites visible). -4. Their `tweakTimings()` CRTC retiming is GB-specific; we run our own 60 Hz - loop and don't need it. - ---- - -## Architecture - -### A. Palette: raw indices + DAC mapping (do this first — small, high-value) - -Framebuffer pixels become **raw GB color indices**, and the DAC maps them: - -| DAC entry | Meaning (DMG now) | GBC (Phase 5) | -|-----------|------------------------------------|------------------------------| -| 0–3 | BG color 0–3 → BGP-mapped shade | BG palette 0, colors 0–3 | -| 4–7 | OBJ via OBP0, colors 0–3 (0 unused)| OBJ palette 0 | -| 8–11 | OBJ via OBP1 | OBJ palette 1 | -| … | | up to 32 BG + 24 OBJ entries | - -- BG tiles write **raw color 0–3**. Sprites write **(4 | color)** for OBP0, - **(8 | color)** for OBP1; color 0 = transparent (skip the write). -- On the per-frame shadow-register commit (frame.asm `commit_shadow_regs`), if - BGP/OBP0/OBP1 changed, rewrite the 4/4/4 DAC entries. No surface/cache rebuild. -- This deletes the tile cache's BGP-bake and its BGP-dirty rebuild trigger: - `rebuild_tile_cache` stores the **raw 2-bit color**, so the cache depends only - on VRAM tile data (`g_tilecache_dirty`), never on palette. -- GBC-ready: a pixel can later encode `(palette<<2)|color` and the DAC holds all - palettes; nothing about the chunky pipeline changes. - -### B. Offscreen surface + viewport blit (the structural win) - -Render the map into a **chunky raw-index surface** and per frame copy a window -out of it — eliminating all per-frame tile addressing and the torus/40-into-32 -fold. - -Surface-size options (pick at implementation time; (2) recommended): -1. **Whole-map surface.** Simplest blits (flat camera offset, no wrap), but a big - map (≈640×960+ px) is ~0.6–1 MB via DPMI. Rebuild on map load only; strip - updates only for animated/script tile changes. -2. **Screen+margin torus surface (recommended).** ~360×240–256×256 (~90–110 KB), - kept in sync by **porting `RedrawRowOrColumn`/`AdvancePlayerSprite`'s - sliding-window trick to write decoded tiles into this surface** instead of - (or in addition to) the GB VRAM tilemap. Per frame: one windowed blit; per 8 - px scrolled: decode one exposed tile row/col into the surface edge. Most - memory-efficient and closest to the GB's own approach. Cost: the blit splits - into two memcpys per row at the torus seam. - -Per-frame overworld pipeline becomes: -1. `commit_shadow_regs` (+ DAC update if palette changed). -2. (option 2) if a tile row/col was exposed this step, decode that strip into the - surface edge (ports the existing RedrawRowOrColumn logic, chunky target). -3. **Blit the 320×200 window** from the surface at `(camX, camY)` → back buffer - *or directly to VGA*. Source stride = surface width; `rep movsd` per row. -4. Composite sprites (raw OBP indices; color-0 skip; BG-priority compares the - surface/backbuf value against 0, same as today). -5. `present` (only if we kept a back buffer). - -### C. Kill the double copy - -Regardless of A/B: render the BG window straight into the back buffer (drop the -separate `bg_scanline_buf` → back-buffer copy), or blit the window **directly to -VGA** and composite sprites on VGA (drop `present` entirely). We already -`wait_vblank`, so direct-to-VGA is likely tear-free; fall back to back -buffer+present if tearing shows. Traffic 192 KB → 64–128 KB/frame. - ---- - -## Faithfulness / risk notes - -- **VRAM-corruption glitches.** Some original glitches depend on the physical - 32×32 VRAM tilemap and its wrap. Rendering from a flat surface built off the - block/tile data may not reproduce VRAM-tilemap-corruption effects. We already - deviate (extended 40×25 viewport, DrawTileBlock clamp), so this is consistent - with current scope — but note it for the glitch-sandbox work. Keep the GB - tilemap updates running as the logical model where cheap, so game logic that - reads them stays faithful. -- **Animated tiles** (water/flower) and script tile changes must mark the - affected surface region dirty (whole-map option) or be re-decoded into the - window (torus option). Add a small "surface dirty rect / tile" hook analogous - to `g_tilecache_dirty`. -- **Extended viewport.** We render 40×25 tiles, wider than the GB's 20×18 and - wider than the 32-tile torus — the surface approach removes the fragile - 40-into-32 fold, which is a net simplification. -- **Sprites over the extended area** behave as today. - ---- - -## Rollout order (each independently shippable + verifiable) - -Progress is tracked step-by-step in **docs/render_opt_handoff.md** (revised each -step). - -1. **Palette → raw indices + DAC mapping** (section A). ✅ DONE 2026-06-15 — - `commit_palette` in video.asm, raw-color PPU output, BGP/cache decoupled. See - the handoff for details. -2. **Kill the double copy** (section C). ✅ DONE 2026-06-15 — `render_bg` - assembles directly into the back buffer with inline `SCX&7` clipping; - `bg_scanline_buf` + the per-line copy removed. Kept the back buffer + - `present` (fast RAM compositing for window/sprites). See the handoff. -3. **Offscreen surface + viewport blit** (section B, torus option). ✅ DONE - 2026-06-15 — `bg_surface` (256×256) mirrors the decoded BG tilemap torus, kept - in sync by a per-frame tilemap-vs-shadow diff (`sync_surface_diff`, only - changed tiles re-decoded) with full rebuild on tile-data/base change; - `render_bg` blits a 320×200 torus-wrapped window. Decoupled — the faithful - scroll/RedrawRowOrColumn logic is untouched (we mirror by VRAM tilemap - address, so the sliding window + edge redraw just works). See the handoff. -4. (Optional, later) Mode X for page-flip / true hardware scroll if still slow on - the target CPU. - -Verify each step with `./test_render.sh` (pixel compare to the known-good Pallet -Town) and, once input automation exists, a four-direction scroll pass. Log frame -timing if a counter is added. diff --git a/docs/session_handoff.md b/docs/session_handoff.md deleted file mode 100644 index 5c1a5221..00000000 --- a/docs/session_handoff.md +++ /dev/null @@ -1,177 +0,0 @@ -# Session Handoff — 2026-06-15 (overworld renderer + collision) - -Pass this to the next session. It captures the current state so you don't have to -re-derive it. Cross-refs: CLAUDE.md (Current Phase), TODO.md (Phase 2), -docs/386_optimization_strategy.md. - ---- - -## TL;DR - -The overworld renders and scrolls **correctly in all four directions** now -(verified by single-step screenshots, 2026-06-15). The window-garbage bug is -fixed. Remaining items, in priority order: - -1. ~~**PERF** — `render_bg` too slow~~ ✅ DONE 2026-06-15 (decoded-tile cache; see below). -2. **Map connections** — un-stub the connection strips; removes map-edge corruption. -3. **BUG** — facing-DOWN collision lets the player walk 1 tile into objects. - -The VGA mode is fine: we **are** rendering 320×200 (mode 13h, `present` copies -exactly 64000 bytes). Screen captures look 640×400 only because **DOSBox-X -upscales 2× for display** — not a bug in our code. - ---- - -## What was fixed this session - -### 1. Smooth-scroll `render_bg` rewrite (src/ppu/ppu.asm) -Replaced the tile-blitter (tiles pinned to 8-px slots, no sub-tile scroll) with a -**scanline renderer**: per output scanline, decode 41 tiles (40 visible + 1 for -the shift) into `bg_scanline_buf`, then copy 320 px starting at `SCX & 7`. Both -axes are now pixel-smooth and the old vertical sub-tile overflow is gone. It is -logically correct (samples the same tilemap rows the blitter did, just adds the -fine offset). **Cost:** ~8× more per-tile address resolves + per-pixel decode than -the blitter → this is the perf problem below. - -### 2. Window-layer garbage at the bottom of the screen (src/ppu/ppu.asm:render_window) -Symptom: red/green vertical lines bottom-right (pixel values >3 indexing the -leftover `test_palette` ramps — nothing in our render path emits >3 normally). -Two compounding causes, both fixed: -- `LCDC_DEFAULT_VAL = 0xE3` has the window-enable bit set (bit 5) — the real - Pokémon value. The game parks the window with `WY=144` to hide it on the 144-px - GB screen, but our viewport is **200 px**, so rows 144–199 rendered the parked, - uninitialized window. **Fix:** the window scanline loop is now bounded at - `SCREEN_H` (144), not `RENDER_H` (200), so a parked `WY=144` stays hidden. -- The `wx_adj ≥ 0` copy path had no length clamp (unlike the left-clip path) and - copied up to 320 bytes out of the 256-byte `row_buf`, spilling into adjacent BSS - → those colored lines. **Fix:** clamp the copy to 256. - -### 3. `DrawTileBlock` out-of-range block clamp (src/overworld/overworld.asm) -Still in place (temporary). Clamps block IDs past the 128-block embedded blockset -to block 0 so the extended 40×25 viewport doesn't paint garbage when the camera -reaches uninitialized `wOverworldMap` padding. **Becomes dead code once map -connections are implemented** (see below) — delete it then. - ---- - -## Verified-good (don't re-investigate) -- Initial Pallet Town render: clean. -- Single-step scroll up / down / left / right: clean, correct content at edges. -- VGA output is 320×200 (640×400 = DOSBox-X 2× display scaling). - ---- - -## Open problem 1 — render speed (TOP priority) — ✅ DONE (2026-06-15) - -**Resolved** via a decoded-tile cache (the recommended pre-decode approach). -`render_bg` no longer bit-decodes per pixel in the hot path. - -Implementation (src/ppu/ppu.asm): -- `tile_cache` (BSS, 384 tiles × 64 B = 24 KB) holds the whole BG/window - tile-data region ($8000-$97FF) pre-decoded to 8bpp, with the BGP shade - already baked in. -- `rebuild_tile_cache` decodes all 384 tiles in one linear pass (contiguous - src + dst pointers) and records the BGP it used. -- `render_bg` rebuilds the cache only when `g_tilecache_dirty` is set **or** - `IO_BGP` differs from the cached value; otherwise it reuses the cache (the - common case while scrolling a static map). The per-tile inner loop is now two - `mov`-pair 4-byte copies (`tile_cache → bg_scanline_buf`) instead of the - 8-iteration `shl`/`rcl` decode. The `SCX & 7` scanline buffer + 320 px copy - for smooth horizontal scroll is unchanged. -- `g_tilecache_dirty` starts at 1 (in `.data`) and is set by every VRAM - tile-data writer: `LoadFontTilePatterns`, `LoadTextBoxTilePatterns` - (load_font.asm), `LoadYellowTitleScreenGFX` (title.asm), - `LoadTilesetTilePatternData`, `LoadPlayerSpriteGraphics`, - `SetupPalletTownNPCs` (overworld.asm), and `ClearVram` (init.asm). **If a new - routine writes VRAM tile data ($8000-$97FF), it must set - `g_tilecache_dirty` too**, or it will render stale tiles. (BGP/palette changes - are auto-detected and need no flag.) - -Verified: Pallet Town renders pixel-identical to the pre-optimization -known-good (SKIP_TITLE screenshot, 2026-06-15). - -Decode work per frame on a static map drops from ~65k px-decodes to ~0 (cache -reused); only the cheap row copies + the existing scanline copy remain. The -cache rebuild (24,576 px) happens only on tileset/sprite/font/palette changes. - -Possible follow-on (not done): make `render_window`/`decode_win_row` read the -same `tile_cache` instead of its own per-pixel decode; it is far less hot -(≤144 rows, only when a window is visible) so it was left as-is to limit scope. - -**Bigger win (architectural — this is the open "VGA-native renderer" refactor in -TODO Phase 2):** pre-render the scrolling map to an offscreen 8bpp surface and -blit a `SCX`/`SCY`-offset viewport each frame, updating only the -`RedrawRowOrColumn` edge. Eliminates nearly all per-frame tile work. NOTE a real -tension to resolve here: the GB VRAM tilemap is **32 tiles wide (256 px)** but our -viewport is **40 tiles (320 px)** — wider than the torus. `CopyMapViewToVRAM` -currently folds the extra 8 tiles into columns 0–7 and relies on the renderer's -mod-32 wrap to unfold them; this is fragile and is part of why the refactor wants a -wider `wTileMap`/`wSurroundingTiles`. Decide: **optimize-in-place (pre-decode -tiles) vs. full VGA-native rewrite.** Recommendation: do the pre-decode first (big, -low-risk win) and treat the offscreen-surface rewrite as the follow-on refactor. - -**Pre-computation answer (user asked):** yes — pre-decoding tile patterns to 8bpp -is the single highest-value precompute. (Sprite pre-scaling at build time is -already noted in the refactor plan.) - ---- - -## Open problem 2 — map connections — ✅ STRIPS DONE (2026-06-15) - -**Resolved (strip loading).** `LoadTileBlockMap` now loads the N/S/W/E -connection strips via translated `LoadNorthSouthConnectionsTileMap` / -`LoadEastWestConnectionsTileMap`. Pallet Town connects north→Route1, -south→Route21 (Route1/Route21 `.blk` embedded at OW_ROUTE1/21_BLK_GBADDR; -structs set in SetupPalletTown). Dump-verified the N border = Route 1 bottom 3 -rows, S border = Route 21 top 3 rows. The border now shows real adjacent terrain. -**Remaining:** the map-*transition* trigger (walking into Route 1 as the active -map) is a separate follow-on (TODO.md); the DrawTileBlock clamp is kept (E/W -borders + reads past wOverworldMapEnd still need it). - -### (historical) Open problem 2 — map connections (removes edge corruption) - -User believes much of the remaining corruption is from missing map connectors — -consistent with the code. `LoadTileBlockMap` (src/overworld/overworld.asm) -translates the N/S/W/E connection-strip logic but forces every connected map to -`$FF` (skip), so the area beyond Pallet Town is uninitialized `wOverworldMap` -padding. Walking toward an edge exposes it → DrawTileBlock clamp → block 0. -**Action:** implement the connection-strip loading (load adjacent maps' edge block -data into the padding rows/cols). Then the edge corruption is gone and the -DrawTileBlock clamp is dead code to delete. - ---- - -## Open problem 3 — facing-DOWN collision penetration (BUG) - -Player can move ~1 tile into objects (signs, building roofs/fronts) when -approaching them **from above (facing down)**. Confirmed not graphical. - -**Root cause:** `GetTileInFrontOfPlayer` (src/overworld/overworld.asm:~1303) -checks the tile **±1** from the player-center tile (20,12). But the player moves in -16px / 2-tile steps, so it must check **±2** — matching pret -`_GetTileAndCoordsInFrontOfPlayer` (engine/overworld/player_state.asm:261), which -uses down=`lda_coord 8,11`, up=`8,7`, left=`6,9`, right=`10,9` (all ±2 from the -center tile 8,9). Up/left/right "work" only because they approach thick solid -masses where ±1 vs ±2 lands on solid either way; down approaches thin top edges -where the ±1 tile is still passable. - -**Proposed fix (verify in-game):** down (20,13)→(20,14), up (20,11)→(20,10), -left (19,12)→(18,12), right (21,12)→(22,12). Then walk into objects from all four -sides and confirm the player stops one tile away with no penetration and no -new over-blocking. Not applied this session because it needs in-game verification -(no input automation available here). - ---- - -## Key file/line references -- `src/ppu/ppu.asm` — `render_bg` (~104, scanline renderer), `render_window` - (~411, window-bound + copy-clamp fix), `render_sprites` (~251). -- `src/overworld/overworld.asm` — `GetTileInFrontOfPlayer` (~1303), - `DrawTileBlock` (~579, temp clamp), `LoadTileBlockMap` (~486, stubbed - connections), `AdvancePlayerSprite` (~793), `CopyMapViewToVRAM` (~738). -- `src/video/frame.asm` — `DelayFrame` per-frame pipeline (render order, ~50). -- `boot/video.asm` — mode 13h + `present` (320×200, confirmed). -- `include/gb_memmap.inc` — RENDER_W/H=320/200, SCREEN_H=144, SCREEN_TILES_W/H - =40/25, TILEMAP_W=32. -- pret reference: `engine/overworld/player_state.asm` (collision geometry), - `home/overworld.asm` (map view/scroll). diff --git a/dos_port/.gitignore b/dos_port/.gitignore index b0e4d308..376d7b20 100644 --- a/dos_port/.gitignore +++ b/dos_port/.gitignore @@ -1 +1,2 @@ DUMP.BIN +FRAME.BIN diff --git a/dos_port/Makefile b/dos_port/Makefile index 8720986d..e21a8ed9 100644 --- a/dos_port/Makefile +++ b/dos_port/Makefile @@ -29,6 +29,31 @@ ifdef DEBUG_DUMP NASMFLAGS += -D DEBUG_DUMP endif +# Debug flag: force a north map transition (Pallet Town -> Route 1) at boot so the +# post-crossing render can be screenshotted without keyboard input. make DEBUG_TRANSITION=1 +ifdef DEBUG_TRANSITION +NASMFLAGS += -D DEBUG_TRANSITION +endif + +# Sub-flag of DEBUG_TRANSITION: skip the CheckMapConnections crossing so the +# DEBUG_TRANSITION reload path dumps a pristine (non-transitioned) current map. +# Use to capture a baseline frame for comparison. make DEBUG_TRANSITION=1 DEBUG_BASELINE=1 +ifdef DEBUG_BASELINE +NASMFLAGS += -D DEBUG_BASELINE +endif + +# Debug flag: simulate a northward walk to the map edge then dump FRAME.BIN, to +# inspect where the transition triggers relative to the visible edge. +# make DEBUG_WALK_NORTH=1 (optionally DEBUG_WALK_STEPS=N via NASM define). +ifdef DEBUG_WALK_NORTH +NASMFLAGS += -D DEBUG_WALK_NORTH +endif + +# Number of north steps for DEBUG_WALK_NORTH (default 8 in overworld.asm). +ifdef DEBUG_WALK_STEPS +NASMFLAGS += -D DEBUG_WALK_STEPS=$(DEBUG_WALK_STEPS) +endif + LDFLAGS := -T link.ld TARGET := PKMN.EXE @@ -60,8 +85,18 @@ GAME_SRCS := \ src/overworld/overworld.asm \ src/overworld/movement.asm -# Debug-only sources (linked only when the corresponding flag is set) +# Debug-only sources (linked only when the corresponding flag is set). +# Both DEBUG_DUMP and DEBUG_TRANSITION use debug_dump.asm; add it once if either set. ifdef DEBUG_DUMP +NEED_DEBUG_DUMP := 1 +endif +ifdef DEBUG_TRANSITION +NEED_DEBUG_DUMP := 1 +endif +ifdef DEBUG_WALK_NORTH +NEED_DEBUG_DUMP := 1 +endif +ifdef NEED_DEBUG_DUMP GAME_SRCS += src/debug/debug_dump.asm endif diff --git a/dos_port/assets/map_headers.inc b/dos_port/assets/map_headers.inc new file mode 100644 index 00000000..82165685 --- /dev/null +++ b/dos_port/assets/map_headers.inc @@ -0,0 +1,132 @@ +; map_headers.inc — generated by tools/gen_map_headers.py. DO NOT EDIT BY HAND. + +OW_TILESET_HDR_GBADDR equ 0x5400 +OW_MAP_HEADERS_GBADDR equ 0x540C + +map_headers_data: +tileset_header_OVERWORLD: + db 0x01 ; bank + dw OW_BLOCKS_GBADDR ; blocks + dw OW_GFX_GBADDR ; gfx + dw OW_COLL_GBADDR ; collision + db 0xFF, 0xFF, 0xFF ; counters + db 0x52 ; grass tile + db 0x02 ; animations + +OW_MAP_HDR_PALLET_TOWN equ 0x540C +map_header_PALLET_TOWN: + db 0x00 ; tileset OVERWORLD + db 9, 10 ; height, width + dw 0x4E00 ; blocks_ptr + dw 0x0000 ; text_ptr + dw 0x0000 ; script_ptr + db 0x0C ; connections + ; NORTH connection to ROUTE_1 + db 0x0C ; map id + dw 0x5096 ; strip src + dw 0xE583 ; strip dest + db 10 ; strip length + db 10 ; connected map width + db 35, 0 ; Y, X align + dw 0xE691 ; view ptr + ; SOUTH connection to ROUTE_21 + db 0x20 ; map id + dw 0x5200 ; strip src + dw 0xE643 ; strip dest + db 10 ; strip length + db 10 ; connected map width + db 0, 0 ; Y, X align + dw 0xE591 ; view ptr + dw 0x542E ; object_data_ptr +map_object_PALLET_TOWN: + db 0x0B ; border block + db 3 ; warp count + times 12 db 0 ; warp stubs + db 4 ; sign count + times 12 db 0 ; sign stubs + db 3 ; sprite count + +OW_MAP_HDR_ROUTE_1 equ 0x544A +map_header_ROUTE_1: + db 0x00 ; tileset OVERWORLD + db 18, 10 ; height, width + dw 0x5000 ; blocks_ptr + dw 0x0000 ; text_ptr + dw 0x0000 ; script_ptr + db 0x04 ; connections + ; SOUTH connection to PALLET_TOWN + db 0x00 ; map id + dw 0x4E00 ; strip src + dw 0xE6D3 ; strip dest + db 10 ; strip length + db 10 ; connected map width + db 0, 0 ; Y, X align + dw 0xE591 ; view ptr + dw 0x5461 ; object_data_ptr +map_object_ROUTE_1: + db 0x0B ; border block + db 0 ; warp count + db 1 ; sign count + times 3 db 0 ; sign stubs + db 2 ; sprite count + +OW_MAP_HDR_ROUTE_21 equ 0x5468 +map_header_ROUTE_21: + db 0x00 ; tileset OVERWORLD + db 45, 10 ; height, width + dw 0x5200 ; blocks_ptr + dw 0x0000 ; text_ptr + dw 0x0000 ; script_ptr + db 0x08 ; connections + ; NORTH connection to PALLET_TOWN + db 0x00 ; map id + dw 0x4E3C ; strip src + dw 0xE583 ; strip dest + db 10 ; strip length + db 10 ; connected map width + db 17, 0 ; Y, X align + dw 0xE601 ; view ptr + dw 0x547F ; object_data_ptr +map_object_ROUTE_21: + db 0x43 ; border block + db 0 ; warp count + db 0 ; sign count + db 9 ; sprite count + +MapHeaderPointers: + dw OW_MAP_HDR_PALLET_TOWN + dw 0x0000 ; map 1 unimplemented + dw 0x0000 ; map 2 unimplemented + dw 0x0000 ; map 3 unimplemented + dw 0x0000 ; map 4 unimplemented + dw 0x0000 ; map 5 unimplemented + dw 0x0000 ; map 6 unimplemented + dw 0x0000 ; map 7 unimplemented + dw 0x0000 ; map 8 unimplemented + dw 0x0000 ; map 9 unimplemented + dw 0x0000 ; map 10 unimplemented + dw 0x0000 ; map 11 unimplemented + dw OW_MAP_HDR_ROUTE_1 + dw 0x0000 ; map 13 unimplemented + dw 0x0000 ; map 14 unimplemented + dw 0x0000 ; map 15 unimplemented + dw 0x0000 ; map 16 unimplemented + dw 0x0000 ; map 17 unimplemented + dw 0x0000 ; map 18 unimplemented + dw 0x0000 ; map 19 unimplemented + dw 0x0000 ; map 20 unimplemented + dw 0x0000 ; map 21 unimplemented + dw 0x0000 ; map 22 unimplemented + dw 0x0000 ; map 23 unimplemented + dw 0x0000 ; map 24 unimplemented + dw 0x0000 ; map 25 unimplemented + dw 0x0000 ; map 26 unimplemented + dw 0x0000 ; map 27 unimplemented + dw 0x0000 ; map 28 unimplemented + dw 0x0000 ; map 29 unimplemented + dw 0x0000 ; map 30 unimplemented + dw 0x0000 ; map 31 unimplemented + dw OW_MAP_HDR_ROUTE_21 + +MAP_HEADERS_DATA_SIZE equ $ - map_headers_data + diff --git a/dos_port/include/gb_memmap.inc b/dos_port/include/gb_memmap.inc index 7afd8ea4..517dc846 100644 --- a/dos_port/include/gb_memmap.inc +++ b/dos_port/include/gb_memmap.inc @@ -125,8 +125,9 @@ GB_HRAM_SIZE equ 127 ; Named HRAM variables (subset needed by translated routines). ; All offsets verified from pokeyellow.sym (bank 00). H_SOFT_RESET equ 0xFF8A ; hSoftReset -; 0xFF8B is a UNION of hMapStride / hNorthSouthConnectionStripWidth / hEastWestConnectedMapWidth +; 0xFF8B is a UNION of hMapStride / hNorthSouthConnectionStripWidth / hPreviousTileset H_MAP_STRIDE equ 0xFF8B ; hMapStride (= map block row stride = width + 2*MAP_BORDER) +H_PREVIOUS_TILESET equ 0xFF8B ; hPreviousTileset (HRAM union with hMapStride) ; 0xFF8C is a UNION of hMapWidth / hNorthSouthConnectedMapWidth H_MAP_WIDTH equ 0xFF8C ; hMapWidth (= wCurMapWidth, stored for row-copy inner loop) H_SERIAL_CONN_STATUS equ 0xFFAA ; hSerialConnectionStatus @@ -230,13 +231,21 @@ W_Y_COORD equ 0xD360 ; wYCoord (player map Y, in tiles) W_X_COORD equ 0xD361 ; wXCoord (player map X, in tiles) W_Y_BLOCK_COORD equ 0xD362 ; wYBlockCoord W_X_BLOCK_COORD equ 0xD363 ; wXBlockCoord +W_LAST_MAP equ 0xD364 ; wLastMap ; Fixed map header (wCurMapHeader = wCurMapTileset at $D366) +; This is a contiguous 10-byte region that LoadMapHeader copies into from the ROM header. +; Layout: tileset(1) + height(1) + width(1) + dataptr(2) + textptr(2) + scriptptr(2) + connections(1) +W_CUR_MAP_HEADER equ 0xD366 ; wCurMapHeader (start of fixed header copy target) W_CUR_MAP_TILESET equ 0xD366 ; wCurMapTileset (= wCurMapHeader first byte) W_CUR_MAP_HEIGHT equ 0xD367 ; wCurMapHeight W_CUR_MAP_WIDTH equ 0xD368 ; wCurMapWidth W_CUR_MAP_DATA_PTR equ 0xD369 ; wCurMapDataPtr (2 bytes, LE) +W_CUR_MAP_TEXT_PTR equ 0xD36B ; wCurMapTextPtr (2 bytes, LE) +W_CUR_MAP_SCRIPT_PTR equ 0xD36D ; wCurMapScriptPtr (2 bytes, LE) W_CUR_MAP_CONNECTIONS equ 0xD36F ; wCurMapConnections (NORTH|SOUTH|WEST|EAST bits) +W_CUR_MAP_HEADER_END equ 0xD370 ; wCurMapHeaderEnd (byte after connections) +W_CUR_MAP_HEADER_SIZE equ W_CUR_MAP_HEADER_END - W_CUR_MAP_HEADER ; = 10 bytes ; Connection-strip headers (each 11 bytes: map, strip-src, strip-dst, length, map-width, Y-align, X-align, view-ptr) W_NORTH_CONNECTED_MAP equ 0xD370 ; wNorthConnectedMap ($FF = none) @@ -251,23 +260,45 @@ W_OBJECT_DATA_PTR_TEMP equ 0xD3A8 ; wObjectDataPointerTemp (2 bytes) ; Map object header (follows connections in object data) W_MAP_BACKGROUND_TILE equ 0xD3AC ; wMapBackgroundTile (border block ID for wOverworldMap) W_NUMBER_OF_WARPS equ 0xD3AD ; wNumberOfWarps -W_WARP_ENTRIES equ 0xD3AE ; wWarpEntries +W_WARP_ENTRIES equ 0xD3AE ; wWarpEntries (MAX_WARP_EVENTS * 4 bytes max) +W_DESTINATION_WARP_ID equ 0xD42E ; wDestinationWarpID ($FF = don't update coords) + +; Sign/BG event data +W_NUM_SIGNS equ 0xD4AF ; wNumSigns +W_SIGN_COORDS equ 0xD4B0 ; wSignCoords (MAX_BG_EVENTS * 2 bytes: Y, X pairs) +W_SIGN_TEXT_IDS equ 0xD4D0 ; wSignTextIDs (MAX_BG_EVENTS bytes) + +MAX_WARP_EVENTS equ 32 +MAX_BG_EVENTS equ 16 +MAX_OBJECT_EVENTS equ 16 ; Special-warp step offsets (incremented as the block-map pointer crosses ; block boundaries; faithful bookkeeping, not yet consumed by warp logic). W_Y_OFFSET_SINCE_LAST_SPECIAL_WARP equ 0xD4E1 ; wYOffsetSinceLastSpecialWarp W_X_OFFSET_SINCE_LAST_SPECIAL_WARP equ 0xD4E2 ; wXOffsetSinceLastSpecialWarp +; Edge detection thresholds (height*2 / width*2) — used by CheckMapConnections +W_CURRENT_MAP_HEIGHT_2 equ 0xD523 ; wCurrentMapHeight2 = wCurMapHeight * 2 +W_CURRENT_MAP_WIDTH_2 equ 0xD524 ; wCurrentMapWidth2 = wCurMapWidth * 2 + ; Map-view VRAM pointer and player facing W_MAP_VIEW_VRAM_POINTER equ 0xD525 ; wMapViewVRAMPointer (2 bytes, LE; normally $9800) W_PLAYER_MOVING_DIRECTION equ 0xD527 ; wPlayerMovingDirection (PLAYER_DIR_*) W_PLAYER_DIRECTION equ 0xD529 ; wPlayerDirection (PLAYER_DIR_*) -; Active tileset pointers (set from tileset header by LoadMapHeader) -W_TILESET_BANK equ 0xD52A ; wTilesetBank +; Active tileset WRAM (set from tileset header by LoadTilesetHeader) +; LoadTilesetHeader copies 11 bytes here from the tileset header table, +; then reads the 12th byte (animations) to hTileAnimations. +W_TILESET_BANK equ 0xD52A ; wTilesetBank (1 byte) W_TILESET_BLOCKS_PTR equ 0xD52B ; wTilesetBlocksPtr (2 bytes, LE) W_TILESET_GFX_PTR equ 0xD52D ; wTilesetGfxPtr (2 bytes, LE) W_TILESET_COLLISION_PTR equ 0xD52F ; wTilesetCollisionPtr (2 bytes, LE; passable-tile list) +W_TILESET_TALKING_OVER_TILES equ 0xD531 ; wTilesetTalkingOverTiles (3 bytes: counter tiles) +W_TILESET_GRASS_TILE equ 0xD534 ; (overlaps wGrassTile — handled by LoadTilesetHeader) +; NOTE: wTilesetGrassTile ($D534) is the same address as wGrassTile. +; LoadTilesetHeader copies it as part of the 11-byte block. +W_TILESET_HEADER_COPY_SIZE equ 11 ; bytes copied from tileset header to wTilesetBank +W_TILESET_HEADER_FULL_SIZE equ 12 ; full tileset header entry size (11 + animations) ; Walk/bike/surf state W_WALK_BIKE_SURF_STATE equ 0xD6FF ; wWalkBikeSurfState @@ -350,18 +381,24 @@ PLAYER_DIR_BIT_LEFT equ 1 PLAYER_DIR_BIT_DOWN equ 2 PLAYER_DIR_BIT_UP equ 3 -; ROM-window area for embedded overworld assets (Phase 2 scaffold) +; ROM-window area for embedded overworld assets. ; The unused ROM bank-switch window ($4000–$7FFF) is repurposed to hold asset data -; that faithful routines (LoadTilesetTilePatternData, DrawTileBlock, LoadTileBlockMap) -; read via wTilesetGfxPtr / wTilesetBlocksPtr / wCurMapDataPtr pointers. +; that faithful routines (LoadTilesetTilePatternData, DrawTileBlock, LoadTileBlockMap, +; LoadMapHeader, LoadTilesetHeader) read via pointers stored in WRAM. OW_GFX_GBADDR equ 0x4000 ; overworld tileset 2bpp → vTileset ($9000) OW_BLOCKS_GBADDR equ 0x4600 ; overworld blockset (.bst, 128 blocks × 16 tile-IDs) -OW_MAP_GBADDR equ 0x4E00 ; PalletTown.blk (10×9 = 90 block IDs) +OW_PALLET_BLK_GBADDR equ 0x4E00 ; PalletTown.blk (10×9 = 90 block IDs) +OW_MAP_GBADDR equ OW_PALLET_BLK_GBADDR ; legacy alias (Phase 2 scaffold) OW_COLL_GBADDR equ 0x4F00 ; Overworld_Coll passable-tile list ($FF-terminated) -; Connected-map block data (map-connection border strips). Free ROM-window space -; above the Pallet Town assets; the connection-strip src pointers index into these. -OW_ROUTE1_BLK_GBADDR equ 0x5000 ; Route1.blk (10×18 = 180 block IDs) — Pallet Town north -OW_ROUTE21_BLK_GBADDR equ 0x5200 ; Route21.blk (10×45 = 450 block IDs) — Pallet Town south +; Map block data for each map. Connection strip src pointers index into these. +OW_ROUTE1_BLK_GBADDR equ 0x5000 ; Route1.blk (10×18 = 180 block IDs) +OW_ROUTE21_BLK_GBADDR equ 0x5200 ; Route21.blk (10×45 = 450 block IDs) +; Map headers, tileset header, and object data — generated by gen_map_headers.py. +; Loaded to ROM window at boot; LoadMapHeader reads from [EBP + these addresses]. +OW_TILESET_HDR_GBADDR equ 0x5400 ; OVERWORLD tileset header (12 bytes) +OW_MAP_HEADERS_GBADDR equ 0x540C ; base for map header + object data blobs +; Per-map header addresses are defined in assets/map_headers.inc as +; OW_MAP_HDR_PALLET_TOWN, OW_MAP_HDR_ROUTE_1, etc. ; Connection-strip header field offsets (added to W_{NORTH,SOUTH,WEST,EAST}_CONNECTED_MAP; ; struct is 11 bytes — see map_connection_struct in macros/ram.asm). @@ -372,6 +409,19 @@ CONN_MAP_WIDTH equ 6 ; ConnectedMapWidth (db) CONN_Y_ALIGN equ 7 ; ConnectedMapYAlignment (db) CONN_X_ALIGN equ 8 ; ConnectedMapXAlignment (db) CONN_VIEW_PTR equ 9 ; ConnectedMapViewPointer (dw) +CONN_HEADER_SIZE equ 11 ; total connection header struct size + +; Connection direction bits (constants/map_data_constants.asm) +CONNECTION_NORTH equ 1 << 3 ; bit 3 = $08 +CONNECTION_SOUTH equ 1 << 2 ; bit 2 = $04 +CONNECTION_WEST equ 1 << 1 ; bit 1 = $02 +CONNECTION_EAST equ 1 << 0 ; bit 0 = $01 + +; Map IDs (constants/map_constants.asm — subset for implemented maps) +MAP_ID_PALLET_TOWN equ 0x00 +MAP_ID_ROUTE_1 equ 0x0C +MAP_ID_ROUTE_21 equ 0x20 +MAP_NO_CONNECTION equ 0xFF ; sentinel: no connected map in this direction ; IE register shadow — $FFFF (1 byte) ; Emulated Interrupt Enable register. Not functionally used by the DOS port diff --git a/dos_port/src/debug/debug_dump.asm b/dos_port/src/debug/debug_dump.asm index 445038d9..4f330e98 100644 --- a/dos_port/src/debug/debug_dump.asm +++ b/dos_port/src/debug/debug_dump.asm @@ -24,6 +24,7 @@ bits 32 extern ds_base global DebugDumpMemory +global DumpBackbuffer ; Each window is WIN_SIZE bytes copied from [EBP + window_offset]. ; The host-side layout is simply these windows concatenated in table order. @@ -45,6 +46,7 @@ section .data align 4 fname: db "DUMP.BIN", 0 +fbname: db "FRAME.BIN", 0 ; GB-address start of each 64-byte dump window. Host hexdump offsets: ; 0x000 0x4600 overworld blockset (block 0..3) — asset copy check @@ -167,6 +169,78 @@ DebugDumpMemory: mov ax, 0x4C00 int 0x21 +; --------------------------------------------------------------------------- +; DumpBackbuffer — write the full GB_BACKBUF (RENDER_W*RENDER_H = 64000 raw +; palette-indexed bytes) to FRAME.BIN, then exit. Lets the host render the exact +; pixels the software PPU produced under DOSBox-X (no compositor screenshot). +; Allocates a single 64 KB+ conventional buffer so the data goes out in one write. +; In: EBP = GB memory base. Never returns. +; --------------------------------------------------------------------------- +DumpBackbuffer: + ; --- Allocate a conventional DOS buffer big enough for 0x10 + 64000 bytes --- + ; 0x10 + 64000 = 64016 bytes -> 4001 paragraphs; round up to 0x1001 (4097). + mov ax, 0x0100 + mov bx, 0x1001 + int 0x31 + jc .exit + mov [dos_seg], ax + mov [dos_sel], dx + movzx eax, ax + shl eax, 4 + sub eax, [ds_base] + mov [dos_flat], eax + + ; --- Stage filename at offset 0 --- + mov esi, fbname + mov edi, [dos_flat] + mov ecx, 10 ; "FRAME.BIN" + NUL + rep movsb + + ; --- Copy backbuffer directly to buffer offset 0x10 --- + lea esi, [ebp + GB_BACKBUF] + mov edi, [dos_flat] + add edi, 0x10 + mov ecx, GB_BACKBUF_SIZE + rep movsb + + ; --- Create FRAME.BIN --- + call zero_rmcs + mov word [rmcs + RMCS_EAX], 0x3C00 + mov dword [rmcs + RMCS_EDX], 0 + mov ax, [dos_seg] + mov [rmcs + RMCS_DS], ax + call sim_int21 + test byte [rmcs + RMCS_FLAGS], 1 + jnz .free + mov ax, [rmcs + RMCS_EAX] + mov [file_handle], ax + + ; --- Write 64000 bytes --- + call zero_rmcs + mov word [rmcs + RMCS_EAX], 0x4000 + movzx eax, word [file_handle] + mov [rmcs + RMCS_EBX], eax + mov dword [rmcs + RMCS_ECX], GB_BACKBUF_SIZE + mov dword [rmcs + RMCS_EDX], 0x10 + mov ax, [dos_seg] + mov [rmcs + RMCS_DS], ax + call sim_int21 + + ; --- Close --- + call zero_rmcs + mov word [rmcs + RMCS_EAX], 0x3E00 + movzx eax, word [file_handle] + mov [rmcs + RMCS_EBX], eax + call sim_int21 + +.free: + mov ax, 0x0101 + mov dx, [dos_sel] + int 0x31 +.exit: + mov ax, 0x4C00 + int 0x21 + ; --------------------------------------------------------------------------- ; sim_int21 — reflect INT 21h to real mode using the prepared rmcs. ; DPMI fn 0300h: BL=int#, BH=0, CX=0 (no stack words), ES:EDI -> rmcs. diff --git a/dos_port/src/overworld/overworld.asm b/dos_port/src/overworld/overworld.asm index 6f2c48a5..f0ccc1d0 100644 --- a/dos_port/src/overworld/overworld.asm +++ b/dos_port/src/overworld/overworld.asm @@ -55,6 +55,11 @@ extern g_tilecache_dirty %ifdef DEBUG_DUMP extern DebugDumpMemory %endif +%ifdef DEBUG_TRANSITION +extern DumpBackbuffer +%elifdef DEBUG_WALK_NORTH +extern DumpBackbuffer +%endif global EnterMap global ResetMapVariables @@ -104,7 +109,12 @@ NORTH_STRIP_LENGTH equ 10 NORTH_CONN_MAP_WIDTH equ 10 NORTH_Y_ALIGN equ 35 NORTH_X_ALIGN equ 0 -NORTH_VIEW_PTR equ W_OVERWORLD_MAP + 289 +; NOTE: DEAD CODE. The live north view pointer is the one emitted into +; assets/map_headers.inc by tools/gen_map_headers.py (currently 0xE691 = base+273) +; and loaded into the connection header by LoadMapHeader. This equ is no longer +; read at runtime (SetupPalletTown, which used it, was removed). Kept only as a +; reference value; edit gen_map_headers.py / map_headers.inc to change behavior. +NORTH_VIEW_PTR equ W_OVERWORLD_MAP + 273 ; (conn_width+6)*(conn_height-1)+1 ; south (Route21): _blk = 0; _map = (CUR_W+6)*(CUR_H+3)+3 = 16*12+3 = 195; ; _len = min(CUR_W+3, ROUTE21_W) = 10; _y = 0; _x = 0; _win = ROUTE21_W+7 = 17 @@ -129,10 +139,73 @@ section .text ; Pret ref: home/overworld.asm:EnterMap (simplified; no fly/warp logic yet) ; --------------------------------------------------------------------------- EnterMap: - call SetupPalletTown + call LoadOverworldAssets + call SetupPlayerSprite call LoadMapData %ifdef DEBUG_DUMP call DebugDumpMemory ; dump GB memory to DUMP.BIN, then exit (debug only) +%endif +%ifdef DEBUG_WALK_NORTH + ; Walk-simulation harness: drive the REAL movement primitives north for + ; DEBUG_WALK_STEPS steps (default 8: wYCoord 8 -> 0, the north edge), then + ; dump the frame. Reveals where the player is VISUALLY when it reaches the + ; map edge / when CheckMapConnections fires — i.e. whether the transition + ; triggers at an appropriate point. Collision is skipped so the walk is + ; unconditional. If a crossing fires mid-walk, we dump immediately. +%ifndef DEBUG_WALK_STEPS +%define DEBUG_WALK_STEPS 8 +%endif + mov ecx, DEBUG_WALK_STEPS +.wn_step: + push ecx + mov byte [ebp + W_SPRITE_PLAYER_Y_STEP_VECTOR], 0xFF ; -1 (north) + mov byte [ebp + W_SPRITE_PLAYER_X_STEP_VECTOR], 0 + mov byte [ebp + W_PLAYER_DIRECTION], PLAYER_DIR_UP + mov byte [ebp + W_PLAYER_MOVING_DIRECTION], PLAYER_DIR_UP + mov byte [ebp + W_SPRITE_PLAYER_FACING_DIR], SPRITE_FACING_UP + mov byte [ebp + W_WALK_COUNTER], 8 +.wn_frames: + call UpdateSprites + call AdvancePlayerSprite + jc .wn_crossed ; CF=1 → CheckMapConnections fired this step + call DelayFrame + cmp byte [ebp + W_WALK_COUNTER], 0 + jne .wn_frames + pop ecx + dec ecx + jnz .wn_step + call DumpBackbuffer ; reached edge without crossing — dump it +.wn_crossed: + pop ecx ; (balance stack; ecx unused after) + call DumpBackbuffer ; dump the frame at the moment of crossing +%endif +%ifdef DEBUG_TRANSITION + ; Deterministic transition test: simulate stepping off the north edge of + ; Pallet Town (wYCoord wraps to 255), run the real CheckMapConnections, then + ; the same reload .mapTransition does. Lets us screenshot the post-crossing + ; render of Route 1's bottom without keyboard input. +%ifndef DEBUG_BASELINE + mov byte [ebp + W_X_COORD], 8 + mov byte [ebp + W_Y_COORD], 255 + call CheckMapConnections ; sets W_CUR_MAP + view ptr for Route 1 +%endif + mov byte [ebp + W_WALK_COUNTER], 0 + mov byte [ebp + W_SPRITE_PLAYER_Y_STEP_VECTOR], 0 + mov byte [ebp + W_SPRITE_PLAYER_X_STEP_VECTOR], 0 + mov byte [ebp + H_SCY], 0 + mov byte [ebp + H_SCX], 0 + mov word [ebp + W_MAP_VIEW_VRAM_POINTER], GB_TILEMAP0 + call LoadMapHeader + call LoadTileBlockMap + call LoadCurrentMapView + call CopyMapViewToVRAM + call FillExtraVRAMRows + ; Render a few frames so GB_BACKBUF holds the post-transition image, then + ; exfiltrate the exact rendered pixels to FRAME.BIN for host inspection. + call DelayFrame + call DelayFrame + call DelayFrame + call DumpBackbuffer ; writes FRAME.BIN then exits (never returns) %endif ; fall through to OverworldLoop @@ -210,111 +283,43 @@ OverworldLoop: .moveAhead: call AdvancePlayerSprite + jc .mapTransition ; CF=1 means a map connection was crossed jmp OverworldLoop +.mapTransition: + ; A connection was crossed — reload everything for the new map. + mov byte [ebp + W_WALK_COUNTER], 0 + mov byte [ebp + W_SPRITE_PLAYER_Y_STEP_VECTOR], 0 + mov byte [ebp + W_SPRITE_PLAYER_X_STEP_VECTOR], 0 + + ; Reset scroll and VRAM pointer. During the walk, H_SCY/H_SCX accumulated + ; 2 px/frame (e.g. −144 px over 9 north steps). CopyMapViewToVRAM always + ; writes to GB_TILEMAP0 ($9800), so the PPU must start reading from row 0 + ; (SCY=0). W_MAP_VIEW_VRAM_POINTER must also reset so RedrawRowOrColumn + ; uses the correct base address on subsequent frames. + mov byte [ebp + H_SCY], 0 + mov byte [ebp + H_SCX], 0 + mov word [ebp + W_MAP_VIEW_VRAM_POINTER], GB_TILEMAP0 + + call LoadMapHeader + call LoadTileBlockMap + call LoadCurrentMapView + call CopyMapViewToVRAM + call FillExtraVRAMRows ; pre-fill VRAM rows 25-31 to avoid stale tile flicker + + jmp OverworldLoop.lessDelay + ; --------------------------------------------------------------------------- -; SetupPalletTown — Phase 2 scaffold. NOT a faithful translation. -; Hardcodes Pallet Town map-header WRAM variables and copies asset binaries -; from .rodata into the ROM-window area of GB memory (EBP+$4000–$4EFF). ; --------------------------------------------------------------------------- -SetupPalletTown: +; LoadOverworldAssets — Phase 2 scaffold. +; Copies the generated map headers and overworld assets from .rodata into the +; ROM-window area of GB memory (EBP+$4000–$54FF). +; --------------------------------------------------------------------------- +LoadOverworldAssets: push esi push edi push ecx - ; --- Set map identity --- - mov byte [ebp + W_CUR_MAP], MAP_ID_PALLET_TOWN - mov byte [ebp + W_CUR_MAP_TILESET], TILESET_OVERWORLD - mov byte [ebp + W_CUR_MAP_HEIGHT], PALLET_TOWN_HEIGHT - mov byte [ebp + W_CUR_MAP_WIDTH], PALLET_TOWN_WIDTH - - ; wCurMapDataPtr = OW_MAP_GBADDR ($4E00) — pointer into ROM window - mov word [ebp + W_CUR_MAP_DATA_PTR], OW_MAP_GBADDR - - ; --- Tileset pointers --- - mov byte [ebp + W_TILESET_BANK], TILESET_BANK_FLAT - mov word [ebp + W_TILESET_GFX_PTR], OW_GFX_GBADDR - mov word [ebp + W_TILESET_BLOCKS_PTR], OW_BLOCKS_GBADDR - mov word [ebp + W_TILESET_COLLISION_PTR], OW_COLL_GBADDR - - ; --- Map object data (normally read from PalletTown_Object by LoadMapHeader) --- - mov byte [ebp + W_MAP_BACKGROUND_TILE], PALLET_TOWN_BORDER_BLOCK - mov byte [ebp + W_NUMBER_OF_WARPS], 0 - - ; --- Map connections: Pallet Town connects north→Route1, south→Route21 --- - mov byte [ebp + W_CUR_MAP_CONNECTIONS], CONNECTION_NORTH | CONNECTION_SOUTH - - ; North connection strip header (Route 1) - mov byte [ebp + W_NORTH_CONNECTED_MAP], MAP_ID_ROUTE_1 - mov word [ebp + W_NORTH_CONNECTED_MAP + CONN_STRIP_SRC], NORTH_STRIP_SRC - mov word [ebp + W_NORTH_CONNECTED_MAP + CONN_STRIP_DEST], NORTH_STRIP_DEST - mov byte [ebp + W_NORTH_CONNECTED_MAP + CONN_STRIP_LENGTH], NORTH_STRIP_LENGTH - mov byte [ebp + W_NORTH_CONNECTED_MAP + CONN_MAP_WIDTH], NORTH_CONN_MAP_WIDTH - mov byte [ebp + W_NORTH_CONNECTED_MAP + CONN_Y_ALIGN], NORTH_Y_ALIGN - mov byte [ebp + W_NORTH_CONNECTED_MAP + CONN_X_ALIGN], NORTH_X_ALIGN - mov word [ebp + W_NORTH_CONNECTED_MAP + CONN_VIEW_PTR], NORTH_VIEW_PTR - - ; South connection strip header (Route 21) - mov byte [ebp + W_SOUTH_CONNECTED_MAP], MAP_ID_ROUTE_21 - mov word [ebp + W_SOUTH_CONNECTED_MAP + CONN_STRIP_SRC], SOUTH_STRIP_SRC - mov word [ebp + W_SOUTH_CONNECTED_MAP + CONN_STRIP_DEST], SOUTH_STRIP_DEST - mov byte [ebp + W_SOUTH_CONNECTED_MAP + CONN_STRIP_LENGTH], SOUTH_STRIP_LENGTH - mov byte [ebp + W_SOUTH_CONNECTED_MAP + CONN_MAP_WIDTH], SOUTH_CONN_MAP_WIDTH - mov byte [ebp + W_SOUTH_CONNECTED_MAP + CONN_Y_ALIGN], SOUTH_Y_ALIGN - mov byte [ebp + W_SOUTH_CONNECTED_MAP + CONN_X_ALIGN], SOUTH_X_ALIGN - mov word [ebp + W_SOUTH_CONNECTED_MAP + CONN_VIEW_PTR], SOUTH_VIEW_PTR - - ; West / East: no connection. - mov byte [ebp + W_WEST_CONNECTED_MAP], MAP_NO_CONNECTION - mov byte [ebp + W_EAST_CONNECTED_MAP], MAP_NO_CONNECTION - - ; wCurrentTileBlockMapViewPointer = block (0,0) of Pallet Town in wOverworldMap - mov word [ebp + W_CURRENT_TILE_BLOCK_MAP_VIEW_PTR], PALLET_TOWN_VIEW_PTR - - ; Player starts at block (0,0) — WRAM1 is not zeroed by Init, so must initialize - mov byte [ebp + W_Y_BLOCK_COORD], 0 - mov byte [ebp + W_X_BLOCK_COORD], 0 - - ; Player map tile coords (only used by warp/connection logic, not rendering). - ; Start in the middle of Pallet Town so a stroll stays inside the map border. - mov byte [ebp + W_Y_COORD], 8 - mov byte [ebp + W_X_COORD], 8 - - ; Face down, standing still (no in-progress walk). - mov byte [ebp + W_SPRITE_PLAYER_FACING_DIR], SPRITE_FACING_DOWN - mov byte [ebp + W_PLAYER_DIRECTION], 0 - mov byte [ebp + W_PLAYER_MOVING_DIRECTION], 0 - mov byte [ebp + W_SPRITE_PLAYER_Y_STEP_VECTOR], 0 - mov byte [ebp + W_SPRITE_PLAYER_X_STEP_VECTOR], 0 - mov byte [ebp + W_WALK_COUNTER], 0 - - ; --- Player sprite-state slot (slot 0), per home/reset_player_sprite.asm --- - ; The sprite engine (UpdateSprites + PrepareOAMData) drives the player OAM - ; from this state, so it must be initialized for slot 0 to be drawn. - mov byte [ebp + W_SPRITE_PLAYER_PICTURE_ID], 1 ; non-zero → slot in use - mov byte [ebp + W_SPRITE_PLAYER_IMAGE_BASE_OFFSET], 1 ; player VRAM slot - mov byte [ebp + W_SPRITE_PLAYER_Y_PIXELS], 0x60 ; fixed screen Y (96 = center of 200) - mov byte [ebp + W_SPRITE_PLAYER_X_PIXELS], 0xA0 ; fixed screen X (160 = center of 320) - mov byte [ebp + W_SPRITE_PLAYER_IMAGE_INDEX], SPRITE_FACING_DOWN ; facing down, frame 0 - mov byte [ebp + W_SPRITE_PLAYER_INTRA_ANIM], 0 - mov byte [ebp + W_SPRITE_PLAYER_ANIM_FRAME], 0 - mov byte [ebp + W_SPRITE_PLAYER_WALK_ANIM_COUNTER], 0 - mov byte [ebp + W_SPRITE_PLAYER_GRASS_PRIORITY], 0 - - ; Sprite-engine scratch state: no grass tile match, no font in VRAM, no - ; movement flags (spinning / ledge) so the standard walk path runs. - mov byte [ebp + W_GRASS_TILE], 0xFF ; no tile counts as grass - mov byte [ebp + W_FONT_LOADED], 0 - mov byte [ebp + W_MOVEMENT_FLAGS], 0 - - ; The overworld scrolls via RedrawRowOrColumn, not the auto-BG transfer - ; (which would fight it by re-blitting wTileMap to $9800 every frame). - mov byte [ebp + H_AUTO_BG_TRANSFER_EN], 0 - - ; The real player OAM sprite now renders via the sprite engine - ; (UpdateSprites + PrepareOAMData); keep the legacy placeholder marker off. - mov byte [g_player_marker_on], 0 - ; --- Copy overworld.2bpp to ROM window at OW_GFX_GBADDR --- mov esi, overworld_gfx lea edi, [ebp + OW_GFX_GBADDR] @@ -327,20 +332,12 @@ SetupPalletTown: mov ecx, OVERWORLD_BLOCKS_SIZE rep movsb - ; --- Copy PalletTown.blk to ROM window at OW_MAP_GBADDR --- + ; --- Copy map block data to ROM window --- mov esi, pallet_town_blk - lea edi, [ebp + OW_MAP_GBADDR] + lea edi, [ebp + OW_PALLET_BLK_GBADDR] mov ecx, PALLET_TOWN_BLK_SIZE rep movsb - ; --- Copy Overworld_Coll passable-tile list to ROM window at OW_COLL_GBADDR --- - mov esi, overworld_coll - lea edi, [ebp + OW_COLL_GBADDR] - mov ecx, OVERWORLD_COLL_SIZE - rep movsb - - ; --- Copy connected-map block data (Route 1 north, Route 21 south) --- - ; The connection-strip src pointers (NORTH/SOUTH_STRIP_SRC) index into these. mov esi, route1_blk lea edi, [ebp + OW_ROUTE1_BLK_GBADDR] mov ecx, ROUTE1_BLK_SIZE @@ -351,27 +348,71 @@ SetupPalletTown: mov ecx, ROUTE21_BLK_SIZE rep movsb + ; --- Copy Overworld_Coll passable-tile list to ROM window at OW_COLL_GBADDR --- + mov esi, overworld_coll + lea edi, [ebp + OW_COLL_GBADDR] + mov ecx, OVERWORLD_COLL_SIZE + rep movsb + + ; --- Copy map_headers.inc data to ROM window --- + mov esi, map_headers_data + lea edi, [ebp + OW_TILESET_HDR_GBADDR] ; Starts at tileset header + mov ecx, MAP_HEADERS_DATA_SIZE + rep movsb + pop ecx pop edi pop esi ret +; --------------------------------------------------------------------------- +; SetupPlayerSprite — Phase 2 scaffold. +; Initializes the player sprite WRAM variables and starting map. W_CUR_MAP +; must be set here so LoadMapHeader knows which map to load. +; --------------------------------------------------------------------------- +SetupPlayerSprite: + mov byte [ebp + W_CUR_MAP], MAP_ID_PALLET_TOWN + mov byte [ebp + W_Y_COORD], 8 + mov byte [ebp + W_X_COORD], 8 + mov byte [ebp + W_Y_BLOCK_COORD], 0 + mov byte [ebp + W_X_BLOCK_COORD], 0 + mov word [ebp + W_CURRENT_TILE_BLOCK_MAP_VIEW_PTR], PALLET_TOWN_VIEW_PTR + + ; Face down, standing still (no in-progress walk). + mov byte [ebp + W_SPRITE_PLAYER_FACING_DIR], SPRITE_FACING_DOWN + mov byte [ebp + W_PLAYER_DIRECTION], 0 + mov byte [ebp + W_PLAYER_MOVING_DIRECTION], 0 + mov byte [ebp + W_SPRITE_PLAYER_Y_STEP_VECTOR], 0 + mov byte [ebp + W_SPRITE_PLAYER_X_STEP_VECTOR], 0 + mov byte [ebp + W_WALK_COUNTER], 0 + + mov byte [ebp + W_SPRITE_PLAYER_PICTURE_ID], 1 ; non-zero → slot in use + mov byte [ebp + W_SPRITE_PLAYER_IMAGE_BASE_OFFSET], 1 ; player VRAM slot + mov byte [ebp + W_SPRITE_PLAYER_Y_PIXELS], 0x60 ; fixed screen Y (96 = center of 200) + mov byte [ebp + W_SPRITE_PLAYER_X_PIXELS], 0xA0 ; fixed screen X (160 = center of 320) + mov byte [ebp + W_SPRITE_PLAYER_IMAGE_INDEX], SPRITE_FACING_DOWN + mov byte [ebp + W_SPRITE_PLAYER_INTRA_ANIM], 0 + mov byte [ebp + W_SPRITE_PLAYER_ANIM_FRAME], 0 + mov byte [ebp + W_SPRITE_PLAYER_WALK_ANIM_COUNTER], 0 + mov byte [ebp + W_SPRITE_PLAYER_GRASS_PRIORITY], 0 + + mov byte [ebp + W_GRASS_TILE], 0xFF + mov byte [ebp + W_FONT_LOADED], 0 + mov byte [ebp + W_MOVEMENT_FLAGS], 0 + + mov byte [ebp + H_AUTO_BG_TRANSFER_EN], 0 + mov byte [g_player_marker_on], 0 + ret + ; --------------------------------------------------------------------------- ; LoadMapData — faithful translation. ; Pret ref: home/overworld.asm:LoadMapData -; -; Stubs (; TODO-HW or Phase 2 next): -; LoadMapHeader — replaced by SetupPalletTown scaffold -; InitMapSprites — ; TODO: sprite engine (Phase 2) -; RunPaletteCommand(SET_PAL_OVERWORLD) — ; TODO-HW: palette (Phase 5) -; LoadPlayerSpriteGraphics — ; TODO: sprite engine (Phase 2) -; UpdateMusic6Times / PlayDefaultMusicFadeOutCurrent — ; TODO-HW: audio (Phase 3) ; --------------------------------------------------------------------------- LoadMapData: call DisableLCD call ResetMapVariables call LoadTextBoxTilePatterns - ; LoadMapHeader — setup done by SetupPalletTown scaffold; skip here. + call LoadMapHeader ; TODO: InitMapSprites — ; TODO: sprite engine (Phase 2) call LoadScreenRelatedData call CopyMapViewToVRAM @@ -838,7 +879,24 @@ LoadCurrentMapView: push edx ; push de push esi ; push hl + ; STOPGAP (no GB equivalent — remove once map data is extended): the 40×25 + ; viewport is larger than the GB's 20×18, so a player-centered camera near a + ; map edge reaches past wOverworldMap. wOverworldMap ($E580) sits directly + ; above wSurroundingTiles ($E000) in WRAM, so reads above its top border land + ; in the tile buffer and decode tile IDs as block IDs → a garbage band. Any + ; read outside [wOverworldMap, wOverworldMapEnd) instead yields the map's + ; border block, so the extended/out-of-map area renders as clean dummy tiles + ; (matching the in-bounds border) rather than garbage. See CLAUDE.md / TODO.md: + ; the real fix is to extend map data to fill the larger viewport. + cmp edx, W_OVERWORLD_MAP + jb .oobBlock + cmp edx, W_OVERWORLD_MAP + W_OVERWORLD_MAP_SIZE + jae .oobBlock movzx eax, byte [ebp + edx] ; A = block ID from wOverworldMap + jmp .haveBlock +.oobBlock: + movzx eax, byte [ebp + W_MAP_BACKGROUND_TILE] ; dummy = map border block +.haveBlock: mov bl, al ; BL = block ID arg to DrawTileBlock (C) call DrawTileBlock ; writes 4×4 tiles to [EBP+ESI..] ; ECX preserved by DrawTileBlock @@ -916,6 +974,54 @@ LoadCurrentMapView: ; wrap to columns 0-7 of the same VRAM row (handled by the tile-blitter's ; (SCX/8+col)&31 modular addressing). ; --------------------------------------------------------------------------- +; --------------------------------------------------------------------------- +; FillExtraVRAMRows — fill VRAM rows 25-31 with wTileMap row 0. +; +; CopyMapViewToVRAM writes only 25 rows (SCREEN_TILES_H). VRAM rows 25-31 then +; hold stale data from the previous map. On the first scroll step after a map +; transition, H_SCY decreases to 254 and render_bg reads VRAM row 31 — which +; still shows old tiles. Filling rows 25-31 with the top screen row (a neutral +; Route-tileset pattern) prevents that flicker until RedrawRowOrColumn fixes the +; ring normally. +; --------------------------------------------------------------------------- +FillExtraVRAMRows: + push ebx + push esi + push edx + + mov edx, GB_TILEMAP0 + SCREEN_TILES_H * TILEMAP_WIDTH ; VRAM row 25 ($9B20) + mov bh, 32 - SCREEN_TILES_H ; 7 extra rows + +.fer_row: + mov esi, W_TILEMAP ; always re-read from wTileMap row 0 + mov bl, TILEMAP_WIDTH ; 32 tiles +.fer_c1: + mov al, byte [ebp + esi] + mov byte [ebp + edx], al + inc esi + inc edx + dec bl + jnz .fer_c1 + ; wrap last 8 tiles back to start of this VRAM row + sub edx, TILEMAP_WIDTH + mov bl, SCREEN_TILES_W - TILEMAP_WIDTH ; 8 tiles +.fer_c2: + mov al, byte [ebp + esi] + mov byte [ebp + edx], al + inc esi + inc edx + dec bl + jnz .fer_c2 + add edx, TILEMAP_WIDTH - (SCREEN_TILES_W - TILEMAP_WIDTH) ; advance to next row + + dec bh + jnz .fer_row + + pop edx + pop esi + pop ebx + ret + CopyMapViewToVRAM: mov edx, GB_TILEMAP0 ; DE = vBGMap0 = $9800 @@ -989,6 +1095,8 @@ AdvancePlayerSprite: mov al, [ebp + W_X_COORD] add al, cl mov [ebp + W_X_COORD], al + call CheckMapConnections + jc .transitionExit ; CF=1 → map changed, abort frame .afterUpdateMapCoords: cmp byte [ebp + W_WALK_COUNTER], 7 jne .scroll ; only the first frame slides the view @@ -1147,6 +1255,16 @@ AdvancePlayerSprite: pop ecx pop ebx pop eax + clc ; CF=0 → no transition + ret + +.transitionExit: + ; CheckMapConnections set CF=1 → propagate up to caller + pop edx + pop ecx + pop ebx + pop eax + stc ; CF=1 → transition occurred ret ; --------------------------------------------------------------------------- @@ -1515,6 +1633,305 @@ IsTilePassable: stc ret +; --------------------------------------------------------------------------- +; LoadMapHeader — faithful translation. +; Pret ref: home/overworld.asm:LoadMapHeader +; --------------------------------------------------------------------------- +LoadMapHeader: + push eax + push ebx + push ecx + push esi + push edi + + ; W_CUR_MAP_HEADER is a 10-byte buffer: tileset(1), h(1), w(1), blkptr(2), txtptr(2), scrptr(2), conn(1) + movzx eax, byte [ebp + W_CUR_MAP] + add eax, eax ; * 2 (MapHeaderPointers table is 2 bytes per entry) + mov esi, MapHeaderPointers + movzx ebx, word [esi + eax] + add ebx, ebp ; EBX = address of map header in flat space (rom window) + + ; Copy 10 bytes to W_CUR_MAP_HEADER + mov esi, ebx + lea edi, [ebp + W_CUR_MAP_HEADER] + mov ecx, W_CUR_MAP_HEADER_SIZE + rep movsb + + ; Initialize all 4 connected maps to $FF (disabled) before loading actual values. + ; Faithful to pret: home/overworld.asm line 1820-1825. + ; Without this, stale connection data from the previous map persists. + mov byte [ebp + W_NORTH_CONNECTED_MAP], MAP_NO_CONNECTION + mov byte [ebp + W_SOUTH_CONNECTED_MAP], MAP_NO_CONNECTION + mov byte [ebp + W_WEST_CONNECTED_MAP], MAP_NO_CONNECTION + mov byte [ebp + W_EAST_CONNECTED_MAP], MAP_NO_CONNECTION + + ; ESI now points past the 10-byte header. Check connections bitmask. + mov al, [ebp + W_CUR_MAP_CONNECTIONS] + test al, CONNECTION_NORTH + jz .noNorth + mov edi, W_NORTH_CONNECTED_MAP + call CopyMapConnectionHeader +.noNorth: + mov al, [ebp + W_CUR_MAP_CONNECTIONS] + test al, CONNECTION_SOUTH + jz .noSouth + mov edi, W_SOUTH_CONNECTED_MAP + call CopyMapConnectionHeader +.noSouth: + mov al, [ebp + W_CUR_MAP_CONNECTIONS] + test al, CONNECTION_WEST + jz .noWest + mov edi, W_WEST_CONNECTED_MAP + call CopyMapConnectionHeader +.noWest: + mov al, [ebp + W_CUR_MAP_CONNECTIONS] + test al, CONNECTION_EAST + jz .noEast + mov edi, W_EAST_CONNECTED_MAP + call CopyMapConnectionHeader +.noEast: + + ; ESI now points to object_data_ptr + movzx eax, word [esi] + add eax, ebp ; EAX = object data flat address + + ; Read border block + mov bl, [eax] + mov [ebp + W_MAP_BACKGROUND_TILE], bl + inc eax + + ; Skip warps + mov bl, [eax] + mov [ebp + W_NUMBER_OF_WARPS], bl + inc eax + movzx ebx, bl + shl ebx, 2 ; * 4 bytes per warp + add eax, ebx + + ; Skip signs + mov bl, [eax] + mov [ebp + W_NUM_SIGNS], bl + inc eax + movzx ebx, bl + lea ebx, [ebx + ebx * 2] ; * 3 bytes per sign + add eax, ebx + + ; Save object data pointer temp + sub eax, ebp + mov [ebp + W_OBJECT_DATA_PTR_TEMP], ax + + call LoadTilesetHeader + + pop edi + pop esi + pop ecx + pop ebx + pop eax + ret + +CopyMapConnectionHeader: + push ecx + push edi + add edi, ebp + mov ecx, CONN_HEADER_SIZE + rep movsb + pop edi + pop ecx + ret + +; --------------------------------------------------------------------------- +; LoadTilesetHeader — faithful translation. +; Pret ref: home/overworld.asm:LoadTilesetHeader (or engine/overworld/tilesets.asm) +; --------------------------------------------------------------------------- +LoadTilesetHeader: + push eax + push esi + push edi + push ecx + + ; We only have 1 tileset for now: OVERWORLD, located at OW_TILESET_HDR_GBADDR + ; In the future we'll use wCurMapTileset as an index. + lea esi, [ebp + OW_TILESET_HDR_GBADDR] + + lea edi, [ebp + W_TILESET_BANK] + mov ecx, W_TILESET_HEADER_COPY_SIZE ; 11 bytes + rep movsb + + ; 12th byte is hTileAnimations + mov al, [esi] + mov [ebp + H_TILE_ANIMATIONS], al + + pop ecx + pop edi + pop esi + pop eax + ret + +; --------------------------------------------------------------------------- +; CheckMapConnections — faithful translation. +; Pret ref: home/overworld.asm:CheckMapConnections +; --------------------------------------------------------------------------- +CheckMapConnections: + push ebx + push edx + + ; Edge thresholds + mov al, [ebp + W_CUR_MAP_HEIGHT] + add al, al + mov [ebp + W_CURRENT_MAP_HEIGHT_2], al + mov al, [ebp + W_CUR_MAP_WIDTH] + add al, al + mov [ebp + W_CURRENT_MAP_WIDTH_2], al + + ; East connection check + mov al, [ebp + W_X_COORD] + cmp al, [ebp + W_CURRENT_MAP_WIDTH_2] + jne .checkWest + mov al, [ebp + W_EAST_CONNECTED_MAP] + cmp al, MAP_NO_CONNECTION + je .checkWest + mov ebx, W_EAST_CONNECTED_MAP + + mov [ebp + W_CUR_MAP], al + mov al, [ebp + W_EAST_CONNECTED_MAP + CONN_X_ALIGN] + mov [ebp + W_X_COORD], al + mov al, [ebp + W_Y_COORD] + mov cl, al + mov al, [ebp + W_EAST_CONNECTED_MAP + CONN_Y_ALIGN] + add cl, al + mov [ebp + W_Y_COORD], cl + + mov al, [ebp + W_EAST_CONNECTED_MAP + CONN_VIEW_PTR] + mov dl, al + mov al, [ebp + W_EAST_CONNECTED_MAP + CONN_VIEW_PTR + 1] + mov dh, al + + shr cl, 1 + jz .savePointer2 + +.pointerAdjustmentLoop2: + mov al, [ebp + W_EAST_CONNECTED_MAP + CONN_MAP_WIDTH] + add al, MAP_BORDER * 2 + movzx eax, al + add edx, eax + dec cl + jnz .pointerAdjustmentLoop2 +.savePointer2: + mov [ebp + W_CURRENT_TILE_BLOCK_MAP_VIEW_PTR], dx + jmp .loadNewMap + +.checkWest: + mov al, [ebp + W_X_COORD] + cmp al, 255 + jne .checkSouth + mov al, [ebp + W_WEST_CONNECTED_MAP] + cmp al, MAP_NO_CONNECTION + je .checkSouth + mov ebx, W_WEST_CONNECTED_MAP + + mov [ebp + W_CUR_MAP], al + mov al, [ebp + W_WEST_CONNECTED_MAP + CONN_X_ALIGN] + mov [ebp + W_X_COORD], al + mov al, [ebp + W_Y_COORD] + mov cl, al + mov al, [ebp + W_WEST_CONNECTED_MAP + CONN_Y_ALIGN] + add cl, al + mov [ebp + W_Y_COORD], cl + + mov al, [ebp + W_WEST_CONNECTED_MAP + CONN_VIEW_PTR] + mov dl, al + mov al, [ebp + W_WEST_CONNECTED_MAP + CONN_VIEW_PTR + 1] + mov dh, al + + shr cl, 1 + jz .savePointer1 + +.pointerAdjustmentLoop1: + mov al, [ebp + W_WEST_CONNECTED_MAP + CONN_MAP_WIDTH] + add al, MAP_BORDER * 2 + movzx eax, al + add edx, eax + dec cl + jnz .pointerAdjustmentLoop1 +.savePointer1: + mov [ebp + W_CURRENT_TILE_BLOCK_MAP_VIEW_PTR], dx + jmp .loadNewMap + +.checkSouth: + mov al, [ebp + W_Y_COORD] + cmp al, [ebp + W_CURRENT_MAP_HEIGHT_2] + jne .checkNorth + mov al, [ebp + W_SOUTH_CONNECTED_MAP] + cmp al, MAP_NO_CONNECTION + je .checkNorth + mov ebx, W_SOUTH_CONNECTED_MAP + + mov [ebp + W_CUR_MAP], al + mov al, [ebp + W_SOUTH_CONNECTED_MAP + CONN_Y_ALIGN] + mov [ebp + W_Y_COORD], al + mov al, [ebp + W_X_COORD] + mov cl, al + mov al, [ebp + W_SOUTH_CONNECTED_MAP + CONN_X_ALIGN] + add cl, al + mov [ebp + W_X_COORD], cl + + mov al, [ebp + W_SOUTH_CONNECTED_MAP + CONN_VIEW_PTR] + mov dl, al + mov al, [ebp + W_SOUTH_CONNECTED_MAP + CONN_VIEW_PTR + 1] + mov dh, al + + shr cl, 1 + jz .savePointer4 + movzx ecx, cl + add edx, ecx +.savePointer4: + mov [ebp + W_CURRENT_TILE_BLOCK_MAP_VIEW_PTR], dx + jmp .loadNewMap + +.checkNorth: + mov al, [ebp + W_Y_COORD] + cmp al, 255 + jne .done + mov al, [ebp + W_NORTH_CONNECTED_MAP] + cmp al, MAP_NO_CONNECTION + je .done + mov ebx, W_NORTH_CONNECTED_MAP + + mov [ebp + W_CUR_MAP], al + mov al, [ebp + W_NORTH_CONNECTED_MAP + CONN_Y_ALIGN] + mov [ebp + W_Y_COORD], al + mov al, [ebp + W_X_COORD] + mov cl, al + mov al, [ebp + W_NORTH_CONNECTED_MAP + CONN_X_ALIGN] + add cl, al + mov [ebp + W_X_COORD], cl + + mov al, [ebp + W_NORTH_CONNECTED_MAP + CONN_VIEW_PTR] + mov dl, al + mov al, [ebp + W_NORTH_CONNECTED_MAP + CONN_VIEW_PTR + 1] + mov dh, al + + shr cl, 1 + jz .savePointer3 + movzx ecx, cl + add edx, ecx +.savePointer3: + mov [ebp + W_CURRENT_TILE_BLOCK_MAP_VIEW_PTR], dx + jmp .loadNewMap + +.done: + pop edx + pop ebx + clc ; CF=0 → no transition + ret + +.loadNewMap: + ; A connection was crossed. Return CF=1 to signal the caller. + pop edx + pop ebx + stc ; CF=1 → transition occurred + ret + ; --------------------------------------------------------------------------- ; Embedded overworld asset data (Phase 2 scaffold). ; gen_overworld_assets.py regenerates these from source binaries. @@ -1532,3 +1949,4 @@ section .rodata %include "assets/npc_girl_still.inc" %include "assets/npc_fisher_still.inc" %include "assets/npc_oak_still.inc" +%include "assets/map_headers.inc" diff --git a/tools/gen_map_headers.py b/tools/gen_map_headers.py new file mode 100644 index 00000000..e58d3bc0 --- /dev/null +++ b/tools/gen_map_headers.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +"""Generate dos_port/assets/map_headers.inc from hardcoded constants. + +Generates relocated map headers, connection structs, and object data stubs +for Pallet Town, Route 1, and Route 21. +""" +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +ASSETS = ROOT / "dos_port" / "assets" + +def get_connection(direction, conn_map_id, offset, cur_width, cur_height, conn_width, conn_height): + _src = 0 + _tgt = offset + 3 + if _tgt < 2: + _src = -_tgt + _tgt = 0 + + if direction == "NORTH": + _blk = conn_width * (conn_height - 3) + _src + _map = _tgt + # Subtract one block row vs pret: pret carries accumulated H_SCY into the + # new map, so its _win was designed for H_SCY≠0. Our port resets H_SCY=0 + # and calls CopyMapViewToVRAM, placing the player (OAM Y=96→screen Y=80, + # tile row 10) at wSurroundingTiles block row 3 from view_start (Y_BC=1 + # adds 2 tile rows → tile 12 → block 3). So view_start = player_wOW_row + # − 3, which is one stride less than pret's formula. + _win = (conn_width + 6) * (conn_height - 1) + 1 + _y = conn_height * 2 - 1 + _x = offset * -2 + _len = cur_width + 3 - offset + if _len > conn_width: _len = conn_width + + elif direction == "SOUTH": + _blk = _src + _map = (cur_width + 6) * (cur_height + 3) + _tgt + _win = conn_width + 7 + _y = 0 + _x = offset * -2 + _len = cur_width + 3 - offset + if _len > conn_width: _len = conn_width + + elif direction == "WEST": + _blk = conn_width * _src + conn_width - 3 + _map = (cur_width + 6) * _tgt + _win = (conn_width + 6) * 2 - 6 + _y = offset * -2 + _x = conn_width * 2 - 1 + _len = cur_height + 3 - offset + if _len > conn_height: _len = conn_height + + elif direction == "EAST": + _blk = conn_width * _src + _map = (cur_width + 6) * _tgt + cur_width + 3 + _win = conn_width + 7 + _y = offset * -2 + _x = 0 + _len = cur_height + 3 - offset + if _len > conn_height: _len = conn_height + + strip_length = _len - _src + + return { + "conn_map_id": conn_map_id, + "blk": _blk, + "map": _map, + "win": _win, + "y": _y, + "x": _x, + "len": strip_length, + "conn_width": conn_width + } + +def main(): + ASSETS.mkdir(parents=True, exist_ok=True) + out_path = ASSETS / "map_headers.inc" + + lines = [ + "; map_headers.inc — generated by tools/gen_map_headers.py. DO NOT EDIT BY HAND.", + "", + "OW_TILESET_HDR_GBADDR equ 0x5400", + "OW_MAP_HEADERS_GBADDR equ 0x540C", + "", + "map_headers_data:" + ] + + # 1. OVERWORLD tileset header (12 bytes) + lines.extend([ + "tileset_header_OVERWORLD:", + " db 0x01 ; bank", + " dw OW_BLOCKS_GBADDR ; blocks", + " dw OW_GFX_GBADDR ; gfx", + " dw OW_COLL_GBADDR ; collision", + " db 0xFF, 0xFF, 0xFF ; counters", + " db 0x52 ; grass tile", + " db 0x02 ; animations", + "" + ]) + + # Map definitions + W_OVERWORLD_MAP = 0xE580 + + maps = { + "PALLET_TOWN": {"id": 0x00, "width": 10, "height": 9, "blk_addr": 0x4E00, "border": 0x0B, "warps": 3, "signs": 4, "sprites": 3}, + "ROUTE_1": {"id": 0x0C, "width": 10, "height": 18, "blk_addr": 0x5000, "border": 0x0B, "warps": 0, "signs": 1, "sprites": 2}, + "ROUTE_21": {"id": 0x20, "width": 10, "height": 45, "blk_addr": 0x5200, "border": 0x43, "warps": 0, "signs": 0, "sprites": 9}, + } + + # connections: map -> list of (direction, target_name, offset) + conns = { + "PALLET_TOWN": [("NORTH", "ROUTE_1", 0), ("SOUTH", "ROUTE_21", 0)], + "ROUTE_1": [("SOUTH", "PALLET_TOWN", 0)], # Omit NORTH Viridian + "ROUTE_21": [("NORTH", "PALLET_TOWN", 0)] # Omit SOUTH Cinnabar + } + + conn_bits = {"NORTH": 0x08, "SOUTH": 0x04, "WEST": 0x02, "EAST": 0x01} + + current_addr = 0x540C + + map_addrs = {} + + # Generate headers and objects + for name, m in maps.items(): + # Header + map_addrs[name] = current_addr + + my_conns = conns.get(name, []) + conn_flags = sum(conn_bits[d] for d, _, _ in my_conns) + + lines.append(f"OW_MAP_HDR_{name} equ 0x{current_addr:04X}") + lines.append(f"map_header_{name}:") + lines.append(f" db 0x00 ; tileset OVERWORLD") + lines.append(f" db {m['height']}, {m['width']} ; height, width") + lines.append(f" dw 0x{m['blk_addr']:04X} ; blocks_ptr") + lines.append(f" dw 0x0000 ; text_ptr") + lines.append(f" dw 0x0000 ; script_ptr") + lines.append(f" db 0x{conn_flags:02X} ; connections") + current_addr += 10 + + # Output connections in N,S,W,E order. + for check_d in ["NORTH", "SOUTH", "WEST", "EAST"]: + for d, tname, offset in my_conns: + if d == check_d: + t = maps[tname] + c = get_connection(d, t["id"], offset, m["width"], m["height"], t["width"], t["height"]) + + strip_src = t["blk_addr"] + c["blk"] + strip_dest = W_OVERWORLD_MAP + c["map"] + view_ptr = W_OVERWORLD_MAP + c["win"] + + lines.append(f" ; {d} connection to {tname}") + lines.append(f" db 0x{c['conn_map_id']:02X} ; map id") + lines.append(f" dw 0x{strip_src:04X} ; strip src") + lines.append(f" dw 0x{strip_dest:04X} ; strip dest") + lines.append(f" db {c['len']} ; strip length") + lines.append(f" db {c['conn_width']} ; connected map width") + lines.append(f" db {c['y']}, {c['x']} ; Y, X align") + lines.append(f" dw 0x{view_ptr:04X} ; view ptr") + current_addr += 11 + + lines.append(f" dw 0x{current_addr + 2:04X} ; object_data_ptr") + current_addr += 2 + + # Object data + lines.append(f"map_object_{name}:") + lines.append(f" db 0x{m['border']:02X} ; border block") + lines.append(f" db {m['warps']} ; warp count") + current_addr += 2 + if m['warps'] > 0: + lines.append(f" times {m['warps'] * 4} db 0 ; warp stubs") + current_addr += m['warps'] * 4 + lines.append(f" db {m['signs']} ; sign count") + current_addr += 1 + if m['signs'] > 0: + lines.append(f" times {m['signs'] * 3} db 0 ; sign stubs") + current_addr += m['signs'] * 3 + lines.append(f" db {m['sprites']} ; sprite count") + current_addr += 1 + # No sprite data + lines.append("") + + # MapHeaderPointers table + max_id = max(m["id"] for m in maps.values()) + lines.append("MapHeaderPointers:") + for i in range(max_id + 1): + found = None + for name, m in maps.items(): + if m["id"] == i: + found = name + break + if found: + lines.append(f" dw OW_MAP_HDR_{found}") + else: + lines.append(f" dw 0x0000 ; map {i} unimplemented") + + lines.append("") + lines.append(f"MAP_HEADERS_DATA_SIZE equ $ - map_headers_data") + lines.append("") + + out_path.write_text("\n".join(lines) + "\n") + print(f"Wrote {out_path}") + +if __name__ == '__main__': + main() diff --git a/tools/render_frame.py b/tools/render_frame.py new file mode 100644 index 00000000..23e0922c --- /dev/null +++ b/tools/render_frame.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Render FRAME.BIN (320x200 palette-indexed back buffer) to a PNG. + +Values 0-3 = DMG shades (green ramp), 4-11 = sprite/extra colors. +Usage: render_frame.py FRAME.BIN out.png +""" +import sys +from PIL import Image + +W, H = 320, 200 + +# DMG green ramp (debug palette) + a few distinct sprite colors so anything +# out of the 0-3 range is visually obvious. +PAL = { + 0: (224, 248, 208), # lightest + 1: (136, 192, 112), + 2: (52, 104, 86), + 3: (8, 24, 32), # darkest + 4: (255, 0, 0), # sprite colors (magenta/red family) — should stand out + 5: (255, 128, 0), + 6: (255, 255, 0), + 7: (0, 255, 0), + 8: (0, 255, 255), + 9: (0, 128, 255), + 10: (128, 0, 255), + 11: (255, 0, 255), +} + +def main(): + data = open(sys.argv[1], "rb").read() + img = Image.new("RGB", (W, H)) + px = img.load() + for y in range(H): + for x in range(W): + i = y * W + x + v = data[i] if i < len(data) else 0 + px[x, y] = PAL.get(v, (255, 0, 255)) + img.save(sys.argv[2]) + print(f"Wrote {sys.argv[2]} ({len(data)} bytes in)") + +if __name__ == "__main__": + main() diff --git a/verify_dump_custom.py b/verify_dump_custom.py new file mode 100644 index 00000000..3cabdd87 --- /dev/null +++ b/verify_dump_custom.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Parse DUMP.BIN and verify map header state after LoadMapHeader.""" +import sys + +def parse_dump(filename): + with open(filename, 'rb') as f: + data = f.read() + + print(f"DUMP.BIN size: {len(data)} bytes") + + # Window 7 (offset 0x1C0) = GB 0xD358 (map header vars) + wram_window = data[0x1C0:0x1C0+64] + + # wCurMap at 0xD35D = offset 0x05 in this window + cur_map = wram_window[0x05] + print(f"\nwCurMap: 0x{cur_map:02X}", end="") + if cur_map == 0x00: print(" (PALLET_TOWN)") + elif cur_map == 0x0C: print(" (ROUTE_1)") + elif cur_map == 0x20: print(" (ROUTE_21)") + else: print(f" (unknown)") + + # wCurrentTileBlockMapViewPointer at 0xD35E = offset 0x06 + view_ptr = wram_window[0x06] | (wram_window[0x07] << 8) + print(f"wCurrentTileBlockMapViewPointer: 0x{view_ptr:04X}") + + # wYCoord/wXCoord at 0xD360/0xD361 = offset 0x08/0x09 + y_coord = wram_window[0x08] + x_coord = wram_window[0x09] + print(f"wYCoord: {y_coord}, wXCoord: {x_coord}") + + # wCurMapHeader at 0xD366 = offset 0x0E (10 bytes) + hdr_offset = 0x0E + tileset = wram_window[hdr_offset] + height = wram_window[hdr_offset + 1] + width = wram_window[hdr_offset + 2] + data_ptr = wram_window[hdr_offset + 3] | (wram_window[hdr_offset + 4] << 8) + conn = wram_window[hdr_offset + 9] + print(f"\nMap Header (wCurMapHeader):") + print(f" Tileset: 0x{tileset:02X} (expected 0x00 = OVERWORLD)") + print(f" Height: 0x{height:02X} (Pallet=09, Route1=12, Route21=2D)") + print(f" Width: 0x{width:02X} (all=0A)") + print(f" DataPtr: 0x{data_ptr:04X}") + print(f" Connections: 0x{conn:02X} (Pallet=0C, Route1=04, Route21=08)") + + # Connection slots: wNorthConnectedMap at 0xD370 = offset 0x18 + north_map = wram_window[0x18] + # wSouthConnectedMap at 0xD37B = offset 0x23 + south_map = wram_window[0x23] + # wWestConnectedMap at 0xD386 = offset 0x2E + west_map = wram_window[0x2E] + # wEastConnectedMap at 0xD391 = offset 0x39 + east_map = wram_window[0x39] + print(f"\nConnection slots:") + print(f" North: 0x{north_map:02X} {'(ROUTE_1)' if north_map == 0x0C else '(disabled)' if north_map == 0xFF else '(?)'}") + print(f" South: 0x{south_map:02X} {'(ROUTE_21)' if south_map == 0x20 else '(PALLET_TOWN)' if south_map == 0x00 else '(disabled)' if south_map == 0xFF else '(?)'}") + print(f" West: 0x{west_map:02X} {'(disabled)' if west_map == 0xFF else '(?)'}") + print(f" East: 0x{east_map:02X} {'(disabled)' if east_map == 0xFF else '(?)'}") + + # Window 8 (offset 0x200) = GB 0xD520 (tileset ptrs) + tset_window = data[0x200:0x200+64] + tset_bank = tset_window[0x0A] # W_TILESET_BANK at 0xD52A = offset 0x0A + blocks_ptr = tset_window[0x0B] | (tset_window[0x0C] << 8) + gfx_ptr = tset_window[0x0D] | (tset_window[0x0E] << 8) + coll_ptr = tset_window[0x0F] | (tset_window[0x10] << 8) + grass_tile = tset_window[0x14] + print(f"\nTileset Header (wTileset*):") + print(f" Bank: 0x{tset_bank:02X}") + print(f" BlocksPtr: 0x{blocks_ptr:04X} (expected 0x4600)") + print(f" GfxPtr: 0x{gfx_ptr:04X} (expected 0x4000)") + print(f" CollPtr: 0x{coll_ptr:04X} (expected 0x4F00)") + print(f" GrassTile: 0x{grass_tile:02X} (expected 0x52)") + + # Window 3 (offset 0x080) = GB 0x4E00 (PalletTown.blk) + blk_window = data[0x080:0x080+64] + print(f"\nPalletTown.blk first 16 bytes: {' '.join(f'{b:02X}' for b in blk_window[:16])}") + + # Validation + print(f"\n--- VALIDATION ---") + errors = 0 + if tileset != 0x00: + print(f"FAIL: tileset should be 0x00"); errors += 1 + if width != 0x0A: + print(f"FAIL: width should be 0x0A"); errors += 1 + if west_map != 0xFF: + print(f"FAIL: west connection should be 0xFF"); errors += 1 + if east_map != 0xFF: + print(f"FAIL: east connection should be 0xFF"); errors += 1 + if blocks_ptr != 0x4600: + print(f"FAIL: blocks ptr should be 0x4600"); errors += 1 + if gfx_ptr != 0x4000: + print(f"FAIL: gfx ptr should be 0x4000"); errors += 1 + if errors == 0: + print("ALL CHECKS PASSED") + else: + print(f"{errors} CHECK(S) FAILED") + +if __name__ == '__main__': + fn = sys.argv[1] if len(sys.argv) > 1 else 'dos_port/DUMP.BIN' + parse_dump(fn)