================================================================================
MAD-65 system firmware - V0.9, build 260911
The two OS ROMs, one per processor
Release date: 2026-09-11
visit: https://mad65.codepainters.com/software/firmware.html
================================================================================

ABOUT

A MAD-65 has two processors and each boots from its own 16 KiB ROM. Together
they are the console's operating system: everything a game can do without
writing its own drivers.

Both ROMs are copied into RAM at boot and run from there at full speed, because
an EPROM cannot keep up with a 14.318 MHz 65C02. The slow reads happen once.


PACKAGE CONTENTS

  cpu_os.bin        CPU OS - 16 KiB, maps to $C000-$FFFF on CPU1
  gpu_os.bin        GPU OS - 16 KiB, maps to $C000-$FFFF on the GPU
  README.txt        this file


WHAT EACH ROM DOES

  cpu_os.bin - the game side.
    The OS API jump table at $FF00 (78 entries), the builders that write
    drawing commands into the ping-pong shared RAM, joystick reading, the
    SN76489 and YM2413 audio routines, the VGM module player, fixed-point
    maths and the 3-D vector library, cartridge loading and bank switching,
    LZ decompression for game data, and the frame loop a game hooks into.

    The image is two 8 KiB halves. $E000-$FFFF is the OS. $C000-$DFFF holds
    the built-in demo and nothing else - so once a cartridge boots, the demo
    never runs and those 8 KiB are extra full-speed RAM for the game
    (CART_HIRAM).

  gpu_os.bin - the drawing side.
    The command interpreter that reads the shared RAM each frame, and the
    renderers behind it: pixels, lines, circles, polygons, text (horizontal
    and vertical), tiles, the sprite blitters (8/16/32/64 px wide, solid or
    comb) and the background rectangle blitter with its RLE decoder, plus
    both fonts and the boot diagnostic.

  With no cartridge fitted the pair boot into the diagnostic demo, which
  exercises sound, vector graphics, scrolling text and the joystick ports,
  and prints each ROM's build stamp on screen. Its music is an original
  theme by Maro Szczerski.


USING THEM

  With madsim (the simulator)
    Drop both files into the roms\ folder next to madsim.exe, replacing what
    is there. madsim picks them up automatically on the next run.

  On real hardware
    Burn each image to its own 16 KiB device - 27C256 EPROM or 28C256 flash -
    and fit it in the ROM socket of the matching board: cpu_os.bin on PCB1,
    gpu_os.bin on PCB2. Both are mapped at $C000-$FFFF on their own bus.
    The 65C02 reset vector is at $FFFC-$FFFD, which is file offset
    $3FFC-$3FFD; the IRQ/BRK vector at $FFFE-$FFFF is at $3FFE-$3FFF.

  The two ROMs are a matched pair. They talk to each other over the shared
  RAM with a protocol that has changed over time, so do not mix a CPU ROM
  from one release with a GPU ROM from another.


THE THREE NUMBERS

  Firmware version    V0.9      hand-maintained, bumped when the firmware
                                gains something worth announcing. Shown on the
                                diagnostic screen as MAD-65 PROTOTYPE V0.9.
  Build stamp         260911    YYMMDD, regenerated automatically at every
                                assembly. Drawn on the boot screen as
                                GPU OS: 260911 and CPU OS: 260911. This is the
                                number to quote in a bug report.
  OS API ABI          v1        FROZEN. The jump table at $FF00 is append-only:
                                retired calls keep their slot as a no-op rather
                                than being removed, so a cartridge built against
                                any V0.x firmware keeps working. This number
                                moves only on a breaking change, independently
                                of the version above.


CHANGELOG

  V0.9 - since 2026-08-22 (current, and still open)

    Opened by the vertical (TATE) text opcodes, and now also carrying the
    comb sprite, the polygon family, CIRCLE16, the transport block and the
    extra cartridge RAM. The rotate-90 asset script is the only thing still
    wanted under this number before V1.0.

    2026-09-11  8 KiB more RAM for games. The CPU ROM is now two halves: the
                OS at $E000-$FFFF, and at $C000-$DFFF the built-in demo and
                nothing else. The demo only runs when no cartridge is fitted,
                so from cart_init on, $C000-$DFFF is plain full-speed RAM that
                belongs to the game - for code as well as data. New constants
                CART_HIRAM / CART_HIRAM_END. The jump table did not move, so
                existing cartridges work unchanged.

                To fit the demo into its half, its song is now stored
                LZ-compressed (6 432 -> 1 029 bytes) and unpacked into RAM at
                boot. The music player itself is untouched.

                api_decrunch ($FFE7) now expands LZ instead of RLE: same call,
                same arguments, same blob header. LZ copies from what it has
                already written, which CPU1 can do because its destination is
                ordinary RAM; the GPU keeps RLE, because its background layer
                cannot be read back. On this project's data LZ packs far
                better - the demo song 6.3x, where RLE managed 1.0x. The call
                also accepts bank $FF, a plain CPU address, so data can be
                unpacked from RAM or ROM as well as from the cartridge.
                Blobs packed for the 260903 RLE decoder must be repacked with
                lz.py; no released game used it.

    2026-09-06  The polygon family ($4C-$4E) can draw an OPEN figure. Bit 7
                of the vertex count selects a polyline - K vertices, K-1
                segments, no closing edge - through the same rotate and
                scale. With the bit clear it behaves exactly as before.

    2026-09-03  Compressed loading, and rectangles on the background layer.
                Three new opcodes ($31 RECT_BG, $32 RECT_BG_RLE, $33 LOAD_RLE)
                and four new API calls put a byte-aligned rectangle of bitmap
                anywhere on the background - which LOAD could never do, being
                a page copy, and a page straddles 5.12 screen rows. The payload
                travels compressed and is expanded by the GPU, so it is the
                shared-RAM traffic that shrinks, and that is the thing which
                actually limits background loading. Measured on the test
                artwork: 2.2x less traffic, so 2.2x less loading time.

                The same rectangle takes a GAP argument, which finishes the
                comb: source row r lands on background row Y + r*(GAP+1). That
                matters on this layer specifically, because the background
                cannot be read back - no OR, no mask, no blending - so writing
                with gaps is the only way to get partial coverage onto it at
                all. Art authored for a comb also stores at half height and
                comes out the right shape, halving both cartridge ROM and
                loading time again.

                api_decrunch does the same job for CPU1's own data: cartridge
                to RAM, expanded on the way, for level maps and tables. The
                rule the whole block follows is compress at the source and
                decompress at the SINK - data bound for the GPU stays
                compressed until it gets there.

                Jump table grows to 78 entries. Nothing existing changed
                behaviour.

    2026-09-01  CIRCLE16 ($4F) and the polygon family ($4C DOT_POLYGON /
                $4D POLYGON / $4E POLYGON16) landed together, filling the
                drawing block $40-$4F. Both draw around a signed-16 centre
                that may sit off-screen and are always clipped; the polygons
                are also rotated and scaled BY THE GPU, moving that per-vertex
                math off CPU1 (worst measured frame: CPU1 80% -> ~63%). Jump
                table grows to 74 entries. $48 DOT_CIRCLE is untouched and
                stays the cheaper dotted, half-res, on-screen-only call.

                Same day: the GPU's frame-overrun behaviour is now a defined
                policy ("the new frame wins") instead of an unspecified edge
                case, with FRAME_BUSY / OVERRUN_CNT to detect and count it.

    2026-08-27  The line family squared off. Solid lines are now drawn at
                full resolution - the staircase on shallow lines is gone - and
                two new opcodes draw a polyline as one chain, which reuses each
                segment's endpoint instead of re-deriving it (~290 cycles saved
                per joint). Every line family now offers the same three calls:
                clamped, clipped, and chained. Two fixes with it: a frame that
                overran could publish a spliced command list, and a chained
                segment could lose its joint.

    2026-08-24  New demo theme, by Maro Szczerski. The built-in tune is now an
                original piece for the YM2413 and one PSG, 19.2 s long, with
                its percussion on the YM2413's hardware rhythm mode instead of
                PSG noise. It plays its 6.4 s intro once and then loops the
                12.8 s body.

                It only fits because of a new offline stream cleaner, which
                shrank the export from 10 886 to 6 432 bytes - 41% - without
                changing a single thing either sound chip does. It drops
                writes that leave chip state unchanged and collapses silent
                frames into waits. The music player itself was not touched:
                it is frozen ABI and C.E.T.A.S. ships on it.

    2026-08-23  Comb ("ghost") sprites. A new opcode blits source row r to
                screen row Y + r*(GAP+1), never writing the rows in between,
                so the background shows through as hard-edged bands. GAP is an
                argument from 0 to 15 rather than a fixed step, so animating
                it stretches and thins the object at once - a dissolve, not a
                zoom.

                The cost is per source row, so it does not grow with GAP and
                actually falls as the object stretches past the screen edges.
                It is bitmap-only by design, which means one sprite slot
                serves both the solid and the ghost version of the same
                artwork. The existing solid blitter is untouched and
                assembles byte-for-byte identically.

    2026-08-23  The built-in demo gained a PSG2 audio test, the horizontal
                text grid gained a row and was centred with a 2 px margin,
                and the vertical text coordinates were reworked to match the
                horizontal ones exactly, so neither needs its own habits.

    2026-08-22  Vertical (TATE) text. Two new opcodes draw text on a 37 x 50
                grid for a monitor stood on its side, using a rotated font
                generated at build time from the same authored glyphs - one
                font, both orientations. The character cell is a signed
                coordinate clipped per glyph row, so text can slide behind
                either edge one pixel column at a time.

                No hardware change and no existing code path altered: a
                horizontal game behaves identically.

  V0.8 - 2026-07-12 to 2026-08-22

    2026-08-19  OS_VERSION centralised in one include, so the diagnostic screen
                and the demo scroller can no longer drift apart. Version
                deliberately unchanged; both ROMs re-assemble byte-identical.
    2026-07-15  CPU/GPU boot signalling fixed - both processors now signal boot
                before ROM shadowing takes effect. Font data updated.
    2026-07-14  GPU code moved to shadow code-RAM, so the GPU runs at full
                speed from SRAM instead of stalling on EPROM reads.
    2026-07-12  On-screen build-date stamp on the boot background, drawn into
                both double-buffered banks. Version bumped V0.5 -> V0.8.
    2026-07-08  OP_HDOT_LINE ($49): a byte-aligned horizontal dotted run
                collapses to a constant memfill - roughly 10-14x faster than
                the generic dot line.
    2026-07-05  LINE_BG and PIXEL_BG removed - setting a single bit needs a
                read-modify-write and the background window is write-only, so
                they could never have worked. Their API slots became reserved
                no-ops so every later address stays fixed. Boot background
                clear fixed: a pre-reset image could survive a reset and
                flicker at 30 Hz.
    2026-07-02  vgm_play_loop: a song can play a one-shot intro once and then
                repeat only its body. VGM writes to the second PSG are ignored
                rather than faulting the song, since that chip is reserved for
                sound effects.

  V0.5 - 2026-06-13 to 2026-07-12

    The build-out of the CPU OS and the API surface games actually use.

    2026-06-22  Second fire button, Amiga-style on DE-9 pin 9, published as
                bit 5 of the joystick state. The lower bits keep their
                positions, so single-fire software is unaffected.
    2026-06-21  Sound-effect routine.
    2026-06-18  gpu_load_cart_bg.
    2026-06-15  64 px-wide sprites, with no change to the sprite format. Same
                day: the cartridge-to-GPU loaders, which stream cartridge pages
                straight into drawing commands and drain a multi-page job
                across frames without blocking.
    2026-06-14  Signed 16-bit screen coordinates on a common top-left origin,
                with a clip policy for anything partly off-screen. New
                point-cloud builder for starfields and particles.
    2026-06-13  Tile rendering; cart_load, the bank-crossing cartridge copy,
                which made every jump-table entry real code; bank-aware VGM
                playback; the VGM module player; the vector/3-D and audio
                routines.
    2026-06-12  First version of the CPU OS.

  V0.1 - 2026-06-04 to 2026-06-13

    The GPU OS from nothing to a usable drawing engine. There was no CPU side
    yet.

    2026-06-09  New pixel opcodes, circle rendering, extended sprite formats.
    2026-06-07  Sprites implemented.
    2026-06-06  LOAD redesigned for full-page destinations; pixel, line and
                text drawing optimized.
    2026-06-05  Line opcodes and the multi-segment LINES instruction.
    2026-06-04  First GPU OS framework and font data.

  Before the OS

    2026-05-26  A checkerboard test ROM that only proved the video path. No
                OS, no command protocol.


WHAT IS PLANNED

  Still open under V0.9
         The rotate-90 asset script for sprites, tiles and backgrounds -
         specced, not yet implemented. DOT_CIRCLE16 (the dotted, full-res
         circle) is a filed candidate.
  V1.0   After a successful bring-up on real hardware. No boards have been
         built yet.

================================================================================
