Karateka Speed Patch by Claude Code Opus 5
==========================================

Makes Karateka run about 1.35x faster on an Apple II with a language card: 1.34x in the
attract-mode fight and 1.37x in real play. It makes four optimizations: a faster sprite
engine, a fast path for sprites drawn without a pixel shift, a faster rectangle fill and a
faster bit-6 column routine. The game draws exactly the same frames as before, only sooner;
with no timer in the game, its pace speeds up by the same amount.

Only the KARATEKA file changes, and the patch installs itself however the game is started:
from LOADER.SYSTEM or by running KARATEKA directly. It needs a language card (64K); without
one the game runs unpatched at its original speed.

FastKarateka.po contains the patched KARATEKA and the original KARATEKA.ORIG and both can run.

How it works
  No game file other than KARATEKA is changed; every patch is applied in memory at run time.
  Each replaced routine leaves RAM (screen and zero page included) exactly as the original
  does. The sprite routines also return the same X and Y; the fills and the bit-6 routine
  also return the same A, X, Y and flags.

  Start-up (runs once)
  1. KARATEKA is the original file, byte for byte, except that its uncompressed first-stage
     JMP $A495 (file offset $13) now jumps to an installer appended at $7280. LOADER.SYSTEM
     and Bitsy Bye both load the whole file at $4000 and jump there, so both routes install it.
  2. The installer saves A, X, Y and the flags (the game's $A495 uses the incoming A), then
     checks for a language card by writing $A5 and $5A to $D400 in bank 2 and reading them
     back. Without one it restores everything and the game runs unpatched. With one it:
       - copies the tables and patch code into bank 2 at $D400-$DFEC; ProDOS 8 keeps its
         kernel in bank 1 and only a marker at $D000 and its QUIT code at $D100-$D3FF in
         bank 2, and Karateka never quits;
       - stashes the new $0A00 page, a copy of the original page and the boot hook at
         $A600, $A700 and $A800, RAM the boot never writes;
       - replaces INC $4F / INC $86 / JMP $0200 at $A517 with a jump to the boot hook, if
         those bytes are exactly as expected;
       - restores the registers and continues at $A495.
  3. The boot hook runs once the game's second stage has unpacked the main engine. It does
     the two INCs it replaced, then:
       - installs the new $0A00 page if the page there is byte-identical to the stashed
         original;
       - replaces LDA #$40 / STA $07 / JMP $BFFA at $0230, the point every level load passes
         through, with LDA $C080 / JMP to the level hook in the language card;
       - jumps to $0200 as the original did.

  After every level load
  4. The level hook sets $07 as the replaced code did and copies nine patch blocks into the
     sprite engine at $1900-$1C7F, which is identical in KARATEKA.1-.5 (patches a few bytes
     apart are merged, with the original bytes filling the gaps). It returns through a
     trampoline at $19DC that switches ROM back on and jumps to $BFFA.

  Sprite drawing (entry points $1903, $1906, $1909, $190C)
  5. The entry wrappers read $C083 twice (bank 2 read/write, since the patch code modifies
     its own loops) and jump into bank 2. A shared exit at $1A73 switches ROM back on and
     restores $03/$04, X and Y before returning to the caller.
  6. The original shifts every sprite byte by 0-6 pixels with a JSR to $1A84, a DEX/BEQ
     dispatch chain and a run of ROL/ROR. The patch uses tables instead: for each shift, one
     page at $D400-$DAFF holds the shifted byte (with bit 7 set) at $00-$7F and the carry to
     the next byte at $80-$FF, both indexed by the byte's low 7 bits. The first-byte mask
     table at $1A34 has bit 7 cleared, since the tables now supply it.
  7. Normal sprites not clipped on the left, and mirrored sprites clipped on neither side, get
     a rewritten per-sprite setup and row loop around those lookups. Mirrored sprites still
     use the game's bit-reverse table at $0900.
  8. Sprites at pixel shift 0 (about 30% of sprite bytes) skip the tables: each byte only
     gets bit 7 set and the carry stays 0. Two bytes at the start of each row are changed
     per sprite, so shift-0 rows branch to a table-free loop and skip the screen read; other
     rows are unaffected.
  9. The remaining sprites (normal ones clipped on the left, mirrored ones clipped on either
     side) run the original row code, with its inner loops redirected to table-driven loops.

  Rectangle fills and the bit-6 column routine ($0A00-$0AFF, installed once at boot)
  10. Fills ($0A00 solid, $0A03 alternating by row): a stub makes bank 2 readable and jumps
      to the setup there. The setup runs the clip routine (the original code, moved within
      the page), picks a loop and patches its operands, then returns to main RAM with ROM
      switched back on. Both loops write two bytes per pass: one reloads the two patterns,
      the other keeps a single pattern in A (64% of the bytes filled in fights use one
      pattern). The original's quirks are kept: $0A03 returns with X as the row loop left
      it, and a negative width skips the fill.
  11. The bit-6 column routine ($0A06) clears bit 6 of column 39 on all 192 lines of the
      drawing page once per frame. The original called a subroutine per byte (JSR/RTS plus a
      pointer write, ~30 cycles a byte); the new one keeps the pointer fixed and steps
      through the six line offsets inline (~15 cycles a byte).

  Memory used
    $D400-$DAFF   language card bank 2: shift tables (7 x 256 bytes)
    $DB00-$DFEC   language card bank 2: patch code (19 bytes free)
    $0A00-$0AFF   main RAM: fills, clip and bit-6 routine (replaces the original page)
    $1900-$1C7F   main RAM: sprite engine, patched in place after every level load
    $A600-$A8FF   main RAM, during start-up only (the first level load overwrites it)

Measured (cycle-counting emulator, same frames compared)
  attract-mode fight:      3.84 -> 5.15 frames/s   (1.34x)
  real play into level 2:  3.51 -> 4.80 frames/s   (1.37x)
The game has no timer, so the whole game (moves, fight pace) runs that much faster too.

Contribution of each optimization
  Built one on top of the other, each measured on the same frames. A contribution is the
  gain that step added, in percentage points of the original speed, so they add up.

  Attract-mode fight (215 frames)
    Optimization            Frame rate   Cycles per frame      Contribution
    Original                3.84 fps     265.4K                -
    1. Sprite engine        4.87 fps     209.3K  (-56.1K)      +26.8%
    2. Rectangle fill       5.00 fps     203.8K   (-5.5K)       +3.4%
    3. Bit-6 column         5.07 fps     200.9K   (-2.9K)       +1.9%
    4. Shift-0 fast path    5.15 fps     197.9K   (-3.0K)       +2.0%
    Sum                     5.15 fps     -67.5K  (-25.4%)      +34.1%  (1.34x)

  Real play into level 2 (439 frames)
    Optimization            Frame rate   Contribution
    Original                3.51 fps     -
    1. Sprite engine        4.52 fps     +28.7%
    2. Rectangle fill       4.63 fps      +3.1%
    3. Bit-6 column         4.69 fps      +1.8%
    4. Shift-0 fast path    4.80 fps      +3.0%
    Sum                     4.80 fps     +36.6%  (1.37x)

  Shift 0 gains more in real play: 32% of sprite bytes are drawn at shift 0 there
  (27% in the demo), and play draws more sprite bytes per frame (~1,220 vs ~1,080).

Verification
  - started via LOADER.SYSTEM and via the BIN file: every frame of a 240 s demo run and of a
    300 s game session is byte-identical to the original
  - sprite engine on this build: 150,000 random draws from a fresh engine and 150,000 in
    sequence with the engine and language card state carried over (all four entry points,
    all clip cases, shifts 0-6, all three draw modes, both pages) against each level's
    original engine: all RAM identical
  - $0A00 page: 50,000 random calls on this build, 245,000 on earlier builds (fills from both
    entries with clipped and off-screen rectangles, one and two patterns, both pages; bit-6
    routine with various $07): all RAM, A, X, Y and flags identical
  - without a language card: runs unpatched, identical frames at the original speed
  - AppleWin 1.31 (Enhanced //e, the image's own ProDOS), started from the KARATEKA BIN file:
    the full attract loop plays through (intro, story, fight, title, second loop)
