SIGS (Strong Independent Girls) - VBXE FX attribute map demo (Atari 8-bit + VBXE FX 1.27, llvm-mos)

FILES
  sigs.xex            program (about 120 KB: start message, code, 94 data chunks loaded by DOS)
  sigs.c              main program
  atari/hw.h          hardware access (VBXE registers, MEMAC window, VBLANK, C stack)
  nmi.s               own NMI (frame counter), RESET, IRQ; palN (colors to the palette)
  vxload.s            INIT routine called by DOS after every data chunk (RLE -> VRAM, palettes)
  sigs.ld             memory map
  assets.py, rle.py, mkxex.py, build.sh   data preparation and XEX assembly
  font20.py           font of the texts (proportional, 20 cells high)
  build/assets.h      generated data for the program (VRAM addresses, scenes, looks, tables)
  render_dump.py      composes a frame from a VRAM / register dump following the manual's rules (previews)
  tools/emu.c         6502 emulator with a VBXE model for CPU time measurement
  sigs_colors.png     still background frame of each of the 28 looks (set colors)
  sigs_motion.png     background motion
  sigs_texts.png      both texts in the font20 font (preview)

WHAT IT SHOWS
  Screen 320 x 224, vertically centered (8 empty lines; ANTIC list: 1 x $70 + 224 lines of mode F).
  12 scenes: one or two characters (knight, pilot, punk, scientist) on the SR Overlay; with two,
  the second one is smaller and stands higher (further away). Every second scene is mirrored horizontally.
  Background: the whole screen is the PF2 of the attribute map cells (40 x 28 squares 8 x 8), generated
  mathematically by the blitter - dynamic plasma and rainbows, constantly moving (the waves rotate,
  get denser and calm down).
  28 looks in turn (background recipe + color set), changed under a full curtain together with the
  characters; 20 of them are plasmas, almost all in two- and three-color sets.
  Lightning bolts run across the screen in different directions (also diagonally), behind the characters.
  Petals fall swaying like leaves: two behind the characters, two in front of them.
  Two texts in turn: "VideoBoardXE by Electron 2006-2026" and "Strong Independent Girls Demo"
  (20 cell rows high) fly from right to left (about 9 s and 7.5 s), waving vertically along a
  sine with a period of about 3.8 s (up to 6 rows hide behind the top and bottom of the screen),
  brightening the background pattern - in the 5th scene, then every 13 scenes, each time the other
  text. The scene with the longer text lasts until the text has flown through completely
  (about 448 frames instead of 400).

START
  Right at the start of loading the file (first segment with INIT) "Starting SIGS..." appears in
  the middle of the DOS screen - visible during the whole loading and preparation of the show
  (textures, lists, scenes: done with the OS and the DOS screen on). Then the screen fades out:
  the luminance of the DOS screen colors (COLOR1, COLOR2, COLOR4) goes down to black in about 0.6 s
  (30 PAL frames, one step every few frames), only then ANTIC and the OS are turned off and the show
  starts. At exit the DOS screen colors are restored.

CONTROLS
  The show moves between scenes by itself. Joystick left/right - previous/next scene,
  FIRE - lightning, ESC - exit to DOS.

TECHNICAL SUMMARY
  Picture layers (from the bottom):
    PF2       background - color from byte 3 of the map cell (plain ANTIC bitmap pixel), palette 2
    PM0, PM1  petals behind the characters
    Overlay   characters (SR 320 x 224, palette 1, index 0 transparent)
    PM2, PM3  petals in front of the characters
    PF1       lightning (lit pixel of the ANTIC hires bitmap, XCOLOR = 01), color from byte 2
    PF0       curtain (PF2 hires mod from byte 1 of the cell), color COLPF0
  Priorities: P0..P3 = $E3 (Overlay over PF2, COLBAK, PF1, PM0, PM1; under PF0, PM2, PM3),
  GTIA PRIOR = $08 (players over PF2 - the background). The curtain is made of PF2 pixels with
  PF2 hires mod: they count as PF0 in the Overlay priorities and collisions, while the GTIA PRIOR
  resolves them as PF2 - so the players would be over the curtain.
  Therefore in the curtain frames the program does not draw a petal any of whose corners (the cell
  under a corner of the 16 x 16 pixels) is already covered even slightly (from the shape field and
  the curtain step: e = v - t or t - v >= 2). Byte 4 of the cell = $90 (playfield palette 2,
  Overlay 1, priority P0). FEATURE: XCOLOR = 01 (written after VIDEO_CONTROL).
  XDL: 8 empty lines, then one record: SR Overlay (OV_STEP 320), map (MAP_STEP 160,
  8 x 8 cells), OV_ATT $91, RPT 223. Two XDLs (XDL0 -> map $36000, XDL1 -> map $34000): the
  attribute map is double buffered - the blitter writes the map of the next frame while the other
  one is displayed; right after the VBI (before VSYNC, when the VBXE reads the XDL) the program
  selects the XDL of the map completed in the previous frame. No cell is ever shown halfway through
  an update, no matter when the blitter finishes. The curtain (byte 1) is written every frame to the
  working map, the lightning gradient (byte 2) to both maps.

  VBXE features used:
    - SR Overlay, two palettes (1: 191 character colors, 2: background, curtains, lightning, petals),
    - attribute map: bytes 1..4 of the cells, priorities P0..P3,
    - PF2 hires mod with XCOLOR = 01 (curtain), PF1 hires bypass (lightning),
    - blitter with a BCB list (MODE 0 copy/fill, 1 transparent, 2 add, 3 OR, 4 AND,
      7 table; negative step within the line = mirror, destination step 4 = one byte of the cell),
    - MEMAC-A (4 KB CPU window at $A000): data, BCB list, color ramps,
    - MEMAC-B (window $4000-$7FFF for ANTIC only): hires bitmap, ANTIC list and players are in VRAM.

  Background (PF2) - generated mathematically:
    - at start the blitter composes 6 textures 128 x 128 (periodic, in VRAM) from 1D tables:
      plasma 1, 2, 3 and "bend" - sums of sine waves in directions (n, m): T[v][u] += t[n u + m v]
      (4 passes of MODE 0 / MODE 2, negative steps for diagonal directions), saw (rainbow bands) and
      circles (waves in 4 directions with a common center - smooth and seamless),
    - every frame byte 3 of the cells = sum of two views of these textures in raycasting mode
      (MODE 0 + MODE 2, destination step 4): texel (u, v) = (u0, v0) + k (sx, sy), k = byte number
      in the band, 8.8, wrapping at 128. The step between rows = 40 (sx, sy) mod 128;
      sx = 819 m + e (819/256 = 3.2 = 128/40), so e (a slow sine - LFO) controls the twist and density
      of the waves, and (u0, v0) drift: the waves flow, rotate, get denser and calm down. The map is in
      7 bands of 4 rows (each band has an exact start), so a change of e gives no jumps. 14 BCBs with
      raycasting containers are at the start of the list; every frame the CPU corrects only the steps
      and starts of one of the two views (alternately).
    - 6 recipes: plasma A, plasma B, denser plasma, rotating rainbow, diagonal rainbow, circles
      (texture pair, directions, LFO: center, amplitude, period; drift). All patterns are continuous
      (no torn edges): the rainbow saw (1..95) is combined only with sets whose ramp repeats every
      95 colors (sunset, cold, three-color sets) - an assert in assets.py checks this.
  Colors: palette 2, indexes 1..190 = ramp of the set (10 points x 19 colors, through black and many
  shades), written only when the look changes (under the curtain; palN reads the ramp directly from VRAM).
  The colors flow without rewriting the palette: the last background BCB (MODE 7, table rot_k from
  bank 4) changes the index v of every cell to 1 + (v - 1 + rot_k) mod 190; the 190 tables are built
  by the blitter at start with one BCB (in place of the texture tables). 28 sets: two-color
  blue + red, blue + green, blue + purple, green + orange, magenta + cyan, blue + pink,
  blue + orange, violet + dark red, green + dark red (pure red, without an orange tint);
  single-color turquoise, green; three-color blue + orange + violet, green + dark red + sea green
  (cycle color, black, color, black, color twice in the ramp - all three are visible in every
  frame) - each in a deep and a lighter version; rainbow sunset, cold. Saturated colors (x 1.25),
  brightness of the points x 0.5 / 0.72 / 0.72 (deep / lighter / rainbow), the brightest tones
  dimmed (above 150 the increase is halved) - the background is darker than the characters.

  Curtain (PF0): 4 shapes (heart, circle, star, wave) - field of cell values 40 x 28; 1 MODE 7 BCB
  per frame (fixed in the list after the background BCBs, the CPU corrects its source, map and table)
  converts them to byte 1 (PF2 hires mod mask) through one of 68 tables (opening, closing x 34 steps),
  selected by the cmask field. Color: COLPF0 = curt_col[set] - bright, contrasting with the covered
  background (palette 2 positions $C0-$CF and $F0).

  Lightning (PF1): 6 shapes x 2 flicker variants, clipped to their areas. Every second frame (two
  bolts alternately): clearing of the previous window, OR of the image to the bitmap, color gradient
  in bytes 2 of the cells (bright head, dark tail). The priority alone hides them behind the
  characters: with XCOLOR = 01 a lit hires pixel counts as PF1, and P0..P3 = $E3 puts the Overlay
  over PF1 - pixel for pixel and at every moment of the frame.

  Text: own proportional font font20.py, drawn on the cell grid (1 letter pixel = 1 cell 8 x 8):
  height 20 cells (capital letters and digits 16, lowercase from the 5th row, descenders g p y 4),
  strokes 3 cells (diagonal 4), arcs from superellipses, every letter has its own width. The letters
  are cells with playfield palette 3 = the same ramp of the set brightened (x 1.8 + 18, table brt
  computed by the program from the ramp in VRAM) - the background pattern and the color flow continue
  in the letters. Data: byte 4 for 20 rows (with 40 empty columns on both sides), a separate band for
  each text, both with the same row width (the longest text): $772DE and $798FE;
  every frame: fill of byte 4 of the whole working map with $90, then a MODE 0 BCB copies the text
  window (one cell further, start row from the sine table txt_y, clipped to the screen); at the end
  twice an empty window (both maps clean). Palette 3 is written in three parts in the first frames
  of opening the curtain of a scene with a text (the letters appear only after the opening); its
  positions $C0-$FF are a copy of palette 2 (curtain, lightning, petals unchanged).

  Characters: 191 colors (palette 1, positions 1..191) chosen by weighted k-means in Lab space - the
  faces (skin x 8, head x 3) get more shades, rare saturated colors (the punk's hair, the stock) get
  their own positions; pixels mapped to the nearest color in Lab. Face features (eyes, brows, mouth)
  sharpened with an unsharp mask (sigma 0.8, x 0.7) inside the head, 2 pixels from the outline.
  Edges darkened in rings from the border (1st pixel x 0.62, 2nd x 0.84, 3rd x 0.95; in the top
  27 % of the height - hair - stronger: x 0.42 / 0.62 / 0.8, with a transition over the next 8 %);
  outlines smoothed in the data (no bright remains of the outline on dark edges),
  composed by the blitter at a scene change (MODE 1; mirror: step -1 from the end of the line).

  Petals: players 0..3, double line, falling pendulum (x = A sin, y = fall - B sin^2),
  5 tilt shapes; player data in a MEMAC-B bank. Writes to the players and HPOS only at the start of
  VBLANK (a dozen or so lines after the VBI, before the picture); the motion for the next frame is
  computed later.

  VRAM map (512 KB):
    $00000 XDL0, $00040 XDL1, $00200 lightning gradients, $01000 BCB list,
    $01400 curtain fields, $02800 curtain tables (68 x 256), $07000 ramps of 28 sets,
    $0B000 source of the rotation tables, texture composition passes, LFO sine, $12000 Overlay 320 x 224,
    $24000, $28000, $2C000, $30000, $3C000, $40000 background textures 128 x 128 (aligned to 16 KB),
    $34000 and $36000 two attribute maps, $38000-$3BFFF ANTIC bank (bitmap, list, players),
    $44000 1D tables for composing the textures (only at start), then 190 color rotation tables,
    $50000-$772DD sprites and lightning images, $772DE-$7BF1D two texts.

REQUIREMENTS
  VBXE FX rev >= $27 (otherwise message and exit), Atari XL/XE 64 KB.
  Memory: program $2800-$67FF, load buffer $6800-$7FFF (chunks up to 6 KB, only during loading),
  C stack from $7FFF down, MEMAC-A window $A000-$AFFF (CPU). PORTB = $FE during the show (OS and
  BASIC off, no memory expansion - required by MEMAC-B). Own NMI/IRQ vectors, RESET returns to the OS.

BUILD
  ./build.sh sheet.jpg      (character sheet; needs llvm-mos in PATH and numpy, scipy, Pillow)

CPU TIME MEASUREMENT (tools/emu.c)
  tools/emu.c runs the real XEX on a 6502 emulator with a model of the VBXE registers and of the
  MEMAC-A window and reports the CPU cycles used in every frame (a PAL frame gives about 21 900
  cycles: 35 568 minus the ANTIC DMA).
  Build:  gcc -O2 -o emu tools/emu.c
  Usage:  llvm-nm build/prog.xex.elf | awk 'NF==3{print $1, $3}' | sort > syms.txt
          ./emu sigs.xex build/chunks.bin syms.txt 3000 <vbl_cnt address hex>
