mirror of
https://github.com/pret/pokeyellow.git
synced 2026-09-29 20:57:05 -05:00
docs: Update architecture for native-width BG renderer and map connections
- Adopt a less faithful route replacing the 256px torus with a native-width (352x256) BG renderer. - Remove obsolete handoff files (loadmapheader_handoff.md, render_opt_handoff.md, render_tier2_plan.md, session_handoff.md) and Research.md. - Add comprehensive plan for the new native-width renderer (Native-width BG renderer (retire the 256px torus)_current.md). - Update CLAUDE.md and TODO.md to document the out-of-map clamp and new architectural goals. - Incorporate map changes (gen_map_headers.py, map_headers.inc, overworld.asm, etc.) to support the updated map loading and transitions for the extended viewport.
This commit is contained in:
52
CLAUDE.md
52
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
34
Research.md
34
Research.md
@@ -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.
|
||||
25
TODO.md
25
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:
|
||||
|
||||
@@ -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.
|
||||
@@ -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 <Map>_Object`.
|
||||
|
||||
**Object data** (`data/maps/objects/<Map>.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_<MAP>_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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
1
dos_port/.gitignore
vendored
1
dos_port/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
DUMP.BIN
|
||||
FRAME.BIN
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
132
dos_port/assets/map_headers.inc
Normal file
132
dos_port/assets/map_headers.inc
Normal file
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
205
tools/gen_map_headers.py
Normal file
205
tools/gen_map_headers.py
Normal file
@@ -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()
|
||||
42
tools/render_frame.py
Normal file
42
tools/render_frame.py
Normal file
@@ -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()
|
||||
99
verify_dump_custom.py
Normal file
99
verify_dump_custom.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user