Docs! Getting ready for more platforms.
This commit is contained in:
parent
b2ba0c33e9
commit
07f18d9ef5
68 changed files with 5526 additions and 5964 deletions
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -3,3 +3,4 @@
|
||||||
*.spc filter=lfs diff=lfs merge=lfs -text
|
*.spc filter=lfs diff=lfs merge=lfs -text
|
||||||
*.spr filter=lfs diff=lfs merge=lfs -text
|
*.spr filter=lfs diff=lfs merge=lfs -text
|
||||||
*.png filter=lfs diff=lfs merge=lfs -text
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -59,9 +59,12 @@ nvram/
|
||||||
|
|
||||||
# Crap I added
|
# Crap I added
|
||||||
stuff/*
|
stuff/*
|
||||||
docs/*
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
||||||
|
# Claude's working notes (design scratch, port write-ups, session state).
|
||||||
|
# NOT the user manual -- that lives in docs/ and IS tracked.
|
||||||
|
claudeDocs/*
|
||||||
|
|
||||||
# AGI game data (Sierra/fan-made AGI games used for testing; never committed)
|
# AGI game data (Sierra/fan-made AGI games used for testing; never committed)
|
||||||
examples/agi/gamedata/
|
examples/agi/gamedata/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,217 +0,0 @@
|
||||||
# AGI port — session handoff
|
|
||||||
|
|
||||||
This document captures the state of the AGI interpreter port as of
|
|
||||||
2026-05-15. Hand it to a fresh Claude Code session to pick up where
|
|
||||||
the previous one left off.
|
|
||||||
|
|
||||||
## Project context
|
|
||||||
|
|
||||||
JoeyLib is a unified C game-dev library targeting Apple IIgs (perf
|
|
||||||
floor / reference), Amiga, Atari ST, MS-DOS. The AGI port is a
|
|
||||||
from-scratch reimplementation of Sierra's Adventure Game Interpreter
|
|
||||||
v2 on top of JoeyLib's surface / draw / input APIs. Written from
|
|
||||||
public AGI format specs only -- NOT derived from ScummVM / NAGI /
|
|
||||||
Sarien (those are GPL'd; JoeyLib is not).
|
|
||||||
|
|
||||||
Test data: KQ3 v2, locally installed at
|
|
||||||
`examples/agi/gamedata/kq3/`. The user owns the game and is not
|
|
||||||
distributing it; gamedata is gitignored.
|
|
||||||
|
|
||||||
## Current phase
|
|
||||||
|
|
||||||
**Phase 1 sub-D** (callback-driven rendering): verified working.
|
|
||||||
Logic.0 + room 45 init paint KQ3's title screen on every supported
|
|
||||||
port. Ego sprite (VIEW 0, Graham) draws on top with priority masking,
|
|
||||||
WASD/arrow keys move him, ESC quits.
|
|
||||||
|
|
||||||
What's next: **Phase 1 sub-E** (input + animation opcodes). The
|
|
||||||
title screen does not auto-advance because the VM still stubs ~150
|
|
||||||
opcodes including `wait`, `set.menu`, `accept.input`, animation
|
|
||||||
timers, etc. Real KQ3 progression past the title requires implementing
|
|
||||||
enough of those to let logic.45 complete its intro and call new.room.
|
|
||||||
|
|
||||||
Phase 2 sub-A onward: parser, NPC actors, sound (NTP / PSG).
|
|
||||||
|
|
||||||
## What works (verified)
|
|
||||||
|
|
||||||
- Resource loader (LOGDIR / PICDIR / VIEWDIR / SNDDIR / VOL.x)
|
|
||||||
- PIC decoder (visual + priority planes, flood fill, vector draw)
|
|
||||||
- VIEW decoder (cel parse + RLE blit + priority masking + mirror)
|
|
||||||
- LOGIC VM: 256 vars, 256 flags, 8-deep call stack
|
|
||||||
- Real handlers: arithmetic (0x00-0x11), OP_IF (0xFF), OP_GOTO
|
|
||||||
(0xFE), OP_NEW_ROOM (0x12/0x13), OP_LOAD_LOGIC (0x14/0x15),
|
|
||||||
OP_CALL (0x16/0x17), OP_LOAD_PIC (0x18), OP_DRAW_PIC (0x19),
|
|
||||||
OP_SHOW_PIC (0x1A), OP_DISCARD_PIC (0x1B), OP_OVERLAY_PIC (0x1C)
|
|
||||||
- All ~180 other action opcodes are stubs (skip their arg bytes
|
|
||||||
via `kActionArgBytes[256]` table)
|
|
||||||
- Host callbacks: fetchLogic / loadPic / drawPic / showPic /
|
|
||||||
discardPic / overlayPic
|
|
||||||
- Per-platform save-under for the ego sprite (no full-stage memcpy
|
|
||||||
per frame; IIgs uses MVN-based codegen)
|
|
||||||
- Cross-platform binary works on all 4 ports (`AGI.EXE`,
|
|
||||||
`AGI.PRG`, `Agi`, IIgs `AGI`)
|
|
||||||
|
|
||||||
## What does NOT work
|
|
||||||
|
|
||||||
- **Title screen never advances.** Stubs the input / timer opcodes
|
|
||||||
the title uses. Workaround: starting-room override (argv[2]).
|
|
||||||
- **No NPC actors.** animate.obj / set.cel / set.loop are stubs.
|
|
||||||
- **No parser.** said() and the word-tokenize opcodes are stubs.
|
|
||||||
- **No sound.** Phase 3 work.
|
|
||||||
- **IIgs run path for AGI** is not wired. `scripts/run-agi.sh iigs`
|
|
||||||
prints an explicit "not yet wired" message describing the missing
|
|
||||||
disk-packager work (need bigger 2mg volume + case-folded ProDOS
|
|
||||||
names + game-data argument to `package-disk.sh`).
|
|
||||||
- **ST / Amiga can't pass argv.** Hatari `--auto` and Amiga
|
|
||||||
startup-sequence don't forward args, so the starting-room override
|
|
||||||
only works on DOS. ST / Amiga AGI starts at the title screen.
|
|
||||||
Fix needs either a config-file stage (`ROOM.TXT` read at startup)
|
|
||||||
or enough title opcodes for the title to advance naturally.
|
|
||||||
|
|
||||||
## How to test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
source toolchains/env.sh
|
|
||||||
|
|
||||||
# Host-side parsers + VM (fast iteration, no emulator)
|
|
||||||
./scripts/test-agi.sh
|
|
||||||
|
|
||||||
# Cross-platform binary in DOSBox
|
|
||||||
./scripts/run-agi.sh dos # title screen
|
|
||||||
./scripts/run-agi.sh dos kq3 1 # jump to room 1
|
|
||||||
./scripts/run-agi.sh dos kq3 3 # jump to room 3
|
|
||||||
|
|
||||||
# Other platforms (no argv support yet, always title)
|
|
||||||
./scripts/run-agi.sh atarist
|
|
||||||
./scripts/run-agi.sh amiga
|
|
||||||
./scripts/run-agi.sh iigs # prints "not wired" message
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files touched (this session)
|
|
||||||
|
|
||||||
New:
|
|
||||||
|
|
||||||
- `src/core/random.c` — xorshift32 PRNG (jlRandom / jlRandomRange / jlRandomSeed)
|
|
||||||
- `examples/agi/*.c` — agi.c, agiRes.c, agiPic.c, agiView.c, agiVm.c
|
|
||||||
- `examples/agi/agi.h` — all AGI types + public API
|
|
||||||
- `tests/agi/*.c` — testAgiRes / testAgiPic / testAgiView / testAgiVm / testAgiPipeline
|
|
||||||
- `scripts/test-agi.sh` — host-build runner for all five tests
|
|
||||||
- `scripts/run-agi.sh` — cross-platform emulator launcher (dos | amiga | atarist | iigs)
|
|
||||||
|
|
||||||
Modified:
|
|
||||||
|
|
||||||
- `include/joey/core.h` — random prototypes
|
|
||||||
- `include/joey/platform.h` — auto-detect from compiler defines
|
|
||||||
- `.gitignore` — examples/agi/gamedata/
|
|
||||||
- All four make/*.mk — added agi to EXAMPLES list
|
|
||||||
|
|
||||||
## Architectural decisions worth remembering
|
|
||||||
|
|
||||||
### Render loop: save-under, not full stage copy
|
|
||||||
|
|
||||||
The naive per-frame `jlSurfaceCopy(stage, gBackdrop)` is 32 KB of
|
|
||||||
memcpy + present overhead -- not acceptable on a 2.8 MHz IIgs. The
|
|
||||||
current code:
|
|
||||||
|
|
||||||
1. `cbShowPic` paints the picture onto the stage ONCE and snapshots
|
|
||||||
it as `gBackdrop`. Sets `gPicReady = true` and invalidates any
|
|
||||||
prior ego save-under.
|
|
||||||
2. Per frame: `jlSpriteRestoreUnder` the last-frame ego rect (puts
|
|
||||||
clean backdrop bytes back), `jlSpriteSaveUnder` the new ego rect,
|
|
||||||
then `agiViewDraw` paints the cel with priority masking.
|
|
||||||
3. Placeholder sprite for save-under is 4x5 tiles = 32x40 stage
|
|
||||||
pixels, sized to cover any AGI v2 ego cel. Compiled via
|
|
||||||
`jlSpriteCompile` so save/restore use the platform's compiled
|
|
||||||
fast path (MVN block-copy on IIgs, etc.) instead of the C
|
|
||||||
interpreter fallback.
|
|
||||||
|
|
||||||
Per-frame memory traffic: ~1.3 KB save+restore vs ~32 KB full copy.
|
|
||||||
|
|
||||||
### VM throttling
|
|
||||||
|
|
||||||
AGI's native cycle is ~6 Hz. Running logic.0 every frame (60 Hz) is
|
|
||||||
6800-byte switch dispatcher × 60 = ~400k opcodes/sec for nothing.
|
|
||||||
`agi.c` now runs runVmCycle once every 10 frames (~6 Hz on 60 Hz
|
|
||||||
refresh, ~5 Hz on 50 Hz PAL). Input polling, ego movement, render
|
|
||||||
still run at full refresh -- only the bytecode interp is throttled.
|
|
||||||
|
|
||||||
Startup pumps the VM until `gPicReady` (or 32 cycles) so the user
|
|
||||||
never sees a flash of COLOR_MISSING before the first room paints.
|
|
||||||
|
|
||||||
### VIEW mirror handling
|
|
||||||
|
|
||||||
Sierra's encoder writes `mirror=1, src=N` on BOTH loops in a mirror
|
|
||||||
pair, where N is the lower-numbered loop. `agiView.c:resolveMirror`
|
|
||||||
must only treat a cel as mirrored when `src != current_loop`. KQ3
|
|
||||||
view 0 hit this directly: both loop 0 (right) and loop 1 (left) had
|
|
||||||
`mirror=1, src=0`, and our original code flipped both -- so pressing
|
|
||||||
left and right both faced left. Fixed.
|
|
||||||
|
|
||||||
### Starting-room override
|
|
||||||
|
|
||||||
`agi.c` argv[2] = starting room number 1..255. When set,
|
|
||||||
`runVmCycle`'s NEW_ROOM handler substitutes the override for
|
|
||||||
`vm.newRoomId` on the FIRST NEW_ROOM halt only (one-shot). Lets the
|
|
||||||
user skip past KQ3's title room (45) into the actual game. Once the
|
|
||||||
override fires, subsequent transitions go through unchanged.
|
|
||||||
|
|
||||||
`scripts/run-agi.sh` accepts `<platform> [game] [room]` and forwards
|
|
||||||
the room to DOS via dosbox `-c "AGI.EXE . <room>"`. ST and Amiga
|
|
||||||
can't pass argv via autostart so their AGI starts at the title.
|
|
||||||
|
|
||||||
### Default game dir is "." not "agidemo"
|
|
||||||
|
|
||||||
`resolveGameDir` defaults to "." (CWD). On platforms whose autostart
|
|
||||||
can't pass argv (Hatari, AmigaDOS), `run-agi.sh` stages the AGI
|
|
||||||
binary plus the v2 game data files (LOGDIR / PICDIR / etc.) flat in
|
|
||||||
a temp directory and points the emulator at it; AGI runs from CWD =
|
|
||||||
that temp dir and finds the data files relative to "./".
|
|
||||||
|
|
||||||
## Likely next-step work
|
|
||||||
|
|
||||||
The user's natural next ask is "implement enough title opcodes to
|
|
||||||
let KQ3's title screen auto-advance." That probably means:
|
|
||||||
|
|
||||||
- `wait` (cycle.time gate)
|
|
||||||
- `input.line` / `accept.input`
|
|
||||||
- Animation timers: `cycle.time`, `current.loop`, etc.
|
|
||||||
- `set.menu` / `disable.item` (KQ3 title shows a menu)
|
|
||||||
|
|
||||||
Alternative path: skip the title work and implement enough core
|
|
||||||
opcodes to make a gameplay room functional. animate.obj,
|
|
||||||
set.view.v, set.cel, set.loop, position, draw, erase. That gets us
|
|
||||||
walking around KQ3 room 1 (Manannan's house) with Graham instead
|
|
||||||
of a static title.
|
|
||||||
|
|
||||||
The user should pick which to prioritize.
|
|
||||||
|
|
||||||
## Memory entries you should know about
|
|
||||||
|
|
||||||
The repo's auto-memory at
|
|
||||||
`~/.claude/projects/-home-scott-claude-joeylib/memory/` includes
|
|
||||||
JoeyLib-wide context (CMake quirks, IIgs toolchain gotchas, perf
|
|
||||||
constraints) plus `project_joeylib_agi_plan.md` which describes the
|
|
||||||
overall AGI port plan. All persist into the next session
|
|
||||||
automatically; no need to reload.
|
|
||||||
|
|
||||||
Specifically relevant to the AGI port:
|
|
||||||
|
|
||||||
- `project_joeylib_agi_plan.md` — overall plan, 3-4 month estimate
|
|
||||||
- AGI code uses a manual `parseArgU8` instead of atoi to keep the
|
|
||||||
IIgs link budget small.
|
|
||||||
- `project_perf_directive.md` — IIgs is the perf floor; every
|
|
||||||
other target must match or beat it
|
|
||||||
- `feedback_iigs_speed.md` — IIgs must be absolute fastest
|
|
||||||
technically possible; the save-under decision was made under
|
|
||||||
this constraint.
|
|
||||||
|
|
||||||
## Git / user preferences
|
|
||||||
|
|
||||||
- The user manages git themselves -- do NOT commit / push / branch
|
|
||||||
on JoeyLib without explicit ask (see memory
|
|
||||||
`feedback_git.md`).
|
|
||||||
- Always `source toolchains/env.sh` before any `make` -- the user
|
|
||||||
has added that to .claude/settings.local.json allow list.
|
|
||||||
- DJGPP / llvm-mos / vasm / m68k-atari-mint paths come from env.sh.
|
|
||||||
- The user does NOT need hand-holding -- they're an expert C dev.
|
|
||||||
Skip excessive explanation; describe what was done and move on.
|
|
||||||
|
|
@ -1,294 +0,0 @@
|
||||||
# IIgs Performance Fix Plan
|
|
||||||
|
|
||||||
Derived from the 2026-06 IIgs render-path audit (per-op 65816 cycle analysis +
|
|
||||||
adversarial verification). Numbers below are verifier-confirmed unless marked
|
|
||||||
"est." Ordered by dependency, then by realistic payoff.
|
|
||||||
|
|
||||||
## Implementation status (2026-06-23)
|
|
||||||
|
|
||||||
All phases implemented and compile/link clean via the clang/llvm-mos w65816
|
|
||||||
toolchain. Every change is
|
|
||||||
correct-by-construction from the verified analysis; the hot-path 65816 asm
|
|
||||||
(Phases 3, 4, 5.1) still needs **pixel validation on hardware/MAME** -- the
|
|
||||||
headless emulator here can't load the 1.6 MB disk image, so I could not observe
|
|
||||||
output. Two micro-opts are left as marked residuals where the phase's value is
|
|
||||||
already captured.
|
|
||||||
|
|
||||||
- **Phase 0 -- DONE.** Removed the vestigial `SEI` + chunk-CLI from
|
|
||||||
`iigsFillRectInner`; added the ~29 ops/sec ceiling + SEI-inflation note to
|
|
||||||
`PERF.md` and the `uber-perf-table` generator.
|
|
||||||
- **Phase 1 -- DONE.** `drawShowcase()` in `examples/uber/uber.c` renders one of
|
|
||||||
each primitive into a labelled grid; the timed benchmark is unchanged (op
|
|
||||||
sequence/args identical, so cross-port hashes still compare); ends green. No
|
|
||||||
live progress bar (it would clobber the deliberately-seeded per-op input
|
|
||||||
states; frozen showcase + joeylog is the fidelity-preserving choice).
|
|
||||||
- **Phase 2 -- DONE.** Two parts: (a) `surfaceMarkDirtyRect` widens single-row
|
|
||||||
(`h==1`) marks inline in C, dropping the cross-segment `iigsMarkDirtyRowsInner`
|
|
||||||
JSL; (b) **`jlDrawPixel` fully inlined on IIgs** (draw.c) -- no `plotPixelNoMark`
|
|
||||||
/ `surfaceMarkDirtyRect` call layers around the single asm plot, mark widened
|
|
||||||
inline via `jlStageGet()` guard. This is the 5-10x path. (The plan's
|
|
||||||
fold-into-asm form is UNSAFE -- `iigsDrawPixelInner` can't see `s`, would mark
|
|
||||||
`gStage` for heap-surface draws; the C inline keeps the guard.)
|
|
||||||
- **Phase 3 -- DONE.** Computed-jump slam for ALL middle widths (`friMidSlamTbl`
|
|
||||||
via an **RTS computed-goto** + X bias -- ascending table kept, no bank-0 vector
|
|
||||||
hazard; `nWords==0` lands on the odd-byte writer). Partial fills go 7 -> 3
|
|
||||||
cyc/byte. Plus the `friMidMvn` X-reuse cleanup (now dead code path removed).
|
|
||||||
- **Phase 4 -- DONE (4.1 residual).** 4.2: removed the per-plot `dcSavedCol`
|
|
||||||
round-trip (parity via free X). 4.3: the 4 octant row bases are maintained
|
|
||||||
incrementally (+/-160 per step) instead of 4 LUT lookups/iteration. 4.1 (move
|
|
||||||
remaining `dc*` scratch to direct page) is the **residual**: ~4% more after
|
|
||||||
4.2+4.3, but needs a stack-frame-locals refactor to resolve the `D=SP+8` arg
|
|
||||||
conflict -- disproportionate blind-refactor risk for the gain.
|
|
||||||
- **Phase 5 -- DONE.** 5.1: `iigsSurfaceClearFastInner` -- post-init PHA stack-slam
|
|
||||||
(2 cyc/byte, ~1.5x) with AUXWRITE redirect to bank $01; SHR shadow already
|
|
||||||
inhibited by halInit; SP restored before CLI; the C macro dispatches here and
|
|
||||||
keeps the STA `iigsSurfaceClearInner` as the init-safe fallback. (Non-chunked
|
|
||||||
SEI ~23 ms; chunk like the present if audio glitches.) 5.2: `surfaceMarkDirtyAll`
|
|
||||||
is now two `memset`s. 5.3 subsumed by 5.1.
|
|
||||||
- **Phase 6 -- DONE (deep residual).** `jlSpriteSaveAndDraw` already shares
|
|
||||||
`shift`, dodges the MUL4 2D-index, and marks once (the C-level win). The deeper
|
|
||||||
dest-address sharing threaded through `spriteCompiledSaveUnder`/`Draw` is
|
|
||||||
codegen-level surgery for ~1.05x -- left as the **residual**.
|
|
||||||
- **Phase 7 -- DONE.** `docs/DESIGN.md` documents the deliberate double-buffer
|
|
||||||
cost and why single-buffering the IIgs was rejected.
|
|
||||||
|
|
||||||
### Highest hardware-validation priority (new asm, pixel-validate first)
|
|
||||||
|
|
||||||
1. `jlFillRect` at assorted x/w (NOT just 0/320): the computed-goto slam must
|
|
||||||
fill exactly `[x..x+w)` with no over/underrun. Check odd widths + odd x.
|
|
||||||
2. `iigsSurfaceClearFastInner`: the screen must clear to the right color with no
|
|
||||||
stray writes; watch for audio glitches (the ~23 ms SEI window).
|
|
||||||
3. `jlDrawCircle`: shape unchanged after the incremental-row-base rewrite.
|
|
||||||
4. `jlDrawPixel`: visually correct + ops/sec jump.
|
|
||||||
|
|
||||||
### Hardware-validation checklist (run on your MAME/GSplus + the 4 ports)
|
|
||||||
|
|
||||||
1. Build + run `examples/uber` on IIgs; confirm the showcase grid renders all
|
|
||||||
primitives legibly and the benchmark still completes (green exit).
|
|
||||||
2. Re-capture `joeylog.txt` on all four ports; regenerate `PERF.md` via
|
|
||||||
`tools/uber-perf-table`. Expect IIgs `fillRect 320x200` and `stagePresent`
|
|
||||||
to drop toward ~25-28 (SEI removal de-inflates fillRect).
|
|
||||||
3. `tools/diff-uber-hashes` IIgs-vs-{amiga,st,dos}: all ops must still match
|
|
||||||
(the C dirty-mark + memset changes are portable and must not alter pixels).
|
|
||||||
If any op diverges, the dirty-mark fold changed a present extent -- inspect.
|
|
||||||
4. Eyeball the circle (`jlDrawCircle`) output unchanged after the dcSavedCol
|
|
||||||
change; confirm `jlDrawPixel` ops/sec rose.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Ground truth / framing
|
|
||||||
|
|
||||||
- **Hard floor:** any full-screen 32000-byte move = 16000 word-stores x 6 cyc
|
|
||||||
(`STA long,X $9F` or `PEI`, both 3 cyc/byte) / 2.8 MHz = ~34 ms = **~29 ops/sec
|
|
||||||
max**. Clear, full-width fillRect, and present are all pinned to this; you
|
|
||||||
cannot beat it without moving fewer bytes (the dirty-row system already does
|
|
||||||
that for sparse frames).
|
|
||||||
- **The benchmark lies for SEI ops.** `PERF.md` `fillRect 320x200`=60 and
|
|
||||||
`stagePresent`=42 exceed/approach the 29-ops/sec floor and are inflated; clear=28
|
|
||||||
is honest. Fix measurement FIRST (Phase 0) or you cannot tell wins from clock noise.
|
|
||||||
- **Two architectural realities that are NOT bugs:** (a) the back-buffer ->
|
|
||||||
`$E1` design pays the screen twice on full-redraw frames, but the single-buffer
|
|
||||||
"fix" is refuted (slow `$E1`, overdraw inverts the win, tearing) -- keep it; the
|
|
||||||
dirty-row system is the right mitigation. (b) The circle OUTLINE trails the ST
|
|
||||||
due to 65816 register pressure and may never fully win; bank the partial gain.
|
|
||||||
|
|
||||||
## Cross-cutting rules (apply to every asm change)
|
|
||||||
|
|
||||||
- Keep the assembler's register-width state correct after every `REP`/`SEP` so
|
|
||||||
immediates are sized correctly.
|
|
||||||
- Keep each asm routine in the SAME load segment as its C caller (DRAWPRIMS for
|
|
||||||
the draw primitives); a separate segment silently produces calls-to-nowhere.
|
|
||||||
- `gStageMinWord` / `gStageMaxWord` must stay `uint8_t[]` so the asm that walks
|
|
||||||
them per byte stays correctly aligned with the C-side element size.
|
|
||||||
- PHA/PEI stack-slam paths REQUIRE `SEI` and are unsafe during `joeyInit` -- gate
|
|
||||||
them post-init (`gModeSet`).
|
|
||||||
- 4bpp word stores need even byte index (`x/2` even, i.e. `x % 4 == 0`) for the
|
|
||||||
computed-jump slam entry.
|
|
||||||
- Build hygiene: `source toolchains/env.sh`; DELETE the example binary before
|
|
||||||
rebuild (make leaves IIgs binaries stale); pass `spriteEmitIigs.c` first in
|
|
||||||
LIB_SRCS (linker bank-packing order).
|
|
||||||
- **Validation after every change:** re-run `examples/uber`, confirm
|
|
||||||
`jlSurfaceHash` per op is UNCHANGED (pixel-identical to the IIgs golden) and
|
|
||||||
ops/sec moved the right way. Verify visually in MAME (checkpoint poll or
|
|
||||||
snapshot) -- joeyLog alone misses wrong-pixel-but-right-control-flow bugs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 0 -- Make measurement trustworthy (PREREQUISITE)
|
|
||||||
|
|
||||||
Everything downstream is judged against the benchmark; fix it first.
|
|
||||||
|
|
||||||
- **0.1 Audit & likely remove `iigsFillRectInner`'s `SEI`** (joeyDraw.asm:315,
|
|
||||||
chunk-CLI machinery :585-614). The body does only long-mode writes to the
|
|
||||||
bank-$01 back buffer with no SP/shadow hijack (per its own comment :284-292),
|
|
||||||
so the SEI appears vestigial. Confirm nothing in the fill needs interrupt
|
|
||||||
atomicity (MVN is interruptible/resumable by design), then remove SEI + the
|
|
||||||
FRI_CHUNK_ROWS CLI logic. Expected: fillRect-full de-inflates 60 -> ~27
|
|
||||||
(matching clear), AND the audio-starvation risk goes away. If some atomicity IS
|
|
||||||
required, document exactly what and keep it.
|
|
||||||
- **0.2 Add a cycle-ceiling sanity check / annotation.** Document the ~29 ops/sec
|
|
||||||
full-screen floor in `PERF.md`; flag any full-screen op reading above it as
|
|
||||||
suspect. Optional: assert in the harness.
|
|
||||||
- **0.3 Reconcile `present`=42** (its SEI is REAL -- stack/shadow hijack -- so it
|
|
||||||
can't just be removed). Capture an honest present rate via an SEI-immune time
|
|
||||||
source or a diagnostic no-SEI measurement build; expect ~25 ops/sec. Update the
|
|
||||||
table and the cross-port comparison accordingly.
|
|
||||||
- **Re-baseline `PERF.md`** with honest numbers before Phase 2+.
|
|
||||||
|
|
||||||
Exit criterion: clear, fillRect-full, and present all read within ~10% of each
|
|
||||||
other (~25-28 ops/sec), consistent with the cycle model.
|
|
||||||
|
|
||||||
## Phase 1 -- Make the uber demo legible (show, don't smear)
|
|
||||||
|
|
||||||
Right now `examples/uber` shows the user a solid colored screen and teaches them
|
|
||||||
nothing. Cause, concretely:
|
|
||||||
- `runAllTests()` (uber.c:222-285) runs each op in a 16-frame tight loop and
|
|
||||||
NEVER calls `jlStagePresent()` during or between the timed ops. The screen is
|
|
||||||
frozen on the last present before the run (the red-bar-on-black at uber.c:379)
|
|
||||||
for the whole ~80 s run, then ends on a bare `jlSurfaceClear(gStage, 2)`
|
|
||||||
(uber.c:390) -> solid green.
|
|
||||||
- Even if it did present, the timed ops are full-screen or fixed-coordinate
|
|
||||||
(`fillRect 320x200`, `surfaceClear`, pixel@100,100, line 0,50->319,50, ...), so
|
|
||||||
they overwrite each other into one solid color. There is no spatial separation.
|
|
||||||
|
|
||||||
Goal: the demo should visibly demonstrate what each primitive does AND run the
|
|
||||||
benchmark without leaving a meaningless screen. Separate "show the user" from
|
|
||||||
"measure throughput" -- they are different jobs.
|
|
||||||
|
|
||||||
- **1.1 Visual showcase pass (new), runs once before the benchmark, UNTIMED.**
|
|
||||||
Lay out a labeled grid (e.g. 4 cols x 5 rows of ~72x36 cells with a gutter) and
|
|
||||||
render each primitive ONCE into its own cell so nothing overlaps: pixel, line
|
|
||||||
H/V/diag, rect outline, circle (small), fillRect (small), fillCircle, a tile,
|
|
||||||
tileCopyMasked result, the compiled sprite, a 16-entry palette ramp swatch, an
|
|
||||||
SCB per-line strip, a sample-pixel readout. Full-screen ops (surfaceClear,
|
|
||||||
fillRect 320x200, present) cannot be shown at native size in a cell -- represent
|
|
||||||
each with a small labeled swatch and say so. `jlStagePresent()` once, then hold
|
|
||||||
for a readable beat (frame-counted ~2-3 s, or `jlWaitForAnyKey` -> "press a key
|
|
||||||
to begin benchmark"). THIS is the part that makes sense to a user.
|
|
||||||
- **1.2 Benchmark pass: keep timing/hash fidelity, show a sane status screen.**
|
|
||||||
Keep the `timeOp` loop and every op's args + call order EXACTLY as-is -- the
|
|
||||||
cross-port `jlSurfaceHash` golden values depend on the precise op sequence, so
|
|
||||||
changing them invalidates the validation harness. Stop presenting the timing
|
|
||||||
garbage: draw a static "BENCHMARKING..." screen plus a progress indicator (op
|
|
||||||
counter / bar, optionally each op's just-measured ops/sec) updated BETWEEN
|
|
||||||
`timeOp` calls -- never inside the timed loop. End on an explicit "DONE -- press
|
|
||||||
any key" screen, not a bare solid clear.
|
|
||||||
- **1.3 Labels.** `jlDrawText` exists (tile.h:119) but needs a font surface +
|
|
||||||
asciiMap, which uber does not currently load. Either add a small font surface +
|
|
||||||
asciiMap and label each cell, or (fallback) use fixed spatial layout + a short
|
|
||||||
on-screen color-key strip and a legend printed via `jlLogF`.
|
|
||||||
|
|
||||||
Cautions:
|
|
||||||
- All showcase drawing AND the progress-UI updates must be OUTSIDE any timed
|
|
||||||
window -- zero effect on ops/sec.
|
|
||||||
- Do NOT change the timed ops' arguments or sequence (golden-hash stability).
|
|
||||||
- Keep it port-agnostic: uber is shared example code that also runs on
|
|
||||||
Amiga/ST/DOS -- no IIgs-only assumptions in the showcase.
|
|
||||||
- Do this AFTER Phase 0 so any per-op ops/sec shown on the status screen is the
|
|
||||||
corrected (honest) number, not the inflated one.
|
|
||||||
|
|
||||||
Exit criterion: a first-time user watching the demo can see and identify each
|
|
||||||
primitive's output, and the benchmark phase shows readable progress + a clear
|
|
||||||
finish -- never a bare solid color.
|
|
||||||
|
|
||||||
## Phase 2 -- Kill the per-primitive C+JSL tax (biggest real-world win)
|
|
||||||
|
|
||||||
`jlDrawPixel` costs **~1595 cyc/call** (2.8e6 / 1755), ~94% overhead; the actual
|
|
||||||
nibble RMW is ~30 cyc. Every marking primitive pays a second cross-segment JSL.
|
|
||||||
|
|
||||||
- **2.1 Fold dirty-marking into the asm inner loops.** `jlDrawPixel` (draw.c:95),
|
|
||||||
`jlFillRect` (draw.c:448-ish via fillRectOnSurface), line, and circle each call
|
|
||||||
`surfaceMarkDirtyRect` AFTER the plot JSL -- a second JSL into
|
|
||||||
`iigsMarkDirtyRowsInner` (~49 cyc plumbing x2 + the row loop). The inner loops
|
|
||||||
already know the row(s) and extent (fillRect has `midStart`/`trailingByte` per
|
|
||||||
row; pixel has `y` and the byte). Update `gStageMinWord[y]`/`gStageMaxWord[y]`
|
|
||||||
inline and make `surfaceMarkDirtyRect` a no-op on the IIgs fast paths.
|
|
||||||
- Confirmed win: removes ~120 cyc from EVERY marking primitive; **5-10x on
|
|
||||||
`jlDrawPixel`**.
|
|
||||||
- **2.2 Trim the asm prologue/epilogue for the tiny ops.** `iigsDrawPixelInner`
|
|
||||||
(joeyDraw.asm:1158-1227) keeps scratch in 6-cyc long-absolute (`dpxlTmp`,
|
|
||||||
`dpxlNibPart`) and pays full PHP/PHB/PHD ... PLD/PLB/PLP framing around a ~50-cyc
|
|
||||||
plot. Use registers/DP for the scratch; drop framing not needed when DBR/D are
|
|
||||||
already correct. (est ~1.2-1.4x on the inner once 2.1 lands.)
|
|
||||||
- **2.3 Batch/inline entry for pixel-heavy callers (optional).** A span/row-run
|
|
||||||
API (or inlining the in-bounds nibble RMW into the C via macro, the way fillRect
|
|
||||||
was de-wrappered) amortizes the JSL across many pixels for plotting-heavy code.
|
|
||||||
|
|
||||||
Exit criterion: `jlDrawPixel` >> 1755; line/rect call cost drops by the second-JSL
|
|
||||||
amount; hashes unchanged.
|
|
||||||
|
|
||||||
## Phase 3 -- fillRect partial-width slam gate
|
|
||||||
|
|
||||||
The 3-cyc/byte slam fires ONLY for exactly `x==0 && w==320` (`friFullWidth`,
|
|
||||||
joeyDraw.asm:406-420). Everything else falls to seed+`MVN` at **7 cyc/byte**
|
|
||||||
(2.33x slower), confirmed.
|
|
||||||
|
|
||||||
- **3.1 Computed jump into the unrolled `STA long,X` table.** Set `X =
|
|
||||||
curRow + midStart`, then `JMP (addr)` into the 80-store run at store
|
|
||||||
`#(80 - midBytes)`. Gives 3 cyc/byte for ANY width. Keep MVN only for tiny odd
|
|
||||||
remainders. Drop-in safe (bank-$01 long stores, no SEI/shadow). Respect the
|
|
||||||
4bpp even-byte-index rule for the entry. **1.8-2.3x on all partial-width fills.**
|
|
||||||
- **3.2 Cleanups (minor):** cache `curRow+midStart` instead of recomputing it
|
|
||||||
(joeyDraw.asm:542-565); hoist loop-invariant edge offsets (`leadingByte`/
|
|
||||||
`trailingByte` relative to curRow, += 160/row); one `sep`/`rep` window per row
|
|
||||||
for both edge RMWs.
|
|
||||||
|
|
||||||
## Phase 4 -- circle / line register pressure (~1.27x, partial)
|
|
||||||
|
|
||||||
- **4.1 Move Bresenham scratch to direct page.** `dcX/dcY/dcErr/dcRowYP/dcRowYN/
|
|
||||||
dcRowXP/dcRowXN` (circle, joeyDraw.asm:1500+) and the line's per-pixel state
|
|
||||||
(joeyDraw.asm:1253+) are 6-cyc long-absolute; DP is 5 cyc here (DL != 0 because
|
|
||||||
D=SP+8). Set up a DP scratch page for the hot state.
|
|
||||||
- **4.2 Remove the per-plot parity round-trip.** `sta dcSavedCol` / reload /
|
|
||||||
`and #1` (joeyDraw.asm:1603-1610) recovers a bit `lsr a` already left in carry
|
|
||||||
-- branch on carry instead. ~15 cyc/plot x 8 plots/step.
|
|
||||||
- **4.3 Maintain the 4 row bases incrementally** (+/-160 per Bresenham step)
|
|
||||||
instead of 4 LUT lookups/step.
|
|
||||||
- Net ~1.27x (0.65x -> ~0.83x vs ST). Stop there -- the architecture won't give a
|
|
||||||
full win on the outline. The fill-circle already beats the ST; leave it.
|
|
||||||
|
|
||||||
## Phase 5 -- faster clear (post-Phase-0; lower priority)
|
|
||||||
|
|
||||||
- **5.1 Post-init PHA-slam clear path.** `STA long,X` = 3 cyc/byte; SP-into-`$01`
|
|
||||||
PHA-slam = 2 cyc/byte. ~1.5x (37.6 -> ~24 ms). Gate on `gModeSet` (SEI unsafe at
|
|
||||||
init); keep the `STA long,X` path as the init-safe fallback. Chunk the SEI like
|
|
||||||
present to avoid audio starvation. NOTE: only meaningful to measure once Phase 0
|
|
||||||
fixes the clock.
|
|
||||||
- **5.2 Route `surfaceMarkDirtyAll` through `iigsMarkDirtyRowsInner`** (it's a
|
|
||||||
200-iter C loop today; surface.c:233-243) -- one source of truth with the
|
|
||||||
fillRect marker. ~7600 cyc/clear.
|
|
||||||
- **5.3 Hold `fillWord` in a register** across the clear loop instead of
|
|
||||||
`lda 9,s` per iteration (joeyDraw.asm:137). Subsumed if 5.1 lands.
|
|
||||||
|
|
||||||
## Phase 6 -- sprite dispatch overhead
|
|
||||||
|
|
||||||
Body store mode (`LDA #imm16`/`STA abs,Y`) is already near-optimal -- DO NOT touch
|
|
||||||
it. ~3000-4000 cyc/call of C dispatch wraps ~1000-1500 cyc of real blit.
|
|
||||||
|
|
||||||
- **6.1 Share address/shift/fnAddr across `SaveAndDraw`'s two halves** (sprite.c)
|
|
||||||
-- it recomputes dest address, shift, fnAddr, and re-patches the self-modifying
|
|
||||||
stub twice. (est ~1.05x SaveAndDraw.)
|
|
||||||
- **6.2 Trim per-call C dispatch:** cache fnAddr, dodge the 2D-index MUL4, do
|
|
||||||
`isFullyOnSurface` in 8-bit. (est ~1.10-1.15x per sprite op.)
|
|
||||||
|
|
||||||
## Phase 7 -- architecture (decision, no code)
|
|
||||||
|
|
||||||
Keep the back-buffer + dirty-row model. Document in `docs/DESIGN.md` that
|
|
||||||
full-redraw frames inherently pay the screen cost ~2x and that sparse-update
|
|
||||||
(sprite/HUD) games should lean on partial updates. Do NOT pursue single-buffer
|
|
||||||
direct-to-`$E1` (refuted on slow-bank/overdraw/tearing grounds).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Suggested order of execution
|
|
||||||
|
|
||||||
1. Phase 0 (0.1 fillRect SEI, 0.2 annotate, 0.3 present) -> re-baseline PERF.md.
|
|
||||||
2. Phase 1 (make the demo legible) -- so every later change can be eyeballed, not
|
|
||||||
just hashed; cheap and unblocks visual validation.
|
|
||||||
3. Phase 2.1 (fold dirty-marking) -- highest real-world payoff, touches all primitives.
|
|
||||||
4. Phase 3.1 (fillRect computed-jump slam).
|
|
||||||
5. Phase 2.2, Phase 4 (circle/line DP scratch + parity).
|
|
||||||
6. Phase 5, Phase 6 (clear PHA-slam, sprite dispatch) as time permits.
|
|
||||||
7. Phase 7 doc.
|
|
||||||
|
|
||||||
Each step: re-run uber, assert hashes identical, record ops/sec delta in PERF.md.
|
|
||||||
447
LLVM816-ASKS.md
447
LLVM816-ASKS.md
|
|
@ -1,447 +0,0 @@
|
||||||
# Asks for the llvm816 toolchain session
|
|
||||||
|
|
||||||
JoeyLib-side findings that need toolchain-side fixes. Context:
|
|
||||||
PERF-AUDIT.md (findings #79, #77) and PERF-AUDIT-PLAN.md Phase 1.
|
|
||||||
JoeyLib does not modify toolchains/iigs/llvm-mos per the ownership
|
|
||||||
split -- these are requests, with evidence.
|
|
||||||
|
|
||||||
## 1. MVN-based memset/memcpy in runtime/src/libc.c (PERF-AUDIT #79)
|
|
||||||
|
|
||||||
`memset`, `memcpy` (and `memmove`'s copy loops) in
|
|
||||||
`toolchains/iigs/llvm-mos/runtime/src/libc.c` are one-byte-at-a-time
|
|
||||||
loops through 24-bit pointers:
|
|
||||||
|
|
||||||
void *memset(void *dst, int c, size_t n) {
|
|
||||||
char *d = (char *)dst;
|
|
||||||
while (n--) *d++ = (char)c;
|
|
||||||
return dst;
|
|
||||||
}
|
|
||||||
|
|
||||||
That is ~30 cycles/byte on the 65816. The ORCA/C baseline these
|
|
||||||
replaced lowers both to MVN block moves (~7 cycles/byte after setup).
|
|
||||||
|
|
||||||
Measured impact on the reference platform (UBER, honest post-Phase-0
|
|
||||||
clock): jlScbSetRange -- a 200-byte memset plus a flag write --
|
|
||||||
measures 163 ops/sec on the clang build vs 1005 ops/sec on the
|
|
||||||
ORCA-era capture (~4x regression at the whole-op level). Every
|
|
||||||
memset/memcpy-based JoeyLib path pays it: surface copies, sprite
|
|
||||||
save/restore fallbacks, SCB range fills, stage-alloc clears, the log
|
|
||||||
buffer, and all example/game code.
|
|
||||||
|
|
||||||
Ask: MVN-based memset/memcpy in libc.c (or intrinsic lowering in the
|
|
||||||
backend). Requirements from the JoeyLib side:
|
|
||||||
- Handle bank-crossing source/destination (MVN wraps within a bank;
|
|
||||||
the routine must split runs at bank boundaries).
|
|
||||||
- memset needs a 1-byte seed write + overlapping MVN (classic
|
|
||||||
self-copy fill), or an unrolled STA loop for short fills.
|
|
||||||
- Keep the size_t=unsigned long (32-bit) signature -- JoeyLib call
|
|
||||||
sites pass 32-bit counts today and the current behavior is correct,
|
|
||||||
just slow.
|
|
||||||
- A short-count fast path (n < ~16) matters too: the sprite
|
|
||||||
interpreter's per-row copies are 4-17 bytes.
|
|
||||||
|
|
||||||
## 2. FYI: sprite-codegen corruption under investigation (PERF-AUDIT #77)
|
|
||||||
|
|
||||||
JoeyLib's IIgs runtime sprite code generation corrupts memory when
|
|
||||||
enabled (bisected; disabled by default in spriteCompile.c until
|
|
||||||
root-caused). The current suspect list is JoeyLib-side (emit-time
|
|
||||||
buffer handling), but if the hunt lands on codegen/ABI behavior of
|
|
||||||
clang-emitted calls into arena-resident routines, it becomes a
|
|
||||||
toolchain question. Nothing to do yet -- flagged so the context is not
|
|
||||||
a surprise if it arrives.
|
|
||||||
|
|
||||||
## 3. fflush -> FST commit semantics (PERF-AUDIT #80)
|
|
||||||
|
|
||||||
JoeyLib's logger (src/core/debug.c) fflush()es after every line so a
|
|
||||||
crash/hang leaves the log on disk. On the clang runtime the lines do
|
|
||||||
not reach the disk image until fclose: a run that executed 33 logged
|
|
||||||
operations left 3 lines on disk after a forced emulator exit. Question:
|
|
||||||
does the libc fflush propagate to a GS/OS FST flush for the file, or
|
|
||||||
only drain the userland buffer? If the latter, an FST-flush call in
|
|
||||||
fflush (or an exposed fsync-like hook) would restore crash-durable
|
|
||||||
logging. Not urgent -- JoeyLib works around it with a memory mailbox
|
|
||||||
under emulation -- but real-hardware crash forensics wants it.
|
|
||||||
|
|
||||||
## 4. malloc/calloc returns 1 (not NULL) at heap exhaustion (PERF-AUDIT #81) -- CRITICAL
|
|
||||||
|
|
||||||
Empirically confirmed under MAME with in-app probes: with the default
|
|
||||||
link-layout heap (665 bytes at $00:BC67-$00:BF00 in the JoeyLib UBER
|
|
||||||
build), `calloc(1, ~720)` returns **1**. The bump-path overflow branch
|
|
||||||
in libc.c malloc -- whose comment already records a "historical
|
|
||||||
`p + HDR_SZ + n > heapEnd` over-heap miscompile (only manifested for
|
|
||||||
oversized n)" and assumes the path is unexercised -- is still
|
|
||||||
miscompiled in exactly that oversized case. Downstream, JoeyLib ran for
|
|
||||||
weeks on a phantom struct at (void*)1 whose first field happened to
|
|
||||||
alias the real framebuffer pointer; see PERF-AUDIT.md #81 for the full
|
|
||||||
forensics. JoeyLib no longer allocates anything large on the C heap,
|
|
||||||
but ANY program that exhausts this heap gets silent corruption instead
|
|
||||||
of NULL. Minimal repro: link any GS/OS app with default heap, call
|
|
||||||
calloc(1, 720), observe the return value.
|
|
||||||
|
|
||||||
## 5. Segment BSS/heap spans are not reserved from the Memory Manager (PERF-AUDIT #81/#77/#85) -- CRITICAL, EMPIRICALLY PROVEN
|
|
||||||
|
|
||||||
Three independent confirmations, in escalating severity:
|
|
||||||
1. The bank-0 C heap (unreserved) -- #81's phantom-stage chain.
|
|
||||||
2. The pinned SHR framebuffer $01:2000-$9CFF (unreserved) -- an MM
|
|
||||||
handle landed at $01:8B04 inside the display (#85).
|
|
||||||
3. THE GENERAL CASE: a 32KB NewHandle was placed at $08:4200 -- inside
|
|
||||||
the app's own ENTRY-SEGMENT BANK, overlapping seg1's BSS span. The
|
|
||||||
loader only claims each segment's image bytes; everything above
|
|
||||||
(BSS, heap) is MM-free. ANY GS/OS allocation -- the app's own or
|
|
||||||
the system's -- can land on live program data.
|
|
||||||
|
|
||||||
JoeyLib has stopgap claims for cases 1-2 (jlpInit reserves the
|
|
||||||
framebuffer and $00:B900-$BEFF). Case 3 was attempted app-side on
|
|
||||||
2026-07-05 (spacetaxi acceptance test) and the attempt produced two
|
|
||||||
findings you should have:
|
|
||||||
|
|
||||||
(a) The app CAN learn its BSS spans: the linker-emitted
|
|
||||||
__bss_seg{0..3}_{lo16,bank,size} symbols (the ones crt0.s zeroes
|
|
||||||
BSS through) are readable from C via a `.long` DATA32 table in an
|
|
||||||
asm TU -- direct C IMM16 references fail to link ("cross-segment
|
|
||||||
reloc ... uses 2-byte form; only IMM24 / DATA32 supports cINTERSEG
|
|
||||||
patching"). The DATA32 entries resolve as RELOCATED FULL ADDRESSES:
|
|
||||||
the lo16 entry reads back as the 24-bit runtime span base
|
|
||||||
($08:4F00 observed for the spacetaxi build), the size entry
|
|
||||||
carries the length in its low 16 bits, empty segments read as
|
|
||||||
bank-base-only. Recipe verified working end-to-end under MAME.
|
|
||||||
|
|
||||||
(b) BUT actually claiming the span kills the app: NewHandle(
|
|
||||||
attrAddr|attrFixed|attrLocked, base=$08:4F00, size=$6619) from
|
|
||||||
jlpInit SUCCEEDS -- and the app then dies before reaching main's
|
|
||||||
first log line, with the PC profile showing GS/OS desktop idle
|
|
||||||
(silent return-to-Finder). Reproduced twice; reverting only the
|
|
||||||
claim restores the previous behavior. Mechanism unknown from the
|
|
||||||
app side -- presumably the Loader/MM bookkeeping objects to a
|
|
||||||
user handle overlapping a loaded segment's address range, or the
|
|
||||||
claim starves something during the remaining init. The reverted
|
|
||||||
implementation was never committed; the full code (asm table +
|
|
||||||
claim loop) is reproduced verbatim in the copy of this item handed
|
|
||||||
to the toolchain session at /home/scott/claude/llvm816/JOEYLIB-ASKS.md.
|
|
||||||
|
|
||||||
Ask: crt0/omfEmit/loader cooperation so every segment's FULL
|
|
||||||
in-memory span (image + BSS + heap) is Memory-Manager-reserved at
|
|
||||||
load time -- with (b) suggesting the reservation must be made BY the
|
|
||||||
loader glue (or before/while segments are registered), not by app
|
|
||||||
code after the fact. This single fix unblocks IIgs sprite codegen
|
|
||||||
(PERF-AUDIT #77), the Space Taxi IIgs port (a 32KB font-surface
|
|
||||||
jlpBigAlloc lands on live state layout-dependently -- three distinct
|
|
||||||
failure modes observed as the binary layout shifted), and removes the
|
|
||||||
whole corruption class.
|
|
||||||
|
|
||||||
link816 defines [__heap_start, __heap_end) as the leftover entry-bank
|
|
||||||
space above BSS, but crt0Gsos never claims that range from the GS/OS
|
|
||||||
Memory Manager, so NewHandle can legally place blocks on top of live
|
|
||||||
malloc data (a second, independent corruption channel -- and the
|
|
||||||
crt0Gsos comments already record a Phase-1.1 fopen-hang caused by this
|
|
||||||
class). Ask: reserve the heap range at startup (attrAddr|attrBank|
|
|
||||||
attrFixed|attrLocked), or reimplement malloc on top of an MM handle.
|
|
||||||
Related hardening: pass attrNoSpec on NewHandle calls in the runtime,
|
|
||||||
and consider reserving the pinned SHR stage range $01:2000-$9FFF that
|
|
||||||
JoeyLib's jlpStageAllocPixels hands out as a bare constant.
|
|
||||||
|
|
||||||
## 6. Post-main exit path crashes to the monitor (COP $00 at $00:0002) -- EMPIRICALLY PROVEN, app-independent (2026-07-20)
|
|
||||||
|
|
||||||
Every JoeyLib IIgs example that RETURNS from main() dies in the
|
|
||||||
monitor instead of quitting to Finder. Reproduced identically for
|
|
||||||
KEYS and STAXI under MAME (deterministic, ~3 min): press the app's
|
|
||||||
exit key, and execution ends at `COP #$00` executing at $00:0002 with
|
|
||||||
S=$17FD. Nobody saw this before because every harness kills the
|
|
||||||
emulator after reading logs; an interactive ESC was the first real
|
|
||||||
exit ever exercised.
|
|
||||||
|
|
||||||
MAME debugger execution history at the crash (bp at $000002):
|
|
||||||
|
|
||||||
00:9EE0: sta $00c068 ; GS/OS call tail: state reg restore
|
|
||||||
00:9EE8: xce
|
|
||||||
00:9EE9: plp
|
|
||||||
00:9EEA: rtl ; returns to the app-side thunk:
|
|
||||||
08:0072: php ; <- pushes 1 byte...
|
|
||||||
08:0073: rtl ; <- ...then pulls 3: garbage return
|
|
||||||
00:0002: cop #$00 ; monitor
|
|
||||||
|
|
||||||
The thunk at segment offset $0072 (bank $08 at run time) executes
|
|
||||||
`php / rtl` after a GS/OS call returns to it -- the php mis-balances
|
|
||||||
the stack and the rtl returns to $00:0002. This is in the crt0Gsos /
|
|
||||||
runtime exit code (the offset is constant across different apps, so
|
|
||||||
it is runtime code, not app code). Suspects: a Quit stub whose body
|
|
||||||
was meant to sit between the php and the rtl, or a mis-assembled
|
|
||||||
epilogue.
|
|
||||||
|
|
||||||
Reproducer scripts (MAME, headless, breakpoint + history + RAM dump):
|
|
||||||
this session's scratchpad escCrash.sh / escCrashKeys.sh -- ask me and
|
|
||||||
I'll re-stage them anywhere durable.
|
|
||||||
|
|
||||||
WORKAROUND LANDED IN JOEYLIB (so this is no longer user-visible):
|
|
||||||
src/iigs/hal.c jlpShutdown now issues GS/OS QuitGS ($2029, pCount=0)
|
|
||||||
itself via a runtime-built jsl stub and never returns to the runtime
|
|
||||||
exit path. jlShutdown therefore does not return on IIgs. The root
|
|
||||||
cause still deserves a runtime fix: any app that returns from main
|
|
||||||
WITHOUT calling jlShutdown (or any non-JoeyLib program built on this
|
|
||||||
runtime) still crashes.
|
|
||||||
|
|
||||||
## 7. Volatile uint8_t loads through variable pointers widen to 16-bit bus reads (2026-08-18) -- EMPIRICALLY PROVEN
|
|
||||||
|
|
||||||
A `volatile uint8_t` LOAD through a variable (long) pointer is emitted as a
|
|
||||||
16-bit `lda [dp],y` with m=0 and the value masked afterwards (`and #$ff`) --
|
|
||||||
so the BUS performs a phantom read of address+1. For memory that is harmless;
|
|
||||||
for I/O it reads a neighbouring register, and hardware with adjacent registers
|
|
||||||
has read side effects. Concrete damage on the IIgs SCC (Z8530, four adjacent
|
|
||||||
registers $C038-$C03B): a data-register read of channel B ($C03A) phantom-pops
|
|
||||||
channel A's RX FIFO ($C03B -- the AppleTalk port), and in a printer-port
|
|
||||||
configuration a status poll of $C039 phantom-pops the MODEM port's data at
|
|
||||||
$C03A. (A per-poll RR1 harvest also correlates with MAME 0.264 SCC RX-deafness, but
|
|
||||||
that is NOT this bug: it reproduced through a guaranteed 8-bit read too, so
|
|
||||||
the phantom read is refuted as its mechanism and the harvest ships disabled
|
|
||||||
under emulation. The width violation itself stands proven by disassembly.)
|
|
||||||
|
|
||||||
The width handling is inconsistent, which is what makes it look like a bug
|
|
||||||
rather than a policy:
|
|
||||||
|
|
||||||
- STORES through the same pointers are correctly sep #$20-wrapped 8-bit.
|
|
||||||
- LOADS from CONSTANT addresses are correctly sep-wrapped 8-bit
|
|
||||||
(`sep #$20 / lda $c038 / rep #$20` -- verified via a probe TU).
|
|
||||||
- Only variable-pointer loads take the 16-bit `lda [dp],y` + mask form.
|
|
||||||
|
|
||||||
Ask: honour the declared access width for volatile loads in the long-indirect
|
|
||||||
form -- sep-wrap them exactly like the constant-address and store cases.
|
|
||||||
|
|
||||||
WORKAROUND LANDED IN JOEYLIB: src/iigs/serial.c routes every serial register
|
|
||||||
read through a hand-asm `jlpIoRead8` (global function, so no call site can be
|
|
||||||
inlined back into the widened form): `sep #$20 / lda [$e0],y / rep #$30 /
|
|
||||||
and #$ff`. ABI verified from a compiled probe: pointer lo16 in A, bank in X,
|
|
||||||
return in A, $e0-$e3 imaginary-register scratch. Any OTHER volatile I/O read
|
|
||||||
through a variable pointer (input/video HALs, future code) is still exposed
|
|
||||||
until the codegen fix lands.
|
|
||||||
|
|
||||||
## 8. Peeled-first-iteration address negation is one's complement, not two's (2026-08-20) -- FIXED IN TOOLCHAIN same day (see end of item)
|
|
||||||
|
|
||||||
A loop that indexes a local array at a base computed by SUBTRACTING a
|
|
||||||
uint8_t parameter (`k = 8u - nb;` then `t[(uint8_t)(k + i)]` inside a
|
|
||||||
counted loop) is compiled with the FIRST iteration peeled, and that
|
|
||||||
peeled iteration's address is computed as
|
|
||||||
|
|
||||||
base + sign_extend(~nb) ; one byte LOW: ~nb = -nb - 1
|
|
||||||
|
|
||||||
where two's complement (`-nb`) was needed. Every later iteration
|
|
||||||
increments correctly, so exactly the first element is read one byte
|
|
||||||
low and the remaining seven are right -- a silent wrong-VALUES class
|
|
||||||
with no crash, in the same derived-induction-variable family as the
|
|
||||||
modulo-advance item.
|
|
||||||
|
|
||||||
The emitted idiom (RetroNet src/crypto/rnSipHash.c sipRot, w65816
|
|
||||||
-O2 -ffreestanding -ffunction-sections, disassembly of the peeled
|
|
||||||
first read's address):
|
|
||||||
|
|
||||||
lda 0xb6,s ; nb
|
|
||||||
eor #0x1aff ; low byte: nb ^ 0xff = ~nb (high byte junk)
|
|
||||||
and #0xff
|
|
||||||
eor #0x80 ; sign-extend byte
|
|
||||||
clc
|
|
||||||
adc #0xff80 ; (x ^ 0x80) - 0x80
|
|
||||||
...
|
|
||||||
tsc
|
|
||||||
clc
|
|
||||||
adc #0xa3 ; t[] frame base (t + 8 region)
|
|
||||||
clc
|
|
||||||
adc 0x1f,s ; += sext(~nb) --> t + 8 - nb - 1 = t[k-1]
|
|
||||||
|
|
||||||
Blast radius measured before the workaround: EVERY SipHash-2-4 tag the
|
|
||||||
IIgs client computed was wrong (the rotate primitive feeds every round),
|
|
||||||
so every sealed uplink failed server-side authentication. Nothing
|
|
||||||
client-side ever caught it because the protocol's selective encryption
|
|
||||||
leaves downlink frames plaintext -- silent until a peer verifies.
|
|
||||||
Root-caused via on-target known-answer tests + a per-primitive state
|
|
||||||
probe (SIPBLOCK) diffed against the host build of the same source; the
|
|
||||||
"first iteration reads index k-1" model reproduces both on-target wrong
|
|
||||||
tags bit-for-bit end to end.
|
|
||||||
|
|
||||||
WORKAROUND LANDED IN RETRONET: sipRot now takes k (the precomputed read
|
|
||||||
base) instead of nb, removing the 8-nb negation from the function; the
|
|
||||||
emitted first-iteration address becomes a plain base + k and all KATs
|
|
||||||
pass on target. Any other loop whose peeled first iteration derives
|
|
||||||
its address from a subtracted narrow parameter is still exposed.
|
|
||||||
|
|
||||||
RESOLUTION (2026-08-20): root cause was NOT the address lowering -- the
|
|
||||||
IR and ISel were correct (a true NEGA8 pseudo). The AsmPrinter expanded
|
|
||||||
NEGA8 as an UNWRAPPED `EOR #$FF ; INA` on a false "already 8-bit M"
|
|
||||||
assumption: in the real 16-bit-M context the CPU decoded the 2-byte
|
|
||||||
EOR #imm8 as 3-byte EOR #imm16 and swallowed the INA opcode ($1A) as the
|
|
||||||
immediate's high byte -- the disassembly tell is `eor #$1AFF`. FIXED in
|
|
||||||
W65816AsmPrinter.cpp: NEGA8 now emits emitSepM/emitRepM around the pair,
|
|
||||||
exactly like LDA8absX/STA8absX (safe: SepRepCleanup's isMNeutral never
|
|
||||||
extends an M=8 window across the pseudo). Audited: NEGA8 was the only
|
|
||||||
unwrapped Imm8 expansion. Blast radius before the fix: 6 sites in the
|
|
||||||
RetroNet IIgs client -- the SipHash rotate (every sealed uplink failed to
|
|
||||||
authenticate) and 5 rnWidget functions (index corruption; wild-jump BRK
|
|
||||||
during the download offer frame; likely the phantom-FORM garbage too).
|
|
||||||
|
|
||||||
Original ask (kept for the repro): compile the pre-workaround sipRot
|
|
||||||
shape below at -O2 for w65816 and inspect the first read's address chain.
|
|
||||||
(Verbatim pre-workaround function; gSipV is a 40-byte global.)
|
|
||||||
|
|
||||||
```c
|
|
||||||
static void sipRot(uint8_t a, uint8_t nb, uint8_t mode) {
|
|
||||||
uint8_t t[16];
|
|
||||||
uint8_t i;
|
|
||||||
uint8_t k;
|
|
||||||
uint8_t x;
|
|
||||||
|
|
||||||
for (i = 0u; i < 8u; i++) {
|
|
||||||
x = gSipV[(uint8_t)(a + i)];
|
|
||||||
t[i] = x;
|
|
||||||
t[(uint8_t)(i + 8u)] = x;
|
|
||||||
}
|
|
||||||
k = (uint8_t)(8u - nb);
|
|
||||||
for (i = 0u; i < 8u; i++) {
|
|
||||||
x = t[(uint8_t)(k + i)];
|
|
||||||
if (mode == 3u) {
|
|
||||||
x = (uint8_t)((x >> 3) | (uint8_t)(t[(uint8_t)(k + i + 1u)] << 5));
|
|
||||||
} else if (mode == 1u) {
|
|
||||||
x = (uint8_t)((x << 1) | (uint8_t)(t[(uint8_t)(k + i - 1u)] >> 7));
|
|
||||||
}
|
|
||||||
gSipV[(uint8_t)(a + i)] = x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 9. i8 store to a 1-byte stack slot is a 16-bit STA d,s that overflows the return address (2026-08-20) -- FIXED IN TOOLCHAIN same day
|
|
||||||
|
|
||||||
M=16 is the default mode, so `sta d,s` writes TWO bytes. When the target
|
|
||||||
frame object is only 1 byte (an i8 spill / i8 arg-save) at the TOP slot of a
|
|
||||||
frame, the second byte lands on the caller's saved RETURN ADDRESS low byte and
|
|
||||||
the callee's RTL jumps mid-instruction into a different function. Live proof:
|
|
||||||
RetroNet's rnClientOnFileRecord saving its uint8_t `op` clobbered its own
|
|
||||||
return 0x80->0x01, so its RTL landed inside a different rasterizer switch case,
|
|
||||||
skipped the caller's arg cleanup, and left the DISPLAY record-walk's stack
|
|
||||||
pointer 4 low -> $F6 (big-frame pointer) wrong -> the walk byte-crawled forever
|
|
||||||
on the offer frame (the IIgs download "offer-silence": FILE_OFFER never
|
|
||||||
processed, client never drained the serial).
|
|
||||||
|
|
||||||
Minimal repro: `void fwd(uint8_t op, const uint8_t *r){ sink(op, r); }` at -O2
|
|
||||||
for w65816 emits `sta 0x2, s` (16-bit) to save op adjacent to the return.
|
|
||||||
|
|
||||||
FIX (W65816RegisterInfo.cpp eliminateFrameIndex, STAfi case): when
|
|
||||||
`MFI.getObjectSize(FI) == 1`, wrap the STA_StackRel in SEP #$20 / REP #$20 so a
|
|
||||||
1-byte slot gets a genuine 8-bit store. Only the STORE is a hazard; a 16-bit
|
|
||||||
LOAD of the extra byte is a harmless read (the high byte of an i8 value is
|
|
||||||
ignored), so LDAfi is left 16-bit. Verified: fwd now emits
|
|
||||||
`sep #$20 ; sta 0x1,s ; rep #$20`. Same defect family as ASKS #8 (NEGA8) and
|
|
||||||
the $F8 far-frame bank byte -- "an 8-bit operation emitted without the mode
|
|
||||||
wrap in default-16-bit-M code."
|
|
||||||
|
|
||||||
## 10. Pointer-deref to a GLOBAL hardcodes bank 0 (STZ $E2) under the GS/OS Loader (2026-08-20) -- FIXED IN TOOLCHAIN 2026-08-21 (provenance-based; see end)
|
|
||||||
|
|
||||||
The LDAptr / STAptr / STBptr custom inserter (W65816ISelLowering.cpp) lowers a
|
|
||||||
[dp],Y indirect-long deref with the BANK byte forced to 0 via `STZ $E2` (unless
|
|
||||||
the LoaderBankDeref path, which uses $BE, is taken). Bank-0 is correct for a
|
|
||||||
pointer to a STACK local (the 65816 stack is always bank 0) but WRONG for a
|
|
||||||
pointer to a GLOBAL: under the GS/OS Loader the program's BSS/globals are placed
|
|
||||||
in the program bank ($BE, e.g. $0A), not bank 0. So `&someGlobal` dereferenced
|
|
||||||
through this path reads bank-0 garbage, while the SAME global accessed directly
|
|
||||||
via `sta/lda abs` (DBR-relative, DBR=$BE) is correct -- a silent, layout-
|
|
||||||
dependent miscompile. The inserter's own comment already flags this tension
|
|
||||||
(gmtime's __gmtimeBuf hit it).
|
|
||||||
|
|
||||||
Live proof: RetroNet's inlined rnXferRequest materialised `&gX.id` (a static
|
|
||||||
global) and read it through LDAptr; the FILE_REQUEST then carried xferId=0xAD
|
|
||||||
(bank-0 garbage) instead of 1, and the server rejected the download. 0xAD is
|
|
||||||
not any valid gX field -- the tell of a wrong-bank read. Disasm:
|
|
||||||
`lda #<gX.id off> ; sta $e0 ; stz $e2 ; lda [$e0],y`.
|
|
||||||
|
|
||||||
WORKAROUND in app: `noinline` on the function so the global is read in the
|
|
||||||
caller through DBR-relative abs16 and passed by the ABI (same shape as the
|
|
||||||
existing rnXferState workaround).
|
|
||||||
|
|
||||||
Ask: the deref bank cannot be a blanket 0 or blanket $BE when stack pointers
|
|
||||||
(bank 0) and global pointers ($BE) are both derefed through one pseudo. The
|
|
||||||
principled fix is a 24-bit pointer representation (carry the bank in the pointer
|
|
||||||
value) or provenance tracking so a global-derived pointer derefs with $BE and a
|
|
||||||
stack-derived pointer with 0.
|
|
||||||
|
|
||||||
Fixes EVALUATED and rejected from the app side (2026-08-20):
|
|
||||||
1. -w65816-loader-bank-deref (existing flag): whole-program, flips EVERY
|
|
||||||
LDAptr to $BE. Fixes global derefs but breaks stack-pointer derefs (the
|
|
||||||
reason LDAptrBank0 exists for va_arg). Also the runtime *.o are shared
|
|
||||||
across Loader AND non-Loader (GNO/smoke) builds, so it can't be enabled
|
|
||||||
for just the S16/Loader target without a per-mode runtime split.
|
|
||||||
2. Fold (load <materialized global addr>) -> abs16+DBR at selection -- the
|
|
||||||
clean targeted fix, but NOT minimally reproducible: every small repro
|
|
||||||
(inlined forwarder, barrier-forced re-read, struct field) correctly
|
|
||||||
selects abs16. The mis-selection only appears inside a very large inlined
|
|
||||||
function (RetroNet rnFileRecord, ~0x916 bytes, three transfer routines
|
|
||||||
inlined), so it is a scheduling/CSE artifact of address materialization
|
|
||||||
under register pressure, not a stable pattern -- hard to fix safely
|
|
||||||
without provenance.
|
|
||||||
RESOLUTION (2026-08-21, W65816ISelLowering.cpp LDAptr/STAptr/STBptr custom
|
|
||||||
inserter): instead of a blanket bank policy, use PROVENANCE. When the pointer
|
|
||||||
being derefed traces (through COPYs) to an LDAi16imm of a global / external
|
|
||||||
symbol, the deref is exactly a DBR-relative absolute access to that global --
|
|
||||||
the same thing a direct `global` reference compiles to -- so emit LDAabs /
|
|
||||||
STAabs / STA8abs with the global operand and skip the [dp],Y deref entirely.
|
|
||||||
That lands in DBR's bank (correct for a global) with no bank guess, and only
|
|
||||||
GLOBAL-address derefs are touched -- stack pointers (bank 0) and heap/other
|
|
||||||
pointers keep the existing deref path, so no stack-out-param regression. NB the
|
|
||||||
bank canNOT be sourced from $BE: $BE is the code segment's PBR and in a
|
|
||||||
multi-segment program the data (BSS) segment is a different bank; only DBR (via
|
|
||||||
abs) is right. Single-segment builds are unaffected (DBR==0==old STZ bank).
|
|
||||||
Verified: RetroNet's rnXferRequest/rnXferState no longer need `noinline` (both
|
|
||||||
removed) and the inlined rnFileRecord emits zero bank-0 pointer-derefs; the IIgs
|
|
||||||
download DISK-VERIFIES from the pure-clean build. Also fixes the runtime gmtime
|
|
||||||
__gmtimeBuf case this item's parent note describes. What is still NOT covered:
|
|
||||||
a global address that flows through pointer ARITHMETIC the trace can't follow
|
|
||||||
(e.g. `p = &g; p += i; *p`) -- there the provenance is lost and the deref falls
|
|
||||||
back to bank 0; those remain candidates for the 24-bit-pointer rework.
|
|
||||||
|
|
||||||
## 11. ADDframe (stack-slot LEA) scheduled between a CMP and its branch clobbers the flags (2026-08-24) -- FIXED IN TOOLCHAIN same day
|
|
||||||
|
|
||||||
`ADDframe` - the LEA-equivalent that puts the address of a stack slot in A -
|
|
||||||
expands at PEI (W65816RegisterInfo.cpp eliminateFrameIndex) to `TSC; CLC;
|
|
||||||
ADC #disp`. That sequence rewrites N/Z (TSC, ADC), C (CLC, ADC) and V (ADC).
|
|
||||||
The pseudo deliberately carries no `Defs = [P]` (the same reasoning as the
|
|
||||||
LDA pseudos, see the NOTE above LDAi16imm in W65816InstrInfo.td) and is
|
|
||||||
`isReMaterializable`, so a post-RA pass may legally place it between a CMP
|
|
||||||
and the Bxx that consumes the CMP's flags. Machine Copy Propagation did
|
|
||||||
exactly that in RetroNet's src/platform/rnClient.c (main, the widget action
|
|
||||||
dispatch):
|
|
||||||
|
|
||||||
lda 0xf,s ; act.kind
|
|
||||||
cmp #0x3 ; == RN_WA_SUBMIT ?
|
|
||||||
tsc ; \
|
|
||||||
clc ; > ADDframe: &act, hoisted here for the SUBMIT arm
|
|
||||||
adc #0xc1 ; /
|
|
||||||
bne .LBB3_61 ; tests (SP+0xC1) != 0 - ALWAYS taken
|
|
||||||
|
|
||||||
so `act.kind == RN_WA_SUBMIT` could never be true: the LOGIN button
|
|
||||||
activated, rnWidgetActivate wrote kind=3, and rnClientOnSubmit was never
|
|
||||||
reached. On the IIgs the login form was never submitted; the harness's
|
|
||||||
Tab/Return retry loop then produced exactly nine no-widget Return uplinks,
|
|
||||||
which is what pinned it. A scan of every TU in the IIgs client found this
|
|
||||||
ONE instance (`cmp` immediately followed by `tsc` before the branch), which
|
|
||||||
is why only the submit was affected.
|
|
||||||
|
|
||||||
The existing mitigation - the PHP/PLP wrap in W65816StackSlotCleanup.cpp
|
|
||||||
Pass -2.5 - did not catch it for two reasons: (1) ADDframe was not in the
|
|
||||||
pass's corrupting set (isLdaLike), so the backward walk from the branch hit
|
|
||||||
it, took isFlagDefining's default ("anything else defines flags") and called
|
|
||||||
it the TEST, then declined to wrap; (2) the pass only collects BEQ/BNE/BMI/
|
|
||||||
BPL, on the (true) grounds that LDA-like ops leave C alone - but ADDframe
|
|
||||||
also clobbers C and V, so a `cmp; ADDframe; bcc` would have been miscompiled
|
|
||||||
with no protection at all.
|
|
||||||
|
|
||||||
RESOLUTION (2026-08-24, W65816StackSlotCleanup.cpp Pass -2.5):
|
|
||||||
* ADDframe is now in isLdaLike (N/Z-corrupting), so the walk continues
|
|
||||||
past it to the real CMP and wraps it: `cmp #3; php; tsc; clc; adc #disp+1;
|
|
||||||
plp; bne`. It was already in isStackRel and the bump list, so its
|
|
||||||
ImmOffset gets the +1 that compensates PHP's S decrement; the FP-relative
|
|
||||||
exemption in the bump is skipped for ADDframe because it has no FP-
|
|
||||||
relative expansion (always TSC+ADC, i.e. always SP-relative).
|
|
||||||
* BCC/BCS/BVC/BVS are collected as well, with a separate
|
|
||||||
isCarryCorrupting predicate (ADDframe only): under a C/V branch an
|
|
||||||
N/Z-only corrupter is walked past without a wrap, so the size of every
|
|
||||||
existing carry-branch sequence is unchanged.
|
|
||||||
* Pinned by llvm/test/CodeGen/W65816/addframe-flag-wrap.mir (nz, carry,
|
|
||||||
and the lda-under-carry no-wrap case).
|
|
||||||
Verified: the rnClient.c TU rescans clean (zero cmp/tsc/branch sequences in
|
|
||||||
all 20 IIgs client TUs) and the IIgs login submits live - see the RetroNet
|
|
||||||
e2e matrix (`joey-iigs session`).
|
|
||||||
20
Makefile
20
Makefile
|
|
@ -7,6 +7,8 @@
|
||||||
# make amiga builds Commodore Amiga only
|
# make amiga builds Commodore Amiga only
|
||||||
# make atarist builds Atari ST only
|
# make atarist builds Atari ST only
|
||||||
# make dos builds MS-DOS only
|
# make dos builds MS-DOS only
|
||||||
|
# make docs renders the manual to build/docs (HTML + PDF)
|
||||||
|
# make verify-audio runs every port's audio gate (needs emulators)
|
||||||
# make clean removes all build outputs
|
# make clean removes all build outputs
|
||||||
#
|
#
|
||||||
# The build references cross tools exclusively from toolchains/, set up
|
# The build references cross tools exclusively from toolchains/, set up
|
||||||
|
|
@ -35,7 +37,7 @@ ifeq ($(HAVE_DOS),1)
|
||||||
ALL_TARGETS += dos
|
ALL_TARGETS += dos
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: all iigs iigs-disk iigs-verify amiga atarist dos blank-check tools clean help status
|
.PHONY: all iigs iigs-disk iigs-verify amiga atarist dos blank-check tools clean help status verify-audio docs
|
||||||
|
|
||||||
all: $(ALL_TARGETS) tools
|
all: $(ALL_TARGETS) tools
|
||||||
ifeq ($(ALL_TARGETS),)
|
ifeq ($(ALL_TARGETS),)
|
||||||
|
|
@ -69,6 +71,20 @@ dos:
|
||||||
blank-check:
|
blank-check:
|
||||||
@$(REPO_DIR)/scripts/check-blank.sh
|
@$(REPO_DIR)/scripts/check-blank.sh
|
||||||
|
|
||||||
|
# The manual: docs/Manual.adoc plus its chapter includes -> build/docs.
|
||||||
|
# Needs asciidoctor and asciidoctor-pdf, no cross toolchain.
|
||||||
|
# make docs FORMAT=html renders only the web version (much faster).
|
||||||
|
docs:
|
||||||
|
@$(REPO_DIR)/scripts/build-docs.sh $(FORMAT)
|
||||||
|
|
||||||
|
# Every port's audio gate, run sequentially (the recording gates grade
|
||||||
|
# fixed time windows, so a competing emulator can slide a phase boundary).
|
||||||
|
# Exits 1 on a real failure and 3 if a gate SKIPPED for an unbuilt port --
|
||||||
|
# non-zero on purpose, so a run that tested nothing cannot look green.
|
||||||
|
# Takes a subset: make verify-audio PORTS="atarist amiga"
|
||||||
|
verify-audio:
|
||||||
|
@$(REPO_DIR)/scripts/verify-audio-all.sh $(PORTS)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(REPO_DIR)/build
|
rm -rf $(REPO_DIR)/build
|
||||||
|
|
||||||
|
|
@ -83,4 +99,6 @@ help:
|
||||||
@echo "Targets: iigs amiga atarist dos all clean status help"
|
@echo "Targets: iigs amiga atarist dos all clean status help"
|
||||||
@echo " iigs-disk iigs-verify (IIgs MAME visual check)"
|
@echo " iigs-disk iigs-verify (IIgs MAME visual check)"
|
||||||
@echo " blank-check (host-cc smoke test of the new-port template)"
|
@echo " blank-check (host-cc smoke test of the new-port template)"
|
||||||
|
@echo " verify-audio (all 5 audio gates; PORTS=\"atarist amiga\" for a subset)"
|
||||||
|
@echo " docs (manual to build/docs; FORMAT=html for HTML only)"
|
||||||
@echo "Source toolchains/env.sh first (blank-check needs no toolchain)."
|
@echo "Source toolchains/env.sh first (blank-check needs no toolchain)."
|
||||||
|
|
|
||||||
1164
NATIVE-PERF-PLAN.md
1164
NATIVE-PERF-PLAN.md
File diff suppressed because it is too large
Load diff
|
|
@ -1,750 +0,0 @@
|
||||||
# PERF-AUDIT Fix Plan
|
|
||||||
|
|
||||||
Execution plan for all 71 confirmed findings in PERF-AUDIT.md plus the
|
|
||||||
benchmark-harness fix from its preamble. Finding numbers below (#N) are
|
|
||||||
PERF-AUDIT.md numbers. Read each finding's "Verifier correction" before
|
|
||||||
implementing -- several tighten the fix or the claimed impact, and this
|
|
||||||
plan encodes those corrections as constraints.
|
|
||||||
|
|
||||||
This plan was itself adversarially reviewed (completeness, ordering
|
|
||||||
conflicts, gate adequacy -- 31 issues found and folded in). The most
|
|
||||||
important consequence: Phase 0 builds the verification harness that
|
|
||||||
every later gate depends on, because the existing gates cannot see most
|
|
||||||
of the changes this plan makes.
|
|
||||||
|
|
||||||
Ground rules for every phase:
|
|
||||||
- Source toolchains/env.sh before any make; a missing $AMIGA_CC etc.
|
|
||||||
silently produces stale binaries.
|
|
||||||
- After editing any IIgs .s/.asm file, delete the affected example
|
|
||||||
binaries before rebuilding (stale-binary trap).
|
|
||||||
- Hash gates compare against the golden hashes captured at the end of
|
|
||||||
Phase 0 (the harness additions change the old hashes once, then they
|
|
||||||
are frozen).
|
|
||||||
- "iigs-verify" in a gate always names its example and compares
|
|
||||||
before/after checksums: `make iigs-verify VERIFY_EXAMPLE=<name>`
|
|
||||||
(default is draw; that only covers draw-path changes). It reads SHR
|
|
||||||
memory -- there is NO MAME headless screenshot; do not plan for one.
|
|
||||||
- Scott commits; the plan never includes git operations.
|
|
||||||
- Style rules apply to all touched code (camelCase, // comments, braces
|
|
||||||
always, prototypes at top and alphabetized, two blank lines between
|
|
||||||
functions, ASCII only).
|
|
||||||
|
|
||||||
Phase order rationale: fix the clock and build the gates first
|
|
||||||
(otherwise no perf change can be validated and most bug fixes have no
|
|
||||||
covering check), then correctness bugs, then shared infrastructure,
|
|
||||||
then per-subsystem work, and the new platform asm last (highest effort
|
|
||||||
and risk, benefits from everything before it being measurable).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 0 -- Fix the clock and build the verification harness
|
|
||||||
|
|
||||||
The audit's headline: every small-op UBER number carries ~600-800
|
|
||||||
cycles of per-iteration harness tax on IIgs (GetTick toolbox chain).
|
|
||||||
And the plan review's headline: most fixes below are invisible to the
|
|
||||||
current gates (UBER only hashes on-surface compiled-path ops; Amiga/ST
|
|
||||||
have no screen-content check; several ops are not exercised at all).
|
|
||||||
Phase 0 fixes both.
|
|
||||||
|
|
||||||
1. **uber.c batch polling.** In runForFrames, run the op in batches
|
|
||||||
between jlFrameCount() polls. Batch adaptively: batch=1 for ops
|
|
||||||
slower than ~1 frame (present, surfaceClear, fillRect-full would
|
|
||||||
otherwise overrun the window by up to a full batch), 8-16 for fast
|
|
||||||
ops (pick from the first iteration's duration). Fix the false
|
|
||||||
"~10-30 cyc" comment. Ops are fixed-coordinate/idempotent (review
|
|
||||||
verified), so batching does not change what an iteration means.
|
|
||||||
2. **#15 + #71** -- collapse the IIgs jlFrameCount forwarding: declare
|
|
||||||
iigsGetTickWord in the IIgs section of port.h and
|
|
||||||
`#define jlpFrameCount() iigsGetTickWord()`; delete the hal.c
|
|
||||||
wrapper AND its stale 15-line $C019/gFrameCount comment (#71 -- the
|
|
||||||
comment sits on the function being deleted, so it lands here, not in
|
|
||||||
Phase 8); put a one-line GetTick pointer next to the new macro.
|
|
||||||
Remove the now-stale JL_HAS_FRAME_COUNT entry at platform.h:108.
|
|
||||||
CAUTION (verifier): do NOT do the same for jlpFrameHz -- gFrameHz is
|
|
||||||
hal.c-private state; leave it a function.
|
|
||||||
3. **uber.c non-timed correctness section.** New hashed/logged checks
|
|
||||||
that run after the timed ops and before the final hash, added NOW so
|
|
||||||
golden hashes cover every path later phases touch:
|
|
||||||
- edge-case draws: off-surface pixel spam (both signs, both axes),
|
|
||||||
jlDrawRect w=65535 and h=2 cases, jlFillCircle huge-r (r=300 with
|
|
||||||
clipping, r=40000 full-coverage), jlDrawLine far-off-surface
|
|
||||||
endpoints;
|
|
||||||
- clipped-sprite draws: all four edges, a corner, fully off-surface
|
|
||||||
(exercises the interpreted path on every port);
|
|
||||||
- jlTilePasteMono and jlFloodFill ops drawn into the stage (neither
|
|
||||||
is currently in the hashed set -- Phase 7 replaces both paths);
|
|
||||||
- jlDrawText including a string starting out-of-range (Phase 5
|
|
||||||
changes its validation);
|
|
||||||
- jlPaletteSet readback assert via jlPaletteGet: one conforming row,
|
|
||||||
one deliberately non-conforming row, row[0] forced to 0x0000,
|
|
||||||
logged PASS/FAIL (palette is not in the surface hash);
|
|
||||||
- jlRandom golden-sequence check (first N values from a known seed);
|
|
||||||
- allocator torture: sprite create/compile/destroy churn to fragment
|
|
||||||
then compact the arena (#5/#34 coverage), a sprite-bank
|
|
||||||
load/destroy pass (#31 coverage -- allocator mismatch is fatal on
|
|
||||||
IIgs), and a jlShutdown -> jlInit -> destroy-stale-sprite sequence
|
|
||||||
(#34) IF the harness structure allows re-init; otherwise this one
|
|
||||||
becomes a dedicated tiny example.
|
|
||||||
4. **bench-iigs.sh** -- the IIgs capture path, which does not exist
|
|
||||||
today (PERF.md's IIgs column was carried over, not re-run): boot
|
|
||||||
UBER off a writable joey.2mg under headless MAME, cadius-extract
|
|
||||||
joeylog.txt afterward (or extend the verify-iigs.sh Lua to dump the
|
|
||||||
log lines from emulated memory). Without this, no "hashes identical
|
|
||||||
x4" gate in this plan is executable on the reference port.
|
|
||||||
5. **millis host harness** -- audioSfxMix-style host compile of
|
|
||||||
genericPort millis logic (check-blank.sh pattern) driving simulated
|
|
||||||
frame-delta sequences including the 16-bit wrap, for Phase 1 #23.
|
|
||||||
6. **#25 (first half)** -- run the new harness on all four ports;
|
|
||||||
record the new honest baseline table in PERF.md marked
|
|
||||||
post-clock-fix. MANDATORY: tag the SEI-holding rows (present,
|
|
||||||
surfaceClear, fillRect 320x200) as clock-unreliable in the table --
|
|
||||||
the SEI tick-loss artifact survives the batching fix.
|
|
||||||
7. **#73 (landed during Phase 0 out of necessity)** -- DOS jlpFrameCount
|
|
||||||
was an $3DA edge-poll counter that lost every frame whose ~1ms
|
|
||||||
retrace pulse fell between calls; the batched timing loop stalled on
|
|
||||||
it, and it had been inflating the old DOS column 5-40x on slow ops.
|
|
||||||
Now derived from the PIT/uclock millis source (poll-rate-independent);
|
|
||||||
jlpWaitVBL still syncs to real retrace.
|
|
||||||
8. **#76 (landed during Phase 0 out of necessity)** -- IIgs jlpWaitVBL
|
|
||||||
polled $C019's VBL bit, which never asserts under MAME's apple2gs:
|
|
||||||
every VBL-paced program hung forever in emulation, and UBER (the
|
|
||||||
only in-tree caller) could never complete headless -- the real
|
|
||||||
reason PERF.md's IIgs column was "carried over". Now waits on a
|
|
||||||
GetTick change (VBL-interrupt-driven, same boundary semantics).
|
|
||||||
9. **Freeze golden hashes** -- DONE 2026-07-04 for DOS/ST/Amiga in
|
|
||||||
tests/goldens/uber/ (43/43/42 hash lines; the Amiga log lost its
|
|
||||||
final check line to the bench-script timeout -- re-capture with a
|
|
||||||
longer TIMEOUT when convenient). The IIgs golden is PARTIAL (3 op
|
|
||||||
lines, codegen-off): the clang-era IIgs port cannot complete UBER
|
|
||||||
yet -- sprite codegen corrupts memory (#77), the codegen-off run
|
|
||||||
stalls in the jlDrawPixel timing window after 3 ops (uninvestigated
|
|
||||||
-- first Phase 1 stabilization item), and jlAudioInit fails
|
|
||||||
headless. PERF.md documents the 3-port baseline and the IIgs
|
|
||||||
status; the old IIgs column was ORCA-era and is retired.
|
|
||||||
|
|
||||||
Gate: 4-port build; all new correctness checks PASS; goldens recorded.
|
|
||||||
Expect small-op numbers to jump 2-10x against the old table.
|
|
||||||
|
|
||||||
Effort: ~half a day to a day (the harness additions are the bulk).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1 -- Correctness bugs + IIgs stabilization
|
|
||||||
|
|
||||||
All behavior fixes land before perf work so later phases optimize
|
|
||||||
correct code. Every item here has a covering check from Phase 0.
|
|
||||||
|
|
||||||
THE IIGS STABILIZATION CLUSTER COMPLETED 2026-07-04: UBER runs
|
|
||||||
end-to-end (green screen, audio live, all checks PASS, 39/43 hashes
|
|
||||||
matching DOS, honest floor numbers in PERF.md). Fixed along the way:
|
|
||||||
#81 (phantom stage / tiny-heap malloc-returns-1), #82 (RAM-ring
|
|
||||||
logger), #83/#86 (line+fillCircle asm DBR-vs-D scratch stash), #85
|
|
||||||
(framebuffer + bank-0 MM reservations), audio packaging. #77 is
|
|
||||||
root-caused to the toolchain (loader does not MM-reserve segment
|
|
||||||
BSS spans; arena landed at $08:4200 inside the app's own bank) --
|
|
||||||
codegen stays off until LLVM816-ASKS item 5 lands. New/remaining
|
|
||||||
items for the rest of Phase 1: #87 (circle-outline pixel divergence,
|
|
||||||
the last 4 hash mismatches). Original cluster notes follow:
|
|
||||||
(a) #77 sprite-codegen corruption (MAME write-watchpoint hunt);
|
|
||||||
(b) the codegen-off UBER "stall" -- RESOLVED 2026-07-04 in three
|
|
||||||
layers: #80 (log-durability illusion misdirected the hunt), #81
|
|
||||||
(the master bug: 665-byte unprotected heap + malloc-returns-1 ->
|
|
||||||
phantom stage struct; fixed by moving all core structs to
|
|
||||||
jlpBigAlloc), and #82 (per-line floppy log writes stalled the app
|
|
||||||
in SmartPort firmware; fixed with the RAM-ring logger + live Lua
|
|
||||||
drain). UBER now completes end-to-end on IIgs (33 ops + 17 checks,
|
|
||||||
green screen); 20 hashes match DOS. Remaining IIgs defects filed
|
|
||||||
as #83 (diag-line asm pixel divergence) and #84 (interpreted
|
|
||||||
sprite ops ~1000x slow + wrong) -- both queued in this cluster;
|
|
||||||
(c) headless jlAudioInit failure -- FIXED 2026-07-04: the audio code
|
|
||||||
was wired all along; the merlin32-built "ntpplayer" binary was
|
|
||||||
simply never packed onto joey.2mg. iigs-disk now builds and adds
|
|
||||||
it; UBER logs "audioInit OK";
|
|
||||||
(d) #79 flag to the llvm816 session (libc memset/memcpy byte loops).
|
|
||||||
Only after (b) is fixed can the IIgs golden hashes be frozen and the
|
|
||||||
IIgs column return to PERF.md.
|
|
||||||
|
|
||||||
1. **#1 (CRITICAL)** draw.c:549 -- non-IIgs jlDrawPixel: bounds-check
|
|
||||||
x/y (same `(uint16_t)x >= SURFACE_WIDTH` form as the IIgs branch)
|
|
||||||
after the NULL check and return early; then call jlpDrawPixel
|
|
||||||
directly and mark. Deletes the redundant plotPixelNoMark layer on
|
|
||||||
the public path.
|
|
||||||
2. **#11** draw.c:591 -- jlDrawRect general path: compute bottom/right
|
|
||||||
edge coordinates in int32_t; skip an edge whose coordinate is
|
|
||||||
off-surface instead of letting the int16_t cast wrap. ALSO (from the
|
|
||||||
#65 verifier correction, same function): skip the interior-edge
|
|
||||||
fills entirely when h == 2, so jlDrawRect stops passing h == 0 rects
|
|
||||||
into fillRectOnSurface/surfaceMarkDirtyRect -- this restores the
|
|
||||||
"already clipped, positive dimensions" contract that Phase 2 #65
|
|
||||||
relies on before it drops the guard.
|
|
||||||
3. **#72** draw.c:417 -- jlDrawLine H/V fast paths: clip x0 (resp. y0)
|
|
||||||
into range BEFORE anchoring the clamped span (`if (x0 < 0) { span +=
|
|
||||||
x0; x0 = 0; }` after the 32-bit span, return if span <= 0, then clamp
|
|
||||||
to the surface size). Today a line from far off-surface through the
|
|
||||||
whole visible row draws nothing. Found during Phase 0 harness work;
|
|
||||||
covered by the "edge-lines" uber check (its hash changes with this
|
|
||||||
fix).
|
|
||||||
4. **#12** draw.c:656 -- jlFillCircle/jlDrawCircle huge-r handling:
|
|
||||||
32-bit full-coverage test (farthest corner dist^2 <= r^2 -> full
|
|
||||||
surface fill / early out), clamp the span loop to intersecting rows,
|
|
||||||
uint16_t loop variable. Verifier notes: seed yy = y0*y0 when jumping
|
|
||||||
into the row range (the incremental chain breaks on a jump); the
|
|
||||||
initial x walk-down from r stays O(r) once per call -- document it.
|
|
||||||
5. **#23** genericPort.c:40 -- millisElapsed: accumulate milliseconds
|
|
||||||
incrementally from uint16_t frame deltas (per verifier: do NOT keep
|
|
||||||
a total frame count and multiply -- that re-overflows at ~20h; carry
|
|
||||||
the fractional remainder, e.g. delta*20 at 50Hz, delta*50/3 with
|
|
||||||
mod-3 carry at 60Hz). Update the statics with explicit
|
|
||||||
load/add/store-style C (no `++`/`+=` on globals -- ORCA-C inc-abs
|
|
||||||
DBR trap). Document the poll-at-least-once-per-65535-frames bound
|
|
||||||
(~18 min @60Hz, ~22 min @50Hz -- NOT hours). Delete the dangling
|
|
||||||
iigs/hal.c:382-386 comment. Verify against the Phase 0 host harness.
|
|
||||||
6. **#8** codegenArena.c:236 -- IIgs: clamp totalBytes to 64KB with a
|
|
||||||
specific coreSetError string on failure; fix adventure.c to set
|
|
||||||
codegenBytes per-platform (#ifdef: small on chunky ports, 256KB only
|
|
||||||
where a flat heap exists). Document the cap at jlConfigT.codegenBytes.
|
|
||||||
7. **#46** sprite.c:346 -- gate the IIgs/DOS compiled-draw fast path on
|
|
||||||
`routineOffsets[shift][SPRITE_OP_DRAW] != SPRITE_NOT_COMPILED` (read
|
|
||||||
once via the existing byte-pointer idiom; reuse for the call).
|
|
||||||
8. **#34** codegenArena.c:294 -- shutdown/re-init dangling slots.
|
|
||||||
PREFERRED (the audit's own fix): a small sprite registry in sprite.c
|
|
||||||
-- spriteSystemShutdown, called from jlShutdown BEFORE
|
|
||||||
codegenArenaShutdown, walks live sprites and NULLs sp->slot. Zero
|
|
||||||
hot-path cost and it fixes BOTH arms: the destroy-after-reinit heap
|
|
||||||
corruption AND the draw-after-reinit stale-offset execution (an
|
|
||||||
epoch checked only at free time would miss the second arm).
|
|
||||||
Alternatives if the registry is unwanted: arena epoch stamped into
|
|
||||||
jlSpriteT and checked at every dispatch gate (hot-path cost), or a
|
|
||||||
documented "jlShutdown invalidates all sprites" contract (doc-only).
|
|
||||||
9. **#5** spriteCompile.c:202 -- on codegenArenaAlloc failure, run
|
|
||||||
codegenArenaCompact() and retry once; coreSetError on final failure
|
|
||||||
so prewarm failures are diagnosable.
|
|
||||||
10. **#31** assetLoad.c:167 -- move sprite allocations to
|
|
||||||
jlpBigAlloc/jlpBigFree UNIFORMLY. Alloc sites: cel/tileData buffers
|
|
||||||
(assetLoad.c:167, sprite.c jlSpriteCreateFromSurface buf) AND the
|
|
||||||
jlSpriteT header mallocs (sprite.c:215, sprite.c:306,
|
|
||||||
assetLoad.c:175 -- verifier requires these switch in the same
|
|
||||||
change). Free sites -- ALL FOUR for tileData plus the headers:
|
|
||||||
jlSpriteDestroy (sprite.c:325 tileData, sprite.c free(sp)), the
|
|
||||||
jlSpriteCreateFromSurface error path (sprite.c:308), and both
|
|
||||||
assetLoad error paths (assetLoad.c:172, assetLoad.c:177). A single
|
|
||||||
missed free() of a jlpBigAlloc pointer corrupts the IIgs heap
|
|
||||||
(BIG_ALLOC_HDR offset), and three of these sites are failure-path
|
|
||||||
only -- exercised by the Phase 0 torture check.
|
|
||||||
11. **#58** atarist/hal.c:1410 -- DELETE the dead dst/pd NULL checks
|
|
||||||
(callers validate; the finding's category is
|
|
||||||
unnecessary-conditional, not misordered-check), and apply the same
|
|
||||||
cleanup to the identical pattern in jlpTileSnap (deref at 1454,
|
|
||||||
dead checks at 1458/1462) per the verifier correction.
|
|
||||||
12. **#74** -- root-cause the DOS jlSpriteCompile failure (pre-existing;
|
|
||||||
UBER logs it; every DOS sprite bench measured the interpreter).
|
|
||||||
Instrument the sizing/emit/arena-alloc chain under DOSBox; the x86
|
|
||||||
emitter itself emits real code for shifts 0-1. Gates Phase 4's
|
|
||||||
compiled-path verification on DOS.
|
|
||||||
13. **#75** -- resolve the jlTilePasteMono three-way pixel divergence
|
|
||||||
(chunky generic vs Amiga vs ST overrides, caught by the Phase 0
|
|
||||||
tilePasteMono hash). Decide the contract truth (tile.h: non-zero
|
|
||||||
mono nibble -> fgColor), fix the divergent implementations, and
|
|
||||||
re-golden that hash line on the fixed ports. Blocks Phase 7 #3.
|
|
||||||
14. **#77** -- root-cause the IIgs sprite-codegen memory corruption
|
|
||||||
(MAME write-watchpoint on the stage struct / gStage static during
|
|
||||||
jlSpriteCompile + first compiled draw). Until fixed, IIgs builds
|
|
||||||
run JOEY_IIGS_SPRITE_CODEGEN=0 and the IIgs baseline measures the
|
|
||||||
interpreter. HIGHEST PRIORITY of the phase with #74: both reference
|
|
||||||
platforms' compiled sprite paths are currently dead.
|
|
||||||
15. **#78** -- fix ST jlFillCircle and jlTileFill pixel divergence vs
|
|
||||||
the chunky reference (DOS==Amiga agree); re-golden ST hash lines.
|
|
||||||
16. **#79** -- FLAG TO SCOTT (toolchain-owned): llvm-mos runtime
|
|
||||||
memset/memcpy are ~30 cyc/byte byte-loops vs ORCA's ~7 cyc/byte
|
|
||||||
MVN; reference-platform regression ~4x on every memset/memcpy path.
|
|
||||||
Library-side mitigations are #9/#19/#21; the libc fix belongs to
|
|
||||||
the llvm816 session.
|
|
||||||
|
|
||||||
Gate: 4-port builds; Phase 0 correctness checks all PASS (they cover
|
|
||||||
every edge case fixed here); hashes match Phase 0 goldens;
|
|
||||||
`make iigs-verify VERIFY_EXAMPLE=draw` checksum unchanged.
|
|
||||||
|
|
||||||
Effort: ~4-6 hours.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2 -- Shared hot-path infrastructure
|
|
||||||
|
|
||||||
The cross-TU-call cluster. Everything here touches surfaceInternal.h /
|
|
||||||
port.h / surface.c / draw.c and ripples through every port; do it as
|
|
||||||
one unit with a full 4-port verify.
|
|
||||||
|
|
||||||
1. **#10 + #17** -- make gStage a plain extern (`extern jlSurfaceT
|
|
||||||
*gStage;` in surfaceInternal.h, drop static in surface.c; keep
|
|
||||||
jlStageGet() as the public accessor). Replace jlStageGet() with a
|
|
||||||
direct `== gStage` compare in: port.h IIgs macros (jlpFillRect,
|
|
||||||
jlpSurfaceClear, jlpFillCircle), draw.c:533 jlDrawPixel, palette.c,
|
|
||||||
scb.c, surface.c internal uses. Precedent: codegenArenaBase did
|
|
||||||
exactly this.
|
|
||||||
2. **#20 + #65** -- surfaceMarkDirtyRect: move the stage test and the
|
|
||||||
h==1 widen into a macro in surfaceInternal.h (ORCA/C 2.2.1 has no
|
|
||||||
inline -- macro form is a hard constraint; audit macro argument
|
|
||||||
usage for multiple-evaluation before converting call sites).
|
|
||||||
Non-stage surfaces then cost one inline compare; single-row marks
|
|
||||||
widen with no call; multi-row marks keep one call
|
|
||||||
(iigsMarkDirtyRowsInner on IIgs, C loop elsewhere). Drop the w/h
|
|
||||||
guard (#65) ONLY after Phase 1 item 2 restored the contract
|
|
||||||
(jlDrawRect h==2 was the violator); empty ranges then cannot reach
|
|
||||||
the macro.
|
|
||||||
3. **#21** -- stageDirtyClearAll: two memsets (0xFF / 0x00), matching
|
|
||||||
its sibling surfaceMarkDirtyAll.
|
|
||||||
4. **#47** -- dirty-row test unification, TWO-macro form per the
|
|
||||||
verifier correction (NOT a blanket four-site replacement): convert
|
|
||||||
DOS/ST to STAGE_DIRTY_ROW_CLEAN (min > max); keep Amiga's
|
|
||||||
`min != 0xFF` as a second documented macro (STAGE_DIRTY_ROW_TOUCHED
|
|
||||||
-- it is deliberate and cheaper under the clipped-marks invariant);
|
|
||||||
pin the IIgs present asm's compare with a comment naming the
|
|
||||||
sentinel values it assumes.
|
|
||||||
5. **#14 + #24** -- pixel read: expose the chunky nibble read as a
|
|
||||||
macro in surfaceInternal.h using SURFACE_ROW_OFFSET (verifier:
|
|
||||||
macro, not static inline, so floodFillInternal's chunky-fallback
|
|
||||||
reads can use it too). jlSamplePixel uses it inline after its bounds
|
|
||||||
check on chunky builds; jlpGenericSamplePixel body becomes the same
|
|
||||||
macro. (Do NOT expect a sprite-capture delta: the sprite.c:296-297
|
|
||||||
branch only runs on planar ports, which use their own platform
|
|
||||||
jlpSamplePixel -- review corrected the audit here.)
|
|
||||||
6. **#13** -- jlFillRect: add the 16-bit on-surface fast path before
|
|
||||||
the 32-bit clip, in the guarded form (test `w <= SURFACE_WIDTH` and
|
|
||||||
`h <= SURFACE_HEIGHT` FIRST, then `x >= 0 && y >= 0 &&
|
|
||||||
x <= (int16_t)(SURFACE_WIDTH - (int16_t)w)` etc. -- guarding before
|
|
||||||
the subtraction is what makes 16-bit safe against w up to 65535).
|
|
||||||
Verifier: keep the three span emitters calling fillRectOnSurface
|
|
||||||
directly; do NOT reroute them.
|
|
||||||
7. **#16** -- jlPaletteSet: memcpy the row + force row[0] = 0x0000,
|
|
||||||
dropping the undocumented per-entry $0RGB re-mask (pending the
|
|
||||||
decision-table sign-off; if the mask must stay, at least widen the
|
|
||||||
loop counter to uint16_t). Mirrors jlPaletteGet. Covered by the
|
|
||||||
Phase 0 palette readback assert (the surface hash cannot see
|
|
||||||
palette changes).
|
|
||||||
|
|
||||||
Gate: hashes match goldens x4 (dirty-band changes are hash-blind --
|
|
||||||
that is what the next line is for); present-output check: captureDos.sh
|
|
||||||
against a pre-phase golden PNG for DOS, iigs-verify
|
|
||||||
VERIFY_EXAMPLE=draw checksum compare for IIgs, and a documented
|
|
||||||
run-amiga.sh / run-atarist.sh visual pass of the draw example after
|
|
||||||
multiple presents (68k C dirty loops diverge from the IIgs asm; a
|
|
||||||
dirty-band under-mark shows as stale rows only at present time).
|
|
||||||
Capture bench delta against the Phase 0 baseline.
|
|
||||||
|
|
||||||
Effort: ~4-6 hours (mostly the 4-port present verification).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3 -- draw.c micro-optimizations
|
|
||||||
|
|
||||||
1. **#9 + #37 + #36** -- jlDrawLine/jlDrawCircle fallbacks: compute the
|
|
||||||
dirty box from the endpoints (line) / center+radius (circle) before
|
|
||||||
the loop, clamped after, as now; delete the per-pixel and
|
|
||||||
per-iteration box accumulation (the circle's 8 compares per
|
|
||||||
iteration go with it). RISK NOTE: a too-small precomputed box
|
|
||||||
under-marks and is invisible to hashes -- the Phase 2 gate's
|
|
||||||
present-output checks re-run here.
|
|
||||||
2. **#38** -- jlFillCircle C span loop: one bounding-box dirty mark
|
|
||||||
after the loop instead of per-span marks. Over-approximates each
|
|
||||||
row's band to the circle width; pixel output identical; matches
|
|
||||||
what the IIgs asm path already does. (Over-marking is the SAFE
|
|
||||||
direction, unlike item 1.)
|
|
||||||
3. **#35** -- floodFillInternal C fallback: hoist the matchEqual
|
|
||||||
predicate out of the walk loops (two loop variants), use the Phase 2
|
|
||||||
read macro instead of jlpSamplePixel calls where the port is chunky.
|
|
||||||
4. **#61** -- plotPixelNoMark: drop the NULL re-check (all callers
|
|
||||||
validate); KEEP the per-pixel bounds check (load-bearing for
|
|
||||||
clipping).
|
|
||||||
5. **#62** -- define the shared on-surface rect predicate ONCE, here,
|
|
||||||
in its final form (review blocker): the #13-style guarded 16-bit
|
|
||||||
macro -- `w <= SURFACE_WIDTH && h <= SURFACE_HEIGHT` first, then the
|
|
||||||
subtraction compares. That form is correct for BOTH users: jlDrawRect
|
|
||||||
(raw uint16_t w/h up to 65535) and sprite.c (widthPx/heightPx up to
|
|
||||||
2040 = 255 tiles * 8 -- NOT "bounded by 320/200"). jlDrawRect's
|
|
||||||
fast-path test switches to it now; sprite.c merges in Phase 4 as a
|
|
||||||
pure call-site change with NO rewrite of the macro.
|
|
||||||
|
|
||||||
Gate: hashes match goldens x4 (the Phase 0 edge-case checks cover the
|
|
||||||
w=65535 / huge-r paths through the new predicate); present-output
|
|
||||||
checks as in Phase 2; bench diag-line/circle deltas on DOS (the only
|
|
||||||
port that always runs the fallbacks).
|
|
||||||
|
|
||||||
Effort: ~2-3 hours.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 4 -- Sprite pipeline + codegen
|
|
||||||
|
|
||||||
Landing order within the phase (review): land items 2-12 first as one
|
|
||||||
reviewable, hash-gated unit; land item 1 (the risky inner-loop rewrite)
|
|
||||||
LAST as its own change with its own before/after clipped-sprite hash
|
|
||||||
comparison, so a failed rewrite reverts alone. Within items 2-12, do
|
|
||||||
the deletions (#55, #27) BEFORE the shared-derive refactor (#29 + #64)
|
|
||||||
so the refactor never touches code that is about to be deleted.
|
|
||||||
|
|
||||||
1. **#2 + #18 + #45** -- rewrite spriteDrawInterpreted's inner loop
|
|
||||||
(LAND LAST):
|
|
||||||
- hoist row-invariant source math out of the column loop (#45),
|
|
||||||
- carry dst byte pointer + parity incrementally (kill the
|
|
||||||
writeDstNibble call per pixel, drop its redundant & 0x0F masks),
|
|
||||||
- aligned-phase byte loop: src byte == 0 skips two pixels,
|
|
||||||
both-nibbles-opaque stores the whole byte, single-nibble merges
|
|
||||||
stay RMW; chunk per tile column (4 contiguous bytes, then advance
|
|
||||||
srcTileRow by TILE_BYTES -- verifier: the naive *src++ sketch
|
|
||||||
glosses this); keep the per-pixel walker for the misaligned phase.
|
|
||||||
Rollback: revert just the loop; the writeDstNibble path stays in git
|
|
||||||
history.
|
|
||||||
2. **#19** -- jlSpriteSaveUnder/RestoreUnder fallbacks: hoist
|
|
||||||
SURFACE_ROW_OFFSET-based start pointers, walk with += stride adds;
|
|
||||||
kill both per-row multiplies.
|
|
||||||
3. **#44** -- switch isFullyOnSurface's body to the Phase 3 shared
|
|
||||||
predicate (pure call-site merge; the macro is already 16-bit-safe
|
|
||||||
for the 2040-px sprite domain -- do not modify it).
|
|
||||||
4. **#55** -- remove spriteEmit68k.c from the Amiga/ST build lists.
|
|
||||||
DECISION for Scott: delete the file (git history keeps it) or leave
|
|
||||||
it unbuilt.
|
|
||||||
5. **#27** -- Amiga compiled save/restore dispatchers: replace the
|
|
||||||
bodies with ST-style empty linker stubs -- FULL DELETION BREAKS THE
|
|
||||||
LINK (sprite.c calls them unconditionally; verifier correction).
|
|
||||||
Delete the shift-alias loop and the aspirational comments at
|
|
||||||
spriteCompile.c:172-178/241-245. Note for the future: when real
|
|
||||||
Amiga save/restore emitters land, the restore gate must accept
|
|
||||||
planar copyBytes = spriteBytesPerRow+4.
|
|
||||||
6. **#29 + #64** -- one shared macro/helper derives (shift, routeIdx,
|
|
||||||
routeOffset, fnAddr) once; the surviving dispatcher gates and
|
|
||||||
spriteCompiled* callees consume it.
|
|
||||||
7. **#28** -- spriteCompiledDraw (IIgs): byte-pointer idiom for
|
|
||||||
routineOffsets like the rest of the file.
|
|
||||||
8. **#59** -- SPLIT_BACKUP_CACHED: move the split cache per-sprite (the
|
|
||||||
jlSpriteT already caches banks) or delete it; measure both on IIgs.
|
|
||||||
9. **#32** -- jlSpriteBankLoad calls spriteInitFields (then overrides
|
|
||||||
ownsTileData); fix the spriteInternal.h comment.
|
|
||||||
10. **#39** -- DEFAULT_CODEGEN_BYTES per-platform (#ifdef: keep 8KB
|
|
||||||
IIgs, size 68k ports from their emitters' measured bytes/sprite;
|
|
||||||
document at jlConfigT).
|
|
||||||
11. **#56** -- dedupe emitter helpers (c2pColumn/putWord into a shared
|
|
||||||
m68k header; shiftedByteAt/spriteSourceByte into a shared codegen
|
|
||||||
header).
|
|
||||||
12. **#54 + #63** -- fix the stale spriteEmitIigs.c contract comment
|
|
||||||
and the sprite.c:343 "IIgs never takes this" comment.
|
|
||||||
|
|
||||||
Gate: hashes match goldens x4 -- the Phase 0 clipped-sprite checks are
|
|
||||||
the load-bearing coverage here (UBER's timed sprite ops are compiled
|
|
||||||
and on-surface; they cannot see interpreter regressions).
|
|
||||||
`make iigs-verify VERIFY_EXAMPLE=sprite` checksum before/after;
|
|
||||||
captureDos.sh sprite example compare on DOS. Bench: clipped-sprite ops
|
|
||||||
expect 2-3x; compiled ops modest gains from dispatch dedup.
|
|
||||||
|
|
||||||
Effort: ~1-2 days.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 5 -- Tile path + generic fallbacks
|
|
||||||
|
|
||||||
1. **#22** -- jlDrawText: validate bx/by once at entry (verifier: the
|
|
||||||
wrap logic does not sanitize the FIRST position -- reject
|
|
||||||
out-of-range starts), validate srcBx/srcBy from asciiMap per glyph,
|
|
||||||
call jlpTileCopyMasked directly, accumulate one union dirty rect for
|
|
||||||
the string. Covered by the Phase 0 drawText hash check (uber.c has
|
|
||||||
no other drawText coverage).
|
|
||||||
2. **#48** -- tile.c wrappers: shifts (<< 3) for pixel coords, or fold
|
|
||||||
coord derivation into the dirty-mark call site.
|
|
||||||
3. **#51** -- jlpGenericTileCopyMasked: whole-byte store when both
|
|
||||||
nibbles are opaque; keep the merge for mixed bytes. Bit-identical.
|
|
||||||
4. **#50 + #70** -- shared 4-byte row-copy macro used by TileCopy,
|
|
||||||
TilePaste, TileSnap; jlpGenericTileFill uses GENERIC_TILE_ROW0.
|
|
||||||
5. **#69** -- drop the duplicate fg/bg masks (mask once in the public
|
|
||||||
wrapper, document the precondition in the generic).
|
|
||||||
6. **#49** -- jlpGenericFillRect: hoist pxStart/pxEnd/midBytes/edge
|
|
||||||
nibble work out of the row loop; walk the row pointer with
|
|
||||||
+= stride.
|
|
||||||
7. **#30** -- assetLoad constants: use TILE_BYTES from joey/tile.h;
|
|
||||||
derive ASSET_MAX_TILES_W/H from SURFACE_WIDTH/HEIGHT and
|
|
||||||
TILE_PIXELS_PER_SIDE.
|
|
||||||
8. **#66, #67, #68, #57, #60** -- delete SURFACE_PIXELS_PER_WORD;
|
|
||||||
200 -> SURFACE_HEIGHT in the LUT decl; fix tile.c stale comment; fix
|
|
||||||
surface.h hash doc; fix or implement the assetLoad segment comment
|
|
||||||
(DECISION: add the segment directive it promises, or correct the
|
|
||||||
comment -- check IIgs root-segment pressure first).
|
|
||||||
|
|
||||||
Gate: hashes match goldens x4 (#51 especially must be bit-identical;
|
|
||||||
the drawText check covers #22); DOS bench deltas on tile ops (DOS runs
|
|
||||||
the generics); hello example on DOS + IIgs for text rendering.
|
|
||||||
|
|
||||||
Effort: ~3-4 hours.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 6 -- SFX mixer (DOS/ST builds only)
|
|
||||||
|
|
||||||
Verification FIRST (review: no named gate could actually verify this
|
|
||||||
phase): audioSfxMix.c is portable C -- build a host harness on the
|
|
||||||
check-blank.sh pattern that compiles the OLD and NEW mixer with host
|
|
||||||
cc, mixes a fixed seed sample set (fixed-buffer and streaming slots,
|
|
||||||
rate conversion up and down), and diffs output buffers. This proves #7
|
|
||||||
and #33 bit-exact and bounds #6 to 1 LSB concretely before any
|
|
||||||
emulator run.
|
|
||||||
|
|
||||||
1. **#6** -- interp(): the verifier-validated int16 form (explicit
|
|
||||||
int16_t locals so gcc emits one muls.w):
|
|
||||||
`int16_t d = (int16_t)(s1 - s0);`
|
|
||||||
`int16_t fH = (int16_t)(uint16_t)(frac >> 1);`
|
|
||||||
`return (int)s0 + (int)(((int32_t)d * fH) >> 15);`
|
|
||||||
Output may differ by 1 LSB of 8-bit audio (inaudible) -- accepted
|
|
||||||
deviation, document it.
|
|
||||||
2. **#7** -- posFp: uint32_t index + uint16_t frac with carry-compare;
|
|
||||||
split stepFp once per slot; update audioSfxSlotArm/ArmStream/
|
|
||||||
streamRefill. Bit-identical.
|
|
||||||
3. **#33** -- cache slot fields in locals across the inner loop
|
|
||||||
(volatile dst stores force re-reads today), re-reading only after
|
|
||||||
streamRefill; AND split the fixed-buffer vs streaming cases into two
|
|
||||||
inner loops so the loop-invariant isStream branch runs once per slot
|
|
||||||
(both halves are the audited fix; the plan review caught the split
|
|
||||||
being dropped).
|
|
||||||
|
|
||||||
Gate: host-harness diff (above); DOS + ST builds; audio example smoke
|
|
||||||
on DOSBox/Hatari; absence of the ST ~83ms refill warning under load as
|
|
||||||
a secondary signal.
|
|
||||||
|
|
||||||
Effort: ~3-4 hours including the harness.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 7 -- Platform override work (biggest wins, biggest effort)
|
|
||||||
|
|
||||||
Each item is independently landable. Golden hashes for jlTilePasteMono
|
|
||||||
and jlFloodFill were captured in Phase 0 against the generic paths --
|
|
||||||
the new implementations must reproduce them exactly.
|
|
||||||
|
|
||||||
1. **#3** -- IIgs jlpTilePasteMono: new iigsTilePasteMonoInner in
|
|
||||||
joeyDraw.s -- 16-entry nibble-pair LUT expands each mono byte
|
|
||||||
directly into surface rows, no intermediate jlTileT, no second paste
|
|
||||||
pass. Add JL_HAS_TILE_PASTE_MONO to platform.h IIgs block + port.h
|
|
||||||
macro. ORCA-M rules: explicit LONGA/LONGI after every REP/SEP, same
|
|
||||||
load segment as the C callers (DRAWPRIMS), delete stale example
|
|
||||||
binaries before rebuild. Gate: uber tilePasteMono hash identical to
|
|
||||||
the Phase 0 generic-path golden on IIgs; `make iigs-verify
|
|
||||||
VERIFY_EXAMPLE=spacetaxi` before/after (title screen is the
|
|
||||||
acceptance test -- logo repaint currently costs ~10 frame budgets
|
|
||||||
per 4-frame period).
|
|
||||||
2. **#4** -- ST flood plane hooks: implement jlpFloodWalkPlanes +
|
|
||||||
jlpFloodScanRowPlanes against the word-interleaved StPlanarT layout,
|
|
||||||
modeled on the Amiga versions (walk 16 px per 4-word group via
|
|
||||||
combined match masks). Pure 68000, no blitter. C first; hand-asm
|
|
||||||
only if the C version misses the target. Update the port.h comment
|
|
||||||
claiming these hooks are Amiga-only. Gate: uber floodFill hash
|
|
||||||
identical to golden on ST; Hatari flood timing (expect ~5-15x).
|
|
||||||
3. **#42** -- DOS: #define the planar no-op hooks to ((void)0) like the
|
|
||||||
IIgs block.
|
|
||||||
4. **#41** -- fillWord derivation: pick ONE owner. Either the asm
|
|
||||||
derives it from the nibble (as the port.h comment claims) or C keeps
|
|
||||||
deriving via a 16-entry uint16_t LUT instead of the * 0x1111
|
|
||||||
multiply helper. Update the comment to match reality.
|
|
||||||
5. **#53** -- iigsDrawPixelInner: remove the dead PHB/PLB framing and
|
|
||||||
fold the row offset via `adc gRowOffsetLut,x` to eliminate the
|
|
||||||
dpxlTmp round-trip. Two verifier-identified hazards (the plan review
|
|
||||||
killed the earlier AUXWRITE caveat -- it does not apply here, and
|
|
||||||
dpxlTmp is DBR-absolute, not long-absolute):
|
|
||||||
(a) deleting phb renumbers EVERY stack-relative arg offset
|
|
||||||
(8/10/12,s -> 7/9/11,s) -- miss one and the routine reads the wrong
|
|
||||||
arg while still "working"; update the comment block at
|
|
||||||
joeyDraw.s:1402-1405;
|
|
||||||
(b) do NOT relocate the scratch D-relative -- D-relative stores land
|
|
||||||
at the bank-0 alias, not the data-bank .bss.
|
|
||||||
Gate: iigs-verify VERIFY_EXAMPLE=draw checksum + before/after bench
|
|
||||||
on the fixed clock (hottest inner op).
|
|
||||||
6. **#52** -- iigsBlitStageToShr dead upload paths, verifier keep-list
|
|
||||||
(the review caught two traps here): the entry becomes zero-arg --
|
|
||||||
only ONE far pointer + flags word is currently pushed, not two;
|
|
||||||
delete the marshalling at joeyDraw.s:2765-2772 and both flag-gated
|
|
||||||
MVN blocks (2787-2859); KEEP the caller-DBR stash at 2780-2784 (the
|
|
||||||
narrow-row MVN path reloads $E1:9DFE after every MVN -- deleting the
|
|
||||||
stash corrupts presents); delete the orphaned bsFrame BSS
|
|
||||||
(4417-4420); update hal.c:98/298 and the stale jlpInit comment at
|
|
||||||
hal.c:270-273. Gate: iigs-verify against an example that produces
|
|
||||||
PARTIAL-ROW dirty bands (not just the default draw demo), checksum
|
|
||||||
before/after.
|
|
||||||
7. **#26** -- platform.h decorative IIgs flags: make the registry
|
|
||||||
authoritative AND loud (review: plain #ifdef-consume recreates the
|
|
||||||
silent-fallback rot this finding describes): keep the flags, and
|
|
||||||
pair the port.h IIgs macro block with
|
|
||||||
`#if !defined(JL_HAS_<OP>) #error` checks so a missing/removed flag
|
|
||||||
is a compile failure, not a silent generic fallback.
|
|
||||||
|
|
||||||
Gate (phase-wide): full 4-port UBER re-run against goldens;
|
|
||||||
per-item gates above.
|
|
||||||
|
|
||||||
Effort: ~2-4 days (dominated by the two new implementations).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 8 -- API-surface decisions + final re-baseline
|
|
||||||
|
|
||||||
1. **#40** -- input predicates: DECISION for Scott. Option A: macro
|
|
||||||
fast paths in joey/input.h reading the extern state arrays (keeps
|
|
||||||
functions for ABI; exposes internals in a public header). Option B:
|
|
||||||
accept the call cost (the honest post-Phase-0 number is far better
|
|
||||||
than the old 3382 anyway). Recommendation: B unless a real game
|
|
||||||
shows input in its frame profile.
|
|
||||||
2. **#43** -- jlRandom 16-bit-half rewrite, IIGS-ONLY behind a platform
|
|
||||||
#if (verifier: the decomposed form is a wash-to-slight-LOSS on
|
|
||||||
68000 -- keep the current 32-bit C for the GCC ports). Output is
|
|
||||||
bit-identical either way; the Phase 0 golden-sequence check covers
|
|
||||||
it on all ports.
|
|
||||||
3. **#25 (final)** -- regenerate PERF.md completely: new IIgs floor,
|
|
||||||
new ratios, document BOTH artifacts (SEI clock corruption + the
|
|
||||||
now-fixed per-iteration poll tax) and the honest full-screen
|
|
||||||
ceiling; drop the stale-baseline caveats.
|
|
||||||
|
|
||||||
Gate: full 4-port bench + hash run; PERF.md tells the truth.
|
|
||||||
|
|
||||||
Effort: ~1-2 hours plus bench runs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Decisions Scott must make (collected)
|
|
||||||
|
|
||||||
| # | Decision | Recommendation |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| #16 | Drop the undocumented $0RGB re-mask in jlPaletteSet (memcpy + force color 0)? Slight behavior change for non-conforming input. | Yes -- contract already requires $0RGB |
|
|
||||||
| #22 | jlDrawText: out-of-range initial bx/by becomes an explicit reject instead of silently absorbed per-glyph. | Reject at entry |
|
|
||||||
| #8 | Arena >64KB on IIgs: clamp silently or fail with error? | Clamp + coreSetError note |
|
|
||||||
| #34 | Sprite registry in sprite.c vs arena epoch vs doc-only contract? | Registry (fixes both corruption arms, zero hot-path cost) |
|
|
||||||
| #55 | spriteEmit68k.c: delete or keep unbuilt? | Delete (git history keeps it) |
|
|
||||||
| #38 | Accept dirty-band over-approximation for fillCircle fallback spans? | Yes -- matches IIgs asm path |
|
|
||||||
| #6 | Accept 1-LSB audio deviation for the muls.w interp rewrite? | Yes -- inaudible |
|
|
||||||
| #40 | Input predicate macros in public header vs keep functions? | Keep functions (B) -- DECIDED 2026-07-05, documented at the predicate block in src/core/input.c |
|
|
||||||
| #60 | assetLoad IIgs segment: add the promised directive or fix the comment? | Check root-segment pressure, then decide |
|
|
||||||
|
|
||||||
## What this plan deliberately does NOT do
|
|
||||||
|
|
||||||
- No runtime dispatch, no C++/newer-C features, no ST blitter, no
|
|
||||||
wholesale planar-architecture changes (per the audit's ground rules
|
|
||||||
and the standing 68k-asm-rewrite plan -- deep C micro-opts to the
|
|
||||||
Amiga/ST HALs beyond the listed findings are out of scope because the
|
|
||||||
hand-asm rewrite will replace them).
|
|
||||||
- No toolchain changes (llvm-mos/ORCA are owned elsewhere; anything
|
|
||||||
needing compiler work gets flagged to Scott instead).
|
|
||||||
- Refuted findings (PERF-AUDIT.md bottom) are not actioned.
|
|
||||||
|
|
||||||
## Progress tracking
|
|
||||||
|
|
||||||
Mark each finding number done here as it lands:
|
|
||||||
|
|
||||||
- Phase 0: [x] uber-batch [x] #15 [x] #71 [x] uber-checks [x] bench-iigs.sh [x] millis-harness [x] #73 [x] #76 [x] #25a (3-port; IIgs blocked on stabilization) [x] goldens (DOS/ST/Amiga; IIgs partial)
|
|
||||||
- Phase 1 COMPLETE 2026-07-05 (all four ports 43/43 bit-identical hashes): [x] #1 [x] #11 (+h==2) [x] #72 [x] #12 [x] #74 [x] #75 [x] #77 (root-caused; fix=toolchain ask 5; codegen off) [x] #78 [x] #79-flag [x] #81 [x] #82 [x] #83 [x] #85 [x] #86 [x] #87 [x] #23 [x] #8 [x] #46 [x] #34 [x] #5 [x] #58 [x] #31 -- new: #88 filed (jlTileT snap/mono semantics wart, Phase 5) [ ] #23 [ ] #8 [ ] #46 [ ] #34 [ ] #5 [ ] #31 [ ] #58
|
|
||||||
- Phase 2 COMPLETE 2026-07-05 (bit-neutral, 43/43 held on all four ports; DOS paletteSet +92%, drawPixel +37%, samplePixel +21%; IIgs paletteSet +8%, samplePixel +6%): [x] #10 [x] #17 [x] #20 [x] #65 [x] #21 [x] #47 [x] #14 [x] #24 [x] #13 [x] #16 (final form = unrolled word copy, NOT memcpy -- the clang IIgs libc memcpy byte-loop regressed it; finding #79 caveat)
|
|
||||||
- Phase 3 COMPLETE 2026-07-05 (bit-neutral, 43/43 x4; IIgs drawCircle r=80 -> 70 ops/s, fillCircle r=40 -> 42): [x] #9 [x] #37 [x] #36 [x] #38 [x] #35 [x] #61 [x] #62 -- new: #89 filed (diag fallback int16 dx/dy latent, opportunistic)
|
|
||||||
- Phase 4 COMPLETE 2026-07-05 (43/43 x4; IIgs interpreted spriteDraw 18->48 ops/s (2.7x), save +18%, restore +15%; review harness: 726,915 differential cases, 0 mismatches): all items incl. #19 + #29-runtime stragglers; #55 file deleted; #59 cache deleted; sprite.h docs modernized
|
|
||||||
- Phase 5 COMPLETE 2026-07-05 (bit-neutral 43/43 x4; DOS tileSnap +42%, tileCopyMasked +29%, tilePaste +18%, fillRect +8-9%): all items; #22 landed as entry-sanitation preserving historical wrap semantics (strict reject would have broken the offgrid golden); jlDrawText needed a noinline union helper for the w65816 reg allocator
|
|
||||||
- Phase 6 COMPLETE 2026-07-05 (host harness 12/12: #7+#33 bit-identical in exact mode pre-#6; #6 maxAbsDelta=1 on the 3 odd-step scenarios, within the 1-LSB bound; baseline recaptured post-#6; DOS+ST builds clean; ST asm shows 1 muls.w per split loop, 0 __mulsi3): [x] #6 [x] #7 [x] #33 -- emulator audio smoke (DOSBox/Hatari) not yet run
|
|
||||||
- Phase 7 COMPLETE 2026-07-05: [x] #3 [x] #4 [x] #42 [x] #41 [x] #53 [x] #52 [x] #26 -- #3/#4 landed
|
|
||||||
+ hash-identical x4 (tilePasteMono=B0829554, floodFill=780DC48B). Perf captured
|
|
||||||
2026-07-05 via isolated UBER timed ops (same-CPU override-vs-generic before/after,
|
|
||||||
built by toggling the port.h #3 macro / platform.h #4 flags): #3 IIgs tilePasteMono
|
|
||||||
asm 1083 vs generic 323 ops/sec = 3.35x (timed hash 8A39C22B identical both paths --
|
|
||||||
asm output bit-exact); #4 ST flood plane-hooks 13.5 vs generic 75 frames/flood = 5.6x
|
|
||||||
(within the 5-15x target). Measured via the UBER micro-benchmark rather than the
|
|
||||||
plan's spacetaxi/flood-scenario gates -- more isolated, and correctness already
|
|
||||||
proven by the x4 golden match.
|
|
||||||
+ Golden coverage extended 2026-07-05 (re-analysis follow-up): the original
|
|
||||||
tilePasteMono/floodFill goldens left two paths hash-blind -- the #3 asm combos
|
|
||||||
idx 0 (bg:bg) and idx 3 (fg:fg) were never selected by the alternating 0x0F/0xF0
|
|
||||||
mono tile, and only jlFloodFill (matchEqual) exercised the #4 ST plane hooks, never
|
|
||||||
the bounded stop-mask + group-skip branch. Added checkTileMonoFloodExtra() in
|
|
||||||
uber.c (runs LAST so no earlier cumulative hash shifts): tilePasteMonoAll pastes a
|
|
||||||
0x00/0xFF/0x0F/0xF0 tile (all four combos) and floodFillBounded fills an enclosed
|
|
||||||
color-3 box across four 16px groups. Re-ran all four ports: both new checks
|
|
||||||
hash-identical x4 (tilePasteMonoAll=30C3A2C9, floodFillBounded=EF6B6F51); goldens
|
|
||||||
updated to 45/45. The two previously by-inspection-only paths are now empirically
|
|
||||||
bit-exact vs the DOS generic reference. NOTE: Amiga bench needed TIMEOUT=600 (the
|
|
||||||
last-running checks overran the default 150s, same truncation as the Phase 0 note).
|
|
||||||
+ #41 verified COMPLETE 2026-07-05: found already landed (opportunistically,
|
|
||||||
during the earlier tileFill/fillCircle/fillRect asm work) -- iigsTileFillInner,
|
|
||||||
iigsFillCircleInner, and iigsFillRectInner all take the raw nibble and derive
|
|
||||||
the fill word/bytes in asm (joeyDraw.s:420-430 etc.); the port.h macros pass
|
|
||||||
(c & 0x0F); no C-side * 0x1111 survives anywhere (grep-verified, comments
|
|
||||||
only); the port.h:100-105/136-138 comments match reality. surfaceClear's
|
|
||||||
fillWord stays C-derived via (d | d<<8) -- shifts/OR, no multiply helper, so
|
|
||||||
ownership is consistent per-op and documented at each site. No code change;
|
|
||||||
checkbox was stale.
|
|
||||||
+ #52 verified COMPLETE 2026-07-05: also found already landed (the clang-era
|
|
||||||
joeyDraw.s rewrite folded it in) -- iigsBlitStageToShr is zero-arg (hal.c:106
|
|
||||||
extern, hal.c:309 call), the scbPtr/palettePtr/uploadFlags marshalling and both
|
|
||||||
flag-gated MVN blocks are gone, the caller-DBR stash to $E1:9DFE is KEPT per the
|
|
||||||
verifier correction (joeyDraw.s:3214-3218), bsFrame BSS deleted (grep-verified),
|
|
||||||
and the jlpInit/jlpPresent comments in hal.c already tell the truth (jlpPresent
|
|
||||||
even credits #52). Gate evidence: today's IIgs UBER run exercised the narrow-row
|
|
||||||
MVN path (fillRect-32x32 present = 16-word dirty band < the 32-word threshold,
|
|
||||||
~300 iterations) and completed to the green done-screen, which requires the DBR
|
|
||||||
stash restore to work; UBER 45/45 hashes matched.
|
|
||||||
+ #53 verified COMPLETE 2026-07-05: same story -- iigsDrawPixelInner has no
|
|
||||||
PHB/PLB (frame = php+phd = +3; args correctly renumbered to x@7,s y@9,s
|
|
||||||
nib@11,s with the ABI comment updated, joeyDraw.s:1856-1858), the row offset
|
|
||||||
folds directly via `adc gRowOffsetLut,x` (line 1899), dpxlTmp is deleted
|
|
||||||
(grep-verified), and scratch was NOT relocated D-relative per the verifier
|
|
||||||
hazard note. No isolated before/after bench: #53 landed inside the wholesale
|
|
||||||
clang-era rewrite (no "before" tree), and the verifier-predicted ~1% delta is
|
|
||||||
below MAME run-to-run noise. Correctness gates: UBER jlDrawPixel + edge-pixels
|
|
||||||
hashes bit-exact vs DOS x4 (45/45), and `make iigs-verify VERIFY_EXAMPLE=draw`
|
|
||||||
PASS -- checksum=08483E, distinctNibbles=15 (RECORDED AS THE BASELINE; no prior
|
|
||||||
checksum was ever written down -- future draw-path gates compare against this).
|
|
||||||
RESIDUAL flagged, out of #53 scope: iigsDrawLineInner / iigsDrawCircleInner /
|
|
||||||
the tile inners still carry phb in their prologues -- the audit verifier
|
|
||||||
explicitly deferred that same-pattern audit to the IIGS_PERF_PLAN Phase 4
|
|
||||||
line/circle work; their arg-offset comments all assume the +4 frame, so any
|
|
||||||
future phb deletion must renumber every N,s in each routine.
|
|
||||||
+ #3 SPACETAXI TITLE-SCREEN ACCEPTANCE TEST attempted 2026-07-05 (the plan's
|
|
||||||
original gate, deferred from the Phase 7 landing): BLOCKED ON FINDING #77
|
|
||||||
(toolchain) -- but the attempt built the entire test rig and fixed four real
|
|
||||||
bugs found along the way. Landed: IIgs asset bake (examples/spacetaxi/
|
|
||||||
generated/iigs, .tbk target byte 4), make-iigs-disk.sh DATA-tree packing
|
|
||||||
(CREATEFOLDER + per-file ADDFILE when STAXI is on the disk -- the spacetaxi
|
|
||||||
PLAN.md Phase 6 packager task), STAXI now BUILDS on IIgs (the iigs.mk:74
|
|
||||||
"blocked on 2-byte cross-seg reloc" comment is stale -- the toolchain landed
|
|
||||||
it; STAXI still deliberately omitted from iigs-examples pending the #77 fix),
|
|
||||||
scripts/bench-staxi-title.sh (per-frame SHR logo-cell cadence sampler; the
|
|
||||||
intended 4-frame period vs measured), scripts/diag-staxi-screen.sh (SHR/RAM
|
|
||||||
dump + PC histogram + ring extraction). Bug fixes landed (all 45/45
|
|
||||||
golden-verified x4): (1) IIgs jlLogReset did a floppy-truncate write --
|
|
||||||
first-ever floppy WRITE on the headless path, #82 class; now ring-only, with
|
|
||||||
jlLogFlush switched to "w" (self-truncating full-ring dump, idempotent);
|
|
||||||
(2) jlTileBankLoad read tile banks 32 bytes/fread (1000 freads per font
|
|
||||||
bank!) -- now batched 8KB/fread (ASSET_TILE_READ_BATCH), same truncated-file
|
|
||||||
contract; (3) NEW API jlTileBankLoadToSurface (tile.h) streams a bank
|
|
||||||
straight onto a surface's block grid with a 256-byte static scratch --
|
|
||||||
no caller-side 32KB buffer, which no IIgs example can allocate (heap is
|
|
||||||
665 bytes; a 32KB static blows the bank-0 BSS ceiling); spacetaxi's
|
|
||||||
loadFontSheet now uses it; (4) jlpGenericSurfaceAllocPixels used calloc --
|
|
||||||
on IIgs that's the malloc-returns-1 trap (#81) shredding bank 0 through
|
|
||||||
pointer 1 on the first OFFSCREEN jlSurfaceCreate (stage uses the pinned
|
|
||||||
path, so UBER never hit it); now jlpBigAlloc + memset with matching
|
|
||||||
jlpBigFree in jlpGenericSurfaceFreePixels. THE BLOCKER: with all four
|
|
||||||
fixed, STAXI boots to "title loaded, tilebank=0" but dies layout-dependently
|
|
||||||
when jlpBigAlloc handles land inside the app's own unreserved BSS spans
|
|
||||||
(finding #77 exactly; pixels@$09:0BF4 run survived to title-load, other
|
|
||||||
layouts died in fopen). An app-side BSS-claim mitigation was built and
|
|
||||||
REVERTED: the linker's __bss_seg symbols ARE readable via a DATA32 .long
|
|
||||||
table (C IMM16 refs trip the 2-byte cINTERSEG limit), but a SUCCESSFUL
|
|
||||||
NewHandle claim over the app's own BSS span kills the app instantly
|
|
||||||
(silent return-to-Finder, reproduced twice) -- the reservation must be made
|
|
||||||
loader-side. Full recipe + evidence filed in LLVM816-ASKS.md item 5.
|
|
||||||
Re-run the gate (scripts/bench-staxi-title.sh both port.h-macro arms) once
|
|
||||||
the toolchain lands item 5.
|
|
||||||
- Phase 8 COMPLETE 2026-07-05: [x] #40 [x] #43 [x] #25b -- ALL PHASES DONE.
|
|
||||||
#40 decided per recommendation B (keep functions; documented at the predicate
|
|
||||||
block in src/core/input.c and in the decisions table above). #43 landed as an
|
|
||||||
IIgs-only #if in src/core/random.c: state stored as two uint16_t halves, the
|
|
||||||
13/17/5 xorshift triple decomposed to 16-bit shift identities (hi-before-lo
|
|
||||||
update order per step); bit-identity host-proven (UBER golden sequence 4/4 +
|
|
||||||
50M-state sweep vs the 32-bit reference, scratchpad rngHalvesCheck.c) and
|
|
||||||
emulator-gated (random-golden PASS x4); GCC ports keep the 32-bit form per the
|
|
||||||
verifier correction. Note: UBER has no timed jlRandom row, so the ~2-3x/call
|
|
||||||
win is analytic, not benched. #25b: PERF.md fully regenerated from same-day
|
|
||||||
4-port captures -- fresh table via tools/uber-perf-table, both clock artifacts
|
|
||||||
documented (poll tax fixed / SEI tick loss inherent with the three IIgs rows
|
|
||||||
tagged clock-unreliable against the ~29 ops/sec ceiling), 45/45 x4 correctness
|
|
||||||
statement, per-port sprite-row caveats (DOS compiled, IIgs interpreter pending
|
|
||||||
#77), below-floor punch list, stale stabilization/history sections dropped.
|
|
||||||
Phase-wide gate: full 4-port bench + hash run 2026-07-05, 45/45 goldens on all
|
|
||||||
four ports, random-golden PASS x4.
|
|
||||||
1425
PERF-AUDIT.md
1425
PERF-AUDIT.md
File diff suppressed because it is too large
Load diff
|
|
@ -12,7 +12,7 @@ platforms from a single codebase:
|
||||||
The Apple IIgs defines the capability ceiling. Stronger platforms coast.
|
The Apple IIgs defines the capability ceiling. Stronger platforms coast.
|
||||||
Hot paths are hand-written assembly per port; the public API is C.
|
Hot paths are hand-written assembly per port; the public API is C.
|
||||||
|
|
||||||
See `docs/DESIGN.md` for the full 1.0 design.
|
See `claudeDocs/DESIGN.md` for the full 1.0 design.
|
||||||
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
@ -411,7 +411,7 @@ pops the next typed character (printable ASCII 0x20..0x7E plus
|
||||||
`JL_CHAR_BACKSPACE`/`TAB`/`RETURN`/`ESCAPE`) or -1 when empty, with
|
`JL_CHAR_BACKSPACE`/`TAB`/`RETURN`/`ESCAPE`) or -1 when empty, with
|
||||||
shift, caps lock, and the machine's keyboard layout already applied by
|
shift, caps lock, and the machine's keyboard layout already applied by
|
||||||
the backend -- so punctuation like `.` and `:` arrives correctly on
|
the backend -- so punctuation like `.` and `:` arrives correctly on
|
||||||
every port. Refilled by the same `jlInputPoll`; see `docs/input.md`
|
every port. Refilled by the same `jlInputPoll`; see `claudeDocs/input.md`
|
||||||
for per-port sources and limitations.
|
for per-port sources and limitations.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
|
|
@ -556,7 +556,7 @@ Only the IIgs has multiple built-in ports; elsewhere `MODEM`/`PRINTER` fall
|
||||||
back to the natural port. The `serial` example is an echo terminal and
|
back to the natural port. The `serial` example is an echo terminal and
|
||||||
loopback self-test. `scripts/check-serial.sh` unit-tests the portable gate
|
loopback self-test. `scripts/check-serial.sh` unit-tests the portable gate
|
||||||
layer against a loopback mock with no hardware. Full reference, per-port
|
layer against a loopback mock with no hardware. Full reference, per-port
|
||||||
behavior, and porting notes: [`docs/serial.md`](docs/serial.md).
|
behavior, and porting notes: [`claudeDocs/serial.md`](claudeDocs/serial.md).
|
||||||
|
|
||||||
|
|
||||||
### Files, saves & disk space (`joey/file.h`) -- opt-in
|
### Files, saves & disk space (`joey/file.h`) -- opt-in
|
||||||
|
|
@ -587,7 +587,7 @@ Directory create / delete / free-space use a per-port HAL (POSIX `mkdir` +
|
||||||
`remove` + `_dos_dskfre` on the X68000). `scripts/check-save.sh`
|
`remove` + `_dos_dskfre` on the X68000). `scripts/check-save.sh`
|
||||||
round-trips the core against real files; `make iigs-verify-save` boots the
|
round-trips the core against real files; `make iigs-verify-save` boots the
|
||||||
IIgs save path under MAME. Full reference, per-port behavior, and porting
|
IIgs save path under MAME. Full reference, per-port behavior, and porting
|
||||||
notes: [`docs/save.md`](docs/save.md).
|
notes: [`claudeDocs/save.md`](claudeDocs/save.md).
|
||||||
|
|
||||||
|
|
||||||
### Debug logging (`joey/debug.h`)
|
### Debug logging (`joey/debug.h`)
|
||||||
|
|
|
||||||
9
assets/joeylibLogo.svg
Normal file
9
assets/joeylibLogo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 70 KiB |
BIN
assets/joeylib_logo_-_320x200.png
(Stored with Git LFS)
Normal file
BIN
assets/joeylib_logo_-_320x200.png
(Stored with Git LFS)
Normal file
Binary file not shown.
532
docs/ApiAudio.adoc
Normal file
532
docs/ApiAudio.adoc
Normal file
|
|
@ -0,0 +1,532 @@
|
||||||
|
== Audio
|
||||||
|
|
||||||
|
Declared in `<joey/audio.h>`.
|
||||||
|
|
||||||
|
There are three layers, and on two platforms they share physical hardware:
|
||||||
|
|
||||||
|
* *Module playback* -- `jlAudioPlayMod`, four-channel Protracker `.MOD`.
|
||||||
|
* *Chip tracker* -- `jlMusicPlay`, a portable event stream driving three tone
|
||||||
|
voices plus noise at register-write cost, with no CPU mixing. The recommended
|
||||||
|
music path.
|
||||||
|
* *Voices and effects* -- `jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx` and
|
||||||
|
the streaming forms.
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
On the Atari ST and Amiga the tone voices *are* the hardware the module player
|
||||||
|
drives. While a `.MOD` plays, `jlAudioVoice` writes fight the player's register
|
||||||
|
writes and lose. Choose the module layer or the voice and tracker layer per
|
||||||
|
scene, never both at once.
|
||||||
|
====
|
||||||
|
|
||||||
|
A failed `jlAudioInit` is not fatal: `jlInit` still succeeds and every audio
|
||||||
|
call becomes a silent no-op.
|
||||||
|
|
||||||
|
=== Constants
|
||||||
|
|
||||||
|
[cols="3,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Name |Value |Meaning
|
||||||
|
|
||||||
|
|`JOEY_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4.
|
||||||
|
|`JOEY_AUDIO_VOICES` |3 |PSG-style tone voices, 0..2.
|
||||||
|
|`JL_SFX_RAW_SIGNED8` |0 |Raw stream format: `int8_t` PCM.
|
||||||
|
|`JL_SFX_RAW_UNSIGNED8_NOZERO` |1 |Raw stream format: `uint8_t` biased 0x80,
|
||||||
|
and 0x00 is forbidden (it is the DOC stop byte).
|
||||||
|
|===
|
||||||
|
|
||||||
|
[TIP]
|
||||||
|
====
|
||||||
|
Prefer *slot 4* for game sound effects when music is playing. The IIgs maps
|
||||||
|
tracker tone voices onto slots 0..2 and the noise voice onto slot 3. On the
|
||||||
|
other ports the slots are independent of the voice layer.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlAudioInit
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlAudioInit(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* `true` if the platform has a working audio engine and started it;
|
||||||
|
`false` silently otherwise, with the rest of the API still callable but silent.
|
||||||
|
|
||||||
|
=== jlAudioShutdown
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlAudioShutdown(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Tears the engine down. Safe when audio was never initialised.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlAudioFrameTick
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlAudioFrameTick(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Hooks the engine into your game loop. Most platforms drive their engines from a
|
||||||
|
hardware interrupt and ignore this, but it is required for any port that mixes
|
||||||
|
in user context, it advances tracker playback, and on IIgs and Amiga it is what
|
||||||
|
fires the next chunk of a streaming sound effect.
|
||||||
|
|
||||||
|
Call it once per frame, unconditionally.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlAudioPlayMod
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlAudioPlayMod(const uint8_t *data, uint32_t length, bool loop);
|
||||||
|
----
|
||||||
|
|
||||||
|
Begins module playback. If a module is already playing it is replaced.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`data` |`const uint8_t *` |Platform-native module blob: `.mod` on most ports,
|
||||||
|
`.amod` on Amiga, `.ntp` on IIgs. The asset pipeline produces the right form.
|
||||||
|
|`length` |`uint32_t` |Blob size in bytes.
|
||||||
|
|`loop` |`bool` |`true` plays forever.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
`loop = false` needs help on the Amiga: PTPlayer has no native song-end signal,
|
||||||
|
so the module must contain an `E8FF` marker at song end. The `joeymod` tool's
|
||||||
|
`.amod` output injects it. A plain `.mod` with no marker loops anyway on Amiga.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlAudioStopMod
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlAudioStopMod(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Stops the current module and resets the playhead, so the next `jlAudioPlayMod`
|
||||||
|
starts from the top.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlAudioIsPlayingMod
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlAudioIsPlayingMod(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* `true` while a module is producing output. `false` after a stop,
|
||||||
|
before the first play, and on platforms where audio init failed.
|
||||||
|
|
||||||
|
=== jlAudioPlaySfx
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlAudioPlaySfx(uint8_t slot, const uint8_t *sample,
|
||||||
|
uint32_t length, uint16_t rateHz);
|
||||||
|
----
|
||||||
|
|
||||||
|
Triggers a one-shot digital sound effect. If the slot is already playing, the
|
||||||
|
new sample replaces it.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`slot` |`uint8_t` |Slot 0..`JOEY_AUDIO_SFX_SLOTS`-1. Prefer 4 alongside music.
|
||||||
|
|`sample` |`const uint8_t *` |Raw signed 8-bit PCM.
|
||||||
|
|`length` |`uint32_t` |Sample count in bytes.
|
||||||
|
|`rateHz` |`uint16_t` |The rate the sample was recorded at; the engine
|
||||||
|
re-pitches to its own output rate.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
The full -128..127 range is accepted everywhere, but the IIgs plays -128 as
|
||||||
|
-127: its DOC treats the corresponding byte as a wave-end marker and would halt
|
||||||
|
the voice. One level in 256, inaudible in practice.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlAudioStreamFillT
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
typedef uint32_t (*jlAudioStreamFillT)(void *ctx, int8_t *dst, uint32_t count);
|
||||||
|
----
|
||||||
|
|
||||||
|
Stream refill callback. Write up to `count` signed 8-bit samples to `dst` and
|
||||||
|
return how many you wrote. Returning 0 signals end of stream and the slot
|
||||||
|
deactivates.
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
This is called from the audio engine's refill context -- the main thread on DOS
|
||||||
|
and ST, but the *audio interrupt* on Amiga and IIgs. It must not block and must
|
||||||
|
not allocate.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlAudioPlaySfxStream
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill,
|
||||||
|
void *ctx, uint16_t rateHz);
|
||||||
|
----
|
||||||
|
|
||||||
|
Plays a streaming sound effect, pulling samples from your callback as the
|
||||||
|
slot's prefetch buffer drains. Sound effects can therefore be arbitrarily long
|
||||||
|
with no caller-side allocation.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`slot` |`uint8_t` |Slot 0..4. Replaces whatever is playing there.
|
||||||
|
|`fill` |`jlAudioStreamFillT` |Refill callback.
|
||||||
|
|`ctx` |`void *` |Opaque pointer handed back to the callback.
|
||||||
|
|`rateHz` |`uint16_t` |Rate your callback produces; the engine resamples with
|
||||||
|
linear interpolation.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
[IMPORTANT]
|
||||||
|
====
|
||||||
|
On IIgs and Amiga the engine plays one chunk at a time and fires the next from
|
||||||
|
`jlAudioFrameTick`, so *you must be calling `jlAudioFrameTick` every frame* or
|
||||||
|
the stream stops after its first chunk. A chunk seam on those ports can carry
|
||||||
|
up to one frame of jitter.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlAudioSfxRawFormat / jlAudioPlaySfxStreamRaw
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint8_t jlAudioSfxRawFormat(void);
|
||||||
|
void jlAudioPlaySfxStreamRaw(uint8_t slot, jlAudioStreamFillT fill,
|
||||||
|
void *ctx, uint16_t rateHz);
|
||||||
|
----
|
||||||
|
|
||||||
|
`jlAudioSfxRawFormat` returns this port's raw stream format, either
|
||||||
|
`JL_SFX_RAW_SIGNED8` or `JL_SFX_RAW_UNSIGNED8_NOZERO`.
|
||||||
|
|
||||||
|
`jlAudioPlaySfxStreamRaw` is `jlAudioPlaySfxStream` except your callback
|
||||||
|
delivers bytes *already in that format*, so the port skips its per-byte
|
||||||
|
conversion of every chunk. On the IIgs that conversion is a 65816 byte loop
|
||||||
|
over each 3.5 KB chunk, and it is enough to stall a game for a third of a
|
||||||
|
second per chunk of speech. Every other port streams signed PCM natively, so
|
||||||
|
there this is simply the plain call.
|
||||||
|
|
||||||
|
If your producer cannot make the port's format, use `jlAudioPlaySfxStream`.
|
||||||
|
|
||||||
|
*Parameters:* as `jlAudioPlaySfxStream`. *Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlAudioSetPan
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
typedef enum {
|
||||||
|
AUDIO_PAN_LEFT = 0, AUDIO_PAN_CENTER, AUDIO_PAN_RIGHT, AUDIO_PAN_COUNT
|
||||||
|
} jlAudioPanE;
|
||||||
|
|
||||||
|
void jlAudioSetPan(uint8_t slot, jlAudioPanE pan);
|
||||||
|
----
|
||||||
|
|
||||||
|
Places a sound effect slot in the stereo field. Slots start centred, and the
|
||||||
|
setting sticks to the *slot*, surviving retriggers -- so set it once for a
|
||||||
|
sound that should come from one side.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`slot` |`uint8_t` |Slot 0..4.
|
||||||
|
|`pan` |`jlAudioPanE` |Placement. Out-of-range values are ignored.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
What each port can actually do:
|
||||||
|
|
||||||
|
[cols="1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Platform |Behaviour
|
||||||
|
|
||||||
|
|IIgs |All three. The DOC pans per oscillator, so a centred sound plays on two
|
||||||
|
oscillators, one per side. A stock IIgs sums to mono, so this shows up only on
|
||||||
|
a stereo card or an emulator.
|
||||||
|
|Amiga |Left and right only, by choosing the Paula channel side. Paula cannot
|
||||||
|
place one voice in both ears, so `AUDIO_PAN_CENTER` leaves PTPlayer's own
|
||||||
|
channel choice alone.
|
||||||
|
|X68000 |Ignored. Everything mixes into one ADPCM stream, so panning would move
|
||||||
|
all audio at once.
|
||||||
|
|DOS, ST |Ignored. Output is mono.
|
||||||
|
|===
|
||||||
|
|
||||||
|
=== jlAudioStopSfx
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlAudioStopSfx(uint8_t slot);
|
||||||
|
----
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`slot` |`uint8_t` |Slot to silence. A no-op if already idle.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlAudioTone
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlAudioTone(uint16_t freqHz);
|
||||||
|
----
|
||||||
|
|
||||||
|
Direct hardware tone generator: plays a square wave continuously until the next
|
||||||
|
call. Designed for AGI-era music where the engine programs a divisor and lets
|
||||||
|
the chip play, so CPU cost during playback is zero. Single voice; uses voice 0
|
||||||
|
on platforms with several oscillators. On DOS this programs PIT counter 2 and
|
||||||
|
the speaker gate.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`freqHz` |`uint16_t` |Frequency in Hz. 0 silences the generator.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing. A no-op on platforms with no reachable tone generator --
|
||||||
|
fall back to `jlAudioPlaySfxStream` there if you need synthesised sound.
|
||||||
|
|
||||||
|
=== jlAudioVoice
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten);
|
||||||
|
----
|
||||||
|
|
||||||
|
Three-voice PSG-style polyphonic tone generator.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`voice` |`uint8_t` |Voice 0, 1 or 2.
|
||||||
|
|`freqHz` |`uint16_t` |Frequency in Hz. 0 silences this voice.
|
||||||
|
|`atten` |`uint8_t` |AGI / SN76489-style 4-bit attenuation: 0 loudest,
|
||||||
|
15 silent, about 2 dB per step.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
Mapping to hardware: DOS uses AdLib OPL2 melodic channels 0..2; the ST uses
|
||||||
|
YM2149 channels A, B and C; the Amiga uses Paula channels 0..2 looping a
|
||||||
|
two-byte square sample; the IIgs uses Ensoniq DOC oscillators playing a
|
||||||
|
resident square.
|
||||||
|
|
||||||
|
Voice state is sticky -- a voice keeps its last frequency until you change it
|
||||||
|
-- and CPU cost during sustained playback is zero, because the hardware
|
||||||
|
oscillates on its own.
|
||||||
|
|
||||||
|
[IMPORTANT]
|
||||||
|
====
|
||||||
|
The IIgs is the exception to stickiness: each call is an approximately 80 ms
|
||||||
|
retriggered one-shot, so a note held longer must be re-asserted about once a
|
||||||
|
frame. It also requires `jlAudioInit`, because the DOC path rides the tracker
|
||||||
|
engine. On DOS, ST and Amiga this is direct hardware programming and works
|
||||||
|
without `jlAudioInit`.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlAudioNoise
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlAudioNoise(uint8_t pitch, uint8_t atten);
|
||||||
|
----
|
||||||
|
|
||||||
|
Noise and percussion generator -- the fourth logical voice of the tracker
|
||||||
|
layer.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`pitch` |`uint8_t` |0..31 in YM2149 noise-period style: 0 is the brightest
|
||||||
|
hiss (hats), 31 the lowest rumble (toms).
|
||||||
|
|`atten` |`uint8_t` |As `jlAudioVoice`. 15 or above silences.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
Mapping: the ST uses the YM2149 noise generator mixed onto channel C; DOS uses
|
||||||
|
the OPL2 rhythm-mode snare, leaving the three tone channels untouched; the
|
||||||
|
Amiga uses Paula channel 3 with an LFSR noise sample; the IIgs uses a resident
|
||||||
|
noise sample on slot 3 with the same retriggered one-shot model as its tone
|
||||||
|
voices.
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
On the ST, noise shares channel C's volume register with tone voice 2. Author
|
||||||
|
noise hits and voice-2 notes so they do not sound simultaneously; `songbake`
|
||||||
|
warns when they overlap.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlMusicPlay
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlMusicPlay(const uint8_t *data, uint32_t length, bool loop);
|
||||||
|
----
|
||||||
|
|
||||||
|
Plays a JYM1 event stream baked by `songbake` through the tone and noise layer.
|
||||||
|
One authored song plays on all five ports at register-write cost. Because there
|
||||||
|
is no CPU mixing, this is fully viable on the ST and cheap on DOS, which
|
||||||
|
`.MOD` playback is not.
|
||||||
|
|
||||||
|
Playback is pumped by `jlAudioFrameTick` and paced by `jlMillisElapsed`, so
|
||||||
|
tempo holds across 50, 60 and 70 Hz ports.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`data` |`const uint8_t *` |JYM1 stream. *Read in place* -- it must stay valid
|
||||||
|
until `jlMusicStop` or song end.
|
||||||
|
|`length` |`uint32_t` |Stream size in bytes.
|
||||||
|
|`loop` |`bool` |`true` restarts at the authored loop point, or the top if the
|
||||||
|
song has none. `false` silences the voices at song end.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* `true` on success; `false` on a malformed stream, in which case
|
||||||
|
nothing plays.
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
Call `jlAudioInit` first for the song to be audible on the IIgs -- see
|
||||||
|
`jlAudioVoice`.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlMusicStop
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlMusicStop(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Stops tracker playback and silences tones and noise. The playhead is discarded;
|
||||||
|
use `jlMusicPause` to keep it. Safe when nothing is playing.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlMusicIsPlaying
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlMusicIsPlaying(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* `true` while a stream is loaded and active. Paused counts as active;
|
||||||
|
stopped or finished does not.
|
||||||
|
|
||||||
|
=== jlMusicPause / jlMusicResume
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlMusicPause(void);
|
||||||
|
void jlMusicResume(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Pauses in place -- voices go silent, the playhead keeps its position -- and
|
||||||
|
picks up where it left off. The current row restarts, and wall-clock time spent
|
||||||
|
paused is not fast-forwarded through. Both are safe no-ops when there is no
|
||||||
|
active song.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlMusicSetAtten
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlMusicSetAtten(uint8_t atten);
|
||||||
|
----
|
||||||
|
|
||||||
|
Global music attenuation, added to every authored voice and noise attenuation.
|
||||||
|
Applies immediately to sounding voices and to everything played afterwards.
|
||||||
|
Step it over successive frames for fades.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`atten` |`uint8_t` |0 plays as authored, 15 is silent.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
`jlMusicPlay` does *not* reset this, so a fade-out carries into the next song
|
||||||
|
unless you clear it.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlAudioTickRegister
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
typedef void (*jlAudioTickFnT)(void);
|
||||||
|
bool jlAudioTickRegister(jlAudioTickFnT fn, uint16_t hz);
|
||||||
|
----
|
||||||
|
|
||||||
|
Registers a periodic callback for sound schedulers needing better than
|
||||||
|
frame-rate resolution. PSG-era music drivers use this to write register changes
|
||||||
|
at exact note boundaries: frame-rate scheduling batches several notes into one
|
||||||
|
frame, so the chip only hears the last write and short notes vanish.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`fn` |`jlAudioTickFnT` |Callback, fired from interrupt context. Pass `NULL` to
|
||||||
|
uninstall.
|
||||||
|
|`hz` |`uint16_t` |Firings per second.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* `true` if installed. Ports without a suitable timer source return
|
||||||
|
`false`.
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
`fn` runs in interrupt context. Keep it to memory reads and direct hardware
|
||||||
|
register writes -- no allocation, no `printf`, nothing blocking.
|
||||||
|
====
|
||||||
|
|
||||||
|
On DOS this reprograms PIT counter 0 and hooks INT 8, keeping the BIOS tick
|
||||||
|
counter advancing at the canonical 18.2 Hz through an accumulator so `time`
|
||||||
|
stays correct.
|
||||||
|
|
||||||
|
=== jlAudioCriticalEnter / jlAudioCriticalExit
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlAudioCriticalEnter(void);
|
||||||
|
void jlAudioCriticalExit(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Critical section against the tick interrupt. If you mutate state that the
|
||||||
|
registered tick callback reads, wrap the mutation in these so the interrupt
|
||||||
|
cannot observe a half-written value. On DOS this is `cli` and `sti`; on ports
|
||||||
|
with no audio interrupt both are no-ops.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
268
docs/ApiCore.adoc
Normal file
268
docs/ApiCore.adoc
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
== Core and Lifecycle
|
||||||
|
|
||||||
|
Declared in `<joey/core.h>`, included by `<joey/joey.h>`.
|
||||||
|
|
||||||
|
=== jlConfigT
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
typedef struct {
|
||||||
|
uint32_t codegenBytes;
|
||||||
|
uint32_t audioBytes;
|
||||||
|
} jlConfigT;
|
||||||
|
----
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Field |Type |Description
|
||||||
|
|
||||||
|
|`codegenBytes`
|
||||||
|
|`uint32_t`
|
||||||
|
|Size of the runtime compiled-sprite cache. `0` selects a per-platform
|
||||||
|
default: 8 KB on IIgs, 32 KB on Amiga, ST and DOS. On the IIgs the cache is
|
||||||
|
one Memory Manager block that may not span a 64 KB bank boundary, so values
|
||||||
|
above 65536 are clamped to 65536, and even a clamped request can fail if no
|
||||||
|
free bank exists. Size it against `jlSpriteCompiledSize()`.
|
||||||
|
|
||||||
|
|`audioBytes`
|
||||||
|
|`uint32_t`
|
||||||
|
|Reserved. Not yet consulted by any audio engine.
|
||||||
|
|===
|
||||||
|
|
||||||
|
=== jlInit
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlInit(const jlConfigT *config);
|
||||||
|
----
|
||||||
|
|
||||||
|
Initialises the library: takes over the display, allocates the stage and the
|
||||||
|
codegen arena, and brings up input. Must be called before any other JoeyLib
|
||||||
|
function.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`config` |`const jlConfigT *` |Configuration. Must not be `NULL`.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* `true` on success. On failure returns `false` and `jlLastError()`
|
||||||
|
describes the reason.
|
||||||
|
|
||||||
|
=== jlShutdown
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlShutdown(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Shuts the library down, restoring the display mode, releasing all library
|
||||||
|
resources and un-hooking any interrupt vectors the port installed. Call it on
|
||||||
|
every exit path; skipping it can leave the machine unusable.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlLastError
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
const char *jlLastError(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* the most recent error message, or `NULL` if there has been none. The
|
||||||
|
string is library-owned; do not free it.
|
||||||
|
|
||||||
|
=== jlPlatformName
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
const char *jlPlatformName(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* a human-readable platform identifier, for example `"Apple IIgs"` or
|
||||||
|
`"MS-DOS"`. Library-owned.
|
||||||
|
|
||||||
|
=== jlVersionString
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
const char *jlVersionString(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* the library version, for example `"1.0.0"`. Library-owned.
|
||||||
|
|
||||||
|
=== jlAlloc
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void *jlAlloc(uint32_t bytes);
|
||||||
|
----
|
||||||
|
|
||||||
|
Allocates a large or persistent buffer from the platform's native allocator
|
||||||
|
rather than the small C heap. *Use this instead of `malloc` for anything
|
||||||
|
substantial.* On the IIgs the C heap lives in bank 0 and is tiny, with a single
|
||||||
|
`malloc` capped around 32 KB, so multi-kilobyte caches and frame buffers must
|
||||||
|
come from the Memory Manager instead.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`bytes` |`uint32_t` |Number of bytes required.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* a pointer to the block, or `NULL` on failure. The memory is *not*
|
||||||
|
zero-filled.
|
||||||
|
|
||||||
|
=== jlFree
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlFree(void *p);
|
||||||
|
----
|
||||||
|
|
||||||
|
Releases a buffer from `jlAlloc`.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`p` |`void *` |Block to release. `NULL` is accepted and ignored.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlMemCopy
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlMemCopy(void *dst, const void *src, uint32_t bytes);
|
||||||
|
----
|
||||||
|
|
||||||
|
Copies a block. Same contract as `memcpy`, including that the regions must not
|
||||||
|
overlap, but the IIgs implements it as `MVN` block moves at 7 cycles per byte
|
||||||
|
instead of a libc byte loop, split correctly at bank boundaries.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`dst` |`void *` |Destination.
|
||||||
|
|`src` |`const void *` |Source. Must not overlap `dst`.
|
||||||
|
|`bytes` |`uint32_t` |Byte count.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlWaitVBL
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlWaitVBL(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Blocks until the next display vertical blank, pacing a game loop to the
|
||||||
|
display's native refresh rate. Cheap on every port, because the underlying
|
||||||
|
mechanism is always a hardware-level wait rather than a software timer.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlFrameCount
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint16_t jlFrameCount(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* a monotonic 16-bit frame counter.
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
You must poll faster than twice `jlFrameHz()` or edges are missed, and the
|
||||||
|
counter wraps roughly every 18 minutes at 60 Hz. For elapsed-time work prefer
|
||||||
|
`jlMillisElapsed`.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlFrameHz
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint16_t jlFrameHz(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* the nominal display frame rate in Hz -- 50 on Amiga PAL, 60 on IIgs,
|
||||||
|
ST NTSC and X68000, 70 on VGA mode 13h. The real cadence may drift slightly;
|
||||||
|
this is the figure to divide by when converting frames to seconds.
|
||||||
|
|
||||||
|
=== jlMillisElapsed
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint32_t jlMillisElapsed(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Monotonic millisecond counter since `jlInit`. DOS and ST read a real hardware
|
||||||
|
timer; IIgs and Amiga derive it from the frame counter, so the value steps at
|
||||||
|
frame granularity there. Two calls separated by a long render report the true
|
||||||
|
elapsed time, not the number of vertical blanks that fired.
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* milliseconds since `jlInit`.
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
On the frame-derived ports you must poll at least once every 65535 frames --
|
||||||
|
about 18 minutes at 60 Hz -- or wrapped frames are lost.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlRandom
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint32_t jlRandom(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Deterministic pseudo-random generator, xorshift32. Produces *bit-identical*
|
||||||
|
output on every JoeyLib port for a given seed, which `rand()` does not.
|
||||||
|
Period 2^32^-1. Suitable for games, not for cryptography.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* the next 32-bit value.
|
||||||
|
|
||||||
|
=== jlRandomRange
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint16_t jlRandomRange(uint16_t bound);
|
||||||
|
----
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`bound` |`uint16_t` |Exclusive upper bound.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* a value in `[0, bound)`. A `bound` of 0 returns 0.
|
||||||
|
|
||||||
|
=== jlRandomSeed
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlRandomSeed(uint32_t seed);
|
||||||
|
----
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`seed` |`uint32_t` |Any 32-bit value. A seed of 0 is remapped to 1, because
|
||||||
|
xorshift cannot escape the all-zero state.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
104
docs/ApiDebug.adoc
Normal file
104
docs/ApiDebug.adoc
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
== Debug Logging
|
||||||
|
|
||||||
|
Declared in `<joey/debug.h>`.
|
||||||
|
|
||||||
|
A crash-tracing logger that writes to a file beside the executable. Debugging a
|
||||||
|
program on an emulated 1988 machine is awkward, and this is usually the fastest
|
||||||
|
way to find out what it did before it stopped doing it. The headless run
|
||||||
|
scripts retrieve the log automatically.
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
The logger needs a multi-kilobyte ring buffer, which the tightest targets
|
||||||
|
cannot spare. Space Taxi compiles it out entirely on the IIgs and X68000 for
|
||||||
|
that reason. If you are debugging a memory-tight program, expect to do the
|
||||||
|
same and to observe hardware state through the emulator instead.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlLog
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlLog(const char *msg);
|
||||||
|
----
|
||||||
|
|
||||||
|
Appends a message to the log.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`msg` |`const char *` |NUL-terminated message.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlLogF
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlLogF(const char *fmt, ...);
|
||||||
|
----
|
||||||
|
|
||||||
|
Appends a formatted message, `printf` style.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`fmt` |`const char *` |Format string.
|
||||||
|
|`...` | |Arguments matching the format string.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
[TIP]
|
||||||
|
====
|
||||||
|
Keep format strings simple. The C libraries on these targets vary in what they
|
||||||
|
support, and on the IIgs the `printf` family is expensive in both code size and
|
||||||
|
time.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlLogFlush
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlLogFlush(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Flushes buffered log output to disk. Call it before anything that might crash
|
||||||
|
or exit, or the last and most interesting lines may never be written.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlLogReset
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlLogReset(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Clears the log, discarding previous contents. Call once at startup so each run
|
||||||
|
produces a fresh log rather than appending to the last one.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
int main(void) {
|
||||||
|
jlConfigT config;
|
||||||
|
|
||||||
|
config.codegenBytes = 32UL * 1024;
|
||||||
|
config.audioBytes = 0;
|
||||||
|
if (!jlInit(&config)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
jlLogReset();
|
||||||
|
jlLogF("build %s %s", __DATE__, __TIME__);
|
||||||
|
|
||||||
|
// ... game ...
|
||||||
|
|
||||||
|
jlLogFlush();
|
||||||
|
jlShutdown();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
----
|
||||||
226
docs/ApiDraw.adoc
Normal file
226
docs/ApiDraw.adoc
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
== Drawing Primitives
|
||||||
|
|
||||||
|
Declared in `<joey/draw.h>`.
|
||||||
|
|
||||||
|
All primitives clip to the surface rectangle. Out-of-bounds draws are silent
|
||||||
|
no-ops, never errors, and there are no unclipped variants in the public API --
|
||||||
|
you cannot corrupt memory by drawing off the edge. Every primitive marks the
|
||||||
|
rows it touched, so `jlStagePresent` knows what to copy.
|
||||||
|
|
||||||
|
Primitives work in colour indices. Per-scanline palette resolution happens only
|
||||||
|
at display time, so drawing does not care which palette a line uses.
|
||||||
|
|
||||||
|
=== jlSurfaceClear
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSurfaceClear(jlSurfaceT *s, uint8_t colorIndex);
|
||||||
|
----
|
||||||
|
|
||||||
|
Fills the whole surface with one colour index. Clearing to colour 0 is a
|
||||||
|
legitimate clear-to-black, not a skipped no-op.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Target surface.
|
||||||
|
|`colorIndex` |`uint8_t` |Colour index 0..15.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlDrawPixel
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex);
|
||||||
|
----
|
||||||
|
|
||||||
|
Plots one pixel. Off-surface coordinates are no-ops.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Target surface.
|
||||||
|
|`x`, `y` |`int16_t` |Position. Valid range is 0..319 and 0..199.
|
||||||
|
|`colorIndex` |`uint8_t` |Colour index 0..15.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlSamplePixel
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint8_t jlSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y);
|
||||||
|
----
|
||||||
|
|
||||||
|
Reads one pixel.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`const jlSurfaceT *` |Source surface.
|
||||||
|
|`x`, `y` |`int16_t` |Position.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* the colour index at that position, or 0 for an off-surface read.
|
||||||
|
|
||||||
|
=== jlDrawLine
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0,
|
||||||
|
int16_t x1, int16_t y1, uint8_t colorIndex);
|
||||||
|
----
|
||||||
|
|
||||||
|
Draws a Bresenham line. Both endpoints are inclusive. Off-surface pixels are
|
||||||
|
skipped individually, so a line that crosses the edge draws its visible part; a
|
||||||
|
line entirely off-surface draws nothing. Horizontal and vertical runs take fast
|
||||||
|
paths.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Target surface.
|
||||||
|
|`x0`, `y0` |`int16_t` |Start point, inclusive.
|
||||||
|
|`x1`, `y1` |`int16_t` |End point, inclusive.
|
||||||
|
|`colorIndex` |`uint8_t` |Colour index 0..15.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlDrawRect
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlDrawRect(jlSurfaceT *s, int16_t x, int16_t y,
|
||||||
|
uint16_t w, uint16_t h, uint8_t colorIndex);
|
||||||
|
----
|
||||||
|
|
||||||
|
Draws a one-pixel-wide rectangle outline. A 1xN or Nx1 rectangle degenerates to
|
||||||
|
a line, and 1x1 to a single pixel. Zero dimensions are no-ops.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Target surface.
|
||||||
|
|`x`, `y` |`int16_t` |Top-left corner.
|
||||||
|
|`w`, `h` |`uint16_t` |Size in pixels. Zero is a no-op.
|
||||||
|
|`colorIndex` |`uint8_t` |Colour index 0..15.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlFillRect
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y,
|
||||||
|
uint16_t w, uint16_t h, uint8_t colorIndex);
|
||||||
|
----
|
||||||
|
|
||||||
|
Fills a solid rectangle. Zero dimensions are no-ops.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Target surface.
|
||||||
|
|`x`, `y` |`int16_t` |Top-left corner.
|
||||||
|
|`w`, `h` |`uint16_t` |Size in pixels.
|
||||||
|
|`colorIndex` |`uint8_t` |Colour index 0..15.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlDrawCircle
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy,
|
||||||
|
uint16_t r, uint8_t colorIndex);
|
||||||
|
----
|
||||||
|
|
||||||
|
Draws a circle outline using the Bresenham midpoint algorithm. A radius of 0
|
||||||
|
plots a single pixel.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Target surface.
|
||||||
|
|`cx`, `cy` |`int16_t` |Centre.
|
||||||
|
|`r` |`uint16_t` |Radius in pixels.
|
||||||
|
|`colorIndex` |`uint8_t` |Colour index 0..15.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlFillCircle
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy,
|
||||||
|
uint16_t r, uint8_t colorIndex);
|
||||||
|
----
|
||||||
|
|
||||||
|
Fills a disk, emitting one span per scanline using midpoint symmetry. A radius
|
||||||
|
of 0 plots a single pixel.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Target surface.
|
||||||
|
|`cx`, `cy` |`int16_t` |Centre.
|
||||||
|
|`r` |`uint16_t` |Radius in pixels.
|
||||||
|
|`colorIndex` |`uint8_t` |Colour index 0..15.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlFloodFill
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlFloodFill(jlSurfaceT *s, int16_t x, int16_t y, uint8_t newColor);
|
||||||
|
----
|
||||||
|
|
||||||
|
Flood-fills a 4-connected region, replacing every pixel of the original colour
|
||||||
|
reachable by north, south, east and west steps. A no-op if the start point is
|
||||||
|
off-surface or already `newColor`.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Target surface.
|
||||||
|
|`x`, `y` |`int16_t` |Seed point.
|
||||||
|
|`newColor` |`uint8_t` |Replacement colour index.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlFloodFillBounded
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlFloodFillBounded(jlSurfaceT *s, int16_t x, int16_t y,
|
||||||
|
uint8_t newColor, uint8_t boundaryColor);
|
||||||
|
----
|
||||||
|
|
||||||
|
Flood-fills a 4-connected region, stopping at pixels of `boundaryColor` rather
|
||||||
|
than at pixels differing from the seed colour. Every reachable pixel that is
|
||||||
|
not the boundary colour becomes `newColor`.
|
||||||
|
|
||||||
|
This is the primitive for vector-art playback, such as Sierra-style picture
|
||||||
|
drawing: outline a closed region with `jlDrawLine` in the boundary colour, then
|
||||||
|
fill it with this.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Target surface.
|
||||||
|
|`x`, `y` |`int16_t` |Seed point.
|
||||||
|
|`newColor` |`uint8_t` |Replacement colour index.
|
||||||
|
|`boundaryColor` |`uint8_t` |Colour that stops the fill.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
151
docs/ApiFile.adoc
Normal file
151
docs/ApiFile.adoc
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
== Files and Saves
|
||||||
|
|
||||||
|
Declared in `<joey/file.h>`.
|
||||||
|
|
||||||
|
Two directories sit beside the executable, staged there by each platform's disk
|
||||||
|
packager:
|
||||||
|
|
||||||
|
* `DATA/` -- read-only game content: levels, sprites, audio.
|
||||||
|
* `SAVES/` -- writable saves and preferences.
|
||||||
|
|
||||||
|
Both are addressed by a *bare name relative to their directory*. The prefix is
|
||||||
|
added for you, so never hard-code `"DATA/"` or `"SAVES/"` yourself.
|
||||||
|
|
||||||
|
[IMPORTANT]
|
||||||
|
====
|
||||||
|
Keep names portable across the retro filesystems: at most 8 characters plus a
|
||||||
|
short extension, letters and digits only, so they satisfy both ProDOS and DOS
|
||||||
|
8.3. Do not depend on case being preserved.
|
||||||
|
====
|
||||||
|
|
||||||
|
A failed open or write is non-fatal and simply reports failure, so save code
|
||||||
|
can degrade gracefully on a platform or volume where `SAVES/` is unavailable.
|
||||||
|
|
||||||
|
=== jlDataOpen
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
FILE *jlDataOpen(const char *name, const char *mode);
|
||||||
|
----
|
||||||
|
|
||||||
|
Opens a read-only data file. The `DATA/` prefix is prepended automatically.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`name` |`const char *` |Name relative to `DATA/`, for example
|
||||||
|
`"levels/title.dat"`.
|
||||||
|
|`mode` |`const char *` |Standard `fopen` mode string.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* a `FILE *`, or `NULL` if the path would overflow the internal buffer
|
||||||
|
or the file cannot be opened.
|
||||||
|
|
||||||
|
=== jlSaveOpen
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
FILE *jlSaveOpen(const char *name, const char *mode);
|
||||||
|
----
|
||||||
|
|
||||||
|
Opens a save file for streaming. For write and append modes the `SAVES/`
|
||||||
|
directory is created on demand. Use this for large or incremental saves; for a
|
||||||
|
whole-file blob prefer `jlSaveWrite` and `jlSaveRead`.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`name` |`const char *` |Name relative to `SAVES/`, for example `"hero.sav"`.
|
||||||
|
|`mode` |`const char *` |Standard `fopen` mode string.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* a `FILE *`, or `NULL` on failure.
|
||||||
|
|
||||||
|
=== jlSaveWrite
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlSaveWrite(const char *name, const void *buf, uint32_t len);
|
||||||
|
----
|
||||||
|
|
||||||
|
Writes a whole save file, replacing any existing contents.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`name` |`const char *` |Name relative to `SAVES/`.
|
||||||
|
|`buf` |`const void *` |Data to write.
|
||||||
|
|`len` |`uint32_t` |Byte count.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* `true` only if all `len` bytes were written.
|
||||||
|
|
||||||
|
=== jlSaveRead
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint32_t jlSaveRead(const char *name, void *buf, uint32_t max);
|
||||||
|
----
|
||||||
|
|
||||||
|
Reads a whole save file.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`name` |`const char *` |Name relative to `SAVES/`.
|
||||||
|
|`buf` |`void *` |Destination buffer.
|
||||||
|
|`max` |`uint32_t` |Buffer capacity.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* the number of bytes read; 0 if the file is missing or empty.
|
||||||
|
|
||||||
|
=== jlSaveExists
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlSaveExists(const char *name);
|
||||||
|
----
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`name` |`const char *` |Name relative to `SAVES/`.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* `true` if the file exists and can be opened for reading.
|
||||||
|
|
||||||
|
=== jlSaveDelete
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlSaveDelete(const char *name);
|
||||||
|
----
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`name` |`const char *` |Name relative to `SAVES/`.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* `true` if the file was removed.
|
||||||
|
|
||||||
|
=== jlDiskFree
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint32_t jlDiskFree(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* free space in bytes on the volume holding `SAVES/`. Returns 0 if it
|
||||||
|
cannot be determined -- some ports have no query for it -- and saturates at
|
||||||
|
`UINT32_MAX` on volumes larger than 4 GB.
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
// Check before writing a large save on a floppy-based target.
|
||||||
|
if (jlDiskFree() < neededBytes) {
|
||||||
|
showMessage("Disk full");
|
||||||
|
}
|
||||||
|
----
|
||||||
257
docs/ApiInput.adoc
Normal file
257
docs/ApiInput.adoc
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
== Input
|
||||||
|
|
||||||
|
Declared in `<joey/input.h>`.
|
||||||
|
|
||||||
|
Call `jlInputPoll()` exactly once per frame to refresh keyboard, mouse and
|
||||||
|
joystick state. Everything else in this section reads the state that poll
|
||||||
|
captured.
|
||||||
|
|
||||||
|
Two parallel keyboard interfaces share that one poll. The *predicates* answer
|
||||||
|
"is the fire key held?", which is the right shape for games. The *character
|
||||||
|
queue* answers "which character did the user type?", with shift, caps lock and
|
||||||
|
the machine's keyboard layout already applied, which is the right shape for
|
||||||
|
text entry. They coexist freely.
|
||||||
|
|
||||||
|
Edge predicates are one-shot: they return `true` only in the frame the
|
||||||
|
transition happened.
|
||||||
|
|
||||||
|
=== Enumerations
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
typedef enum {
|
||||||
|
KEY_NONE = 0,
|
||||||
|
KEY_A .. KEY_Z,
|
||||||
|
KEY_0 .. KEY_9,
|
||||||
|
KEY_SPACE, KEY_ESCAPE, KEY_RETURN, KEY_TAB, KEY_BACKSPACE,
|
||||||
|
KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT,
|
||||||
|
KEY_LSHIFT, KEY_RSHIFT, KEY_LCTRL, KEY_LALT,
|
||||||
|
KEY_F1 .. KEY_F10,
|
||||||
|
KEY_COUNT
|
||||||
|
} jlKeyE;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
MOUSE_BUTTON_NONE = 0, MOUSE_BUTTON_LEFT,
|
||||||
|
MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_COUNT
|
||||||
|
} jlMouseButtonE;
|
||||||
|
|
||||||
|
typedef enum { JOYSTICK_0 = 0, JOYSTICK_1, JOYSTICK_COUNT } jlJoystickE;
|
||||||
|
typedef enum { JOY_BUTTON_0 = 0, JOY_BUTTON_1, JOY_BUTTON_COUNT } jlJoyButtonE;
|
||||||
|
----
|
||||||
|
|
||||||
|
Constants: `JOYSTICK_AXIS_MAX` (127) and `JOYSTICK_AXIS_MIN` (-127);
|
||||||
|
`JL_CHAR_QUEUE_SIZE` (32).
|
||||||
|
|
||||||
|
Control codes delivered through the character queue: `JL_CHAR_BACKSPACE`
|
||||||
|
(0x08), `JL_CHAR_TAB` (0x09), `JL_CHAR_RETURN` (0x0D), `JL_CHAR_ESCAPE` (0x1B)
|
||||||
|
and `JL_CHAR_DELETE` (0x7F). That last one is *forward* delete, the key marked
|
||||||
|
Del -- a text editor needs both it and backspace.
|
||||||
|
|
||||||
|
=== jlInputPoll
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlInputPoll(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Refreshes all input state and refills the typed-character queue. Call once per
|
||||||
|
frame, typically just before drawing.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
Exactly once per frame. Polling twice makes edge predicates miss transitions;
|
||||||
|
not polling means nothing ever changes.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlInputGetChar
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
int jlInputGetChar(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Pops the next typed character. Shift, caps lock and the keyboard layout have
|
||||||
|
already been applied by the backend, so punctuation and shifted symbols arrive
|
||||||
|
correctly. The layout is the machine's own on IIgs, Amiga, ST and X68000; the
|
||||||
|
DOS port translates with a fixed US layout.
|
||||||
|
|
||||||
|
Arrow keys are not characters and stay on the predicate API.
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* a 7-bit code -- printable ASCII 0x20..0x7E, or one of the
|
||||||
|
`JL_CHAR_*` control codes -- or `-1` if the queue is empty.
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
The queue holds `JL_CHAR_QUEUE_SIZE - 1` characters, so 31. When full, further
|
||||||
|
characters are dropped until you pop some. Auto-repeat may deliver repeats on
|
||||||
|
ports whose keyboard services repeat; repeat policy is yours to manage. 7-bit
|
||||||
|
ASCII only -- no Unicode, no input methods.
|
||||||
|
|
||||||
|
The return type is plain `int`, not a stdint type, deliberately: this is the
|
||||||
|
`getchar()`-style "byte or -1" contract.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlWaitForAnyKey
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlWaitForAnyKey(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Blocks until the user presses any key, polling internally so per-port input
|
||||||
|
machinery -- including interrupt-driven audio samplers -- keeps running during
|
||||||
|
the wait.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing. The character queue is emptied on
|
||||||
|
return, so the dismissing keystroke is consumed rather than delivered as text.
|
||||||
|
|
||||||
|
=== jlKeyDown / jlKeyPressed / jlKeyReleased
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlKeyDown(jlKeyE key);
|
||||||
|
bool jlKeyPressed(jlKeyE key);
|
||||||
|
bool jlKeyReleased(jlKeyE key);
|
||||||
|
----
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`key` |`jlKeyE` |Key to test.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* `jlKeyDown` is `true` while the key is held. `jlKeyPressed` is
|
||||||
|
`true` only in the frame the key went down; `jlKeyReleased` only in the frame
|
||||||
|
it came up.
|
||||||
|
|
||||||
|
=== jlMousePresent
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlMousePresent(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* `true` if the platform confirmed a mouse at init. On DOS that means
|
||||||
|
the INT 33h driver reset successfully; on Amiga, ST, IIgs and X68000 the OS
|
||||||
|
mouse is always wired.
|
||||||
|
|
||||||
|
=== jlMouseX / jlMouseY
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
int16_t jlMouseX(void);
|
||||||
|
int16_t jlMouseY(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* pointer position in surface coordinates, 0..319 and 0..199.
|
||||||
|
|
||||||
|
=== jlMouseDown / jlMousePressed / jlMouseReleased
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlMouseDown(jlMouseButtonE button);
|
||||||
|
bool jlMousePressed(jlMouseButtonE button);
|
||||||
|
bool jlMouseReleased(jlMouseButtonE button);
|
||||||
|
----
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`button` |`jlMouseButtonE` |Which button.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* held state, rising edge, and falling edge respectively.
|
||||||
|
|
||||||
|
=== jlJoystickConnected
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlJoystickConnected(jlJoystickE js);
|
||||||
|
----
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`js` |`jlJoystickE` |Which stick, `JOYSTICK_0` or `JOYSTICK_1`.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* `true` if the platform reports a stick on that port.
|
||||||
|
|
||||||
|
=== jlJoystickX / jlJoystickY
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
int8_t jlJoystickX(jlJoystickE js);
|
||||||
|
int8_t jlJoystickY(jlJoystickE js);
|
||||||
|
----
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`js` |`jlJoystickE` |Which stick.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* axis value, -127 to +127. Digital sticks snap to the extremes;
|
||||||
|
analog sticks return the raw centred value. Test against a threshold rather
|
||||||
|
than for exact values, so the same code works with both.
|
||||||
|
|
||||||
|
=== jlJoyDown / jlJoyPressed / jlJoyReleased
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlJoyDown(jlJoystickE js, jlJoyButtonE button);
|
||||||
|
bool jlJoyPressed(jlJoystickE js, jlJoyButtonE button);
|
||||||
|
bool jlJoyReleased(jlJoystickE js, jlJoyButtonE button);
|
||||||
|
----
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`js` |`jlJoystickE` |Which stick.
|
||||||
|
|`button` |`jlJoyButtonE` |Which button.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* held state, rising edge, and falling edge respectively.
|
||||||
|
|
||||||
|
=== jlJoystickReset
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlJoystickReset(jlJoystickE js, uint8_t deadZone);
|
||||||
|
----
|
||||||
|
|
||||||
|
Re-enables joystick polling and recalibrates the resting position.
|
||||||
|
|
||||||
|
This matters on the IIgs. That port *auto-disables* joystick polling after a
|
||||||
|
short window with no stick detected, because polling costs about 3 ms a frame
|
||||||
|
there -- and it does not re-probe on its own. If a user plugs a stick in while
|
||||||
|
your program runs, you must call this to resume polling.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`js` |`jlJoystickE` |Which stick.
|
||||||
|
|`deadZone` |`uint8_t` |Raw readings within this many units of centre clamp
|
||||||
|
to 0. Pass 0 to disable the dead zone.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
[IMPORTANT]
|
||||||
|
====
|
||||||
|
The next poll after this call captures the stick's *current* raw position as
|
||||||
|
the new centre, so the user must be holding the stick centred when you call it.
|
||||||
|
|
||||||
|
On Amiga, ST and DOS the sticks are genuinely digital and already report -1, 0
|
||||||
|
or +1, so recalibration is a no-op and `deadZone` is ignored. The call still
|
||||||
|
clears any auto-disconnect state, so it is safe and correct to make it
|
||||||
|
unconditionally.
|
||||||
|
====
|
||||||
120
docs/ApiPalette.adoc
Normal file
120
docs/ApiPalette.adoc
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
== Palette and Scanline Control
|
||||||
|
|
||||||
|
Declared in `<joey/palette.h>`.
|
||||||
|
|
||||||
|
Colours are 12-bit `$0RGB`: bits 11..8 red, 7..4 green, 3..0 blue. Each surface
|
||||||
|
holds 16 palettes of 16 colours, plus a 200-entry table (the SCB table) naming
|
||||||
|
which palette each scanline uses. That is how a JoeyLib program shows more than
|
||||||
|
16 colours at once.
|
||||||
|
|
||||||
|
[IMPORTANT]
|
||||||
|
====
|
||||||
|
Colour 0 of every palette is black, always. `jlPaletteSet` silently masks entry
|
||||||
|
0 to `$000` whatever you pass. This is deliberate: the Amiga and ST hardware
|
||||||
|
border follows colour 0 of the active palette, and colour 0 is the transparent
|
||||||
|
colour for sprite drawing. You have 15 usable colours per palette.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlPaletteSet
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlPaletteSet(jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
|
||||||
|
----
|
||||||
|
|
||||||
|
Loads all 16 colours of one palette.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Target surface.
|
||||||
|
|`paletteIndex` |`uint8_t` |Which palette, 0..15.
|
||||||
|
|`colors16` |`const uint16_t *` |Exactly 16 `$0RGB` values. Entry 0 is forced
|
||||||
|
to `$000` regardless of what you supply.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlPaletteGet
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlPaletteGet(const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16);
|
||||||
|
----
|
||||||
|
|
||||||
|
Reads all 16 colours of one palette.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`const jlSurfaceT *` |Source surface.
|
||||||
|
|`paletteIndex` |`uint8_t` |Which palette, 0..15.
|
||||||
|
|`out16` |`uint16_t *` |Receives 16 `$0RGB` values. Caller-owned, and must have
|
||||||
|
room for all 16.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlScbSet
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlScbSet(jlSurfaceT *s, uint16_t line, uint8_t paletteIndex);
|
||||||
|
----
|
||||||
|
|
||||||
|
Assigns a palette to a single scanline.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Target surface.
|
||||||
|
|`line` |`uint16_t` |Scanline, 0..199.
|
||||||
|
|`paletteIndex` |`uint8_t` |Palette to use on that line, 0..15.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlScbSetRange
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlScbSetRange(jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine,
|
||||||
|
uint8_t paletteIndex);
|
||||||
|
----
|
||||||
|
|
||||||
|
Assigns a palette to an inclusive range of scanlines. The usual way to set up a
|
||||||
|
screen, whether that is one palette everywhere or several horizontal bands.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Target surface.
|
||||||
|
|`firstLine` |`uint16_t` |First scanline, inclusive.
|
||||||
|
|`lastLine` |`uint16_t` |Last scanline, inclusive.
|
||||||
|
|`paletteIndex` |`uint8_t` |Palette to use, 0..15.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
// Sky on the top third, ground below, using two palettes.
|
||||||
|
jlScbSetRange(stage, 0, 65, 0);
|
||||||
|
jlScbSetRange(stage, 66, SURFACE_HEIGHT - 1, 1);
|
||||||
|
----
|
||||||
|
|
||||||
|
=== jlScbGet
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint8_t jlScbGet(const jlSurfaceT *s, uint16_t line);
|
||||||
|
----
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`const jlSurfaceT *` |Source surface.
|
||||||
|
|`line` |`uint16_t` |Scanline, 0..199.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* the palette index assigned to that scanline.
|
||||||
212
docs/ApiSerial.adoc
Normal file
212
docs/ApiSerial.adoc
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
== Serial (Optional Add-On)
|
||||||
|
|
||||||
|
Declared in `<joey/serial.h>`.
|
||||||
|
|
||||||
|
[IMPORTANT]
|
||||||
|
====
|
||||||
|
This header is *not* pulled in by `<joey/joey.h>`. Include it explicitly:
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
#include <joey/joey.h>
|
||||||
|
#include <joey/serial.h>
|
||||||
|
----
|
||||||
|
|
||||||
|
A program that never references `jlSerial*` pays nothing -- the linker drops
|
||||||
|
the module entirely.
|
||||||
|
====
|
||||||
|
|
||||||
|
One byte-stream API spans every port while the HAL hides very different
|
||||||
|
hardware: the IIgs drives a Zilog 8530 SCC directly or a 6551 card in a slot;
|
||||||
|
DOS uses an interrupt-driven 8250/16550 UART; the Amiga uses `serial.device`;
|
||||||
|
the ST goes through the BIOS Aux channel.
|
||||||
|
|
||||||
|
The model is *non-blocking and buffered*. Received bytes accumulate in a
|
||||||
|
background ring -- a hardware interrupt on DOS, the OS on Amiga and ST, or
|
||||||
|
`jlSerialPoll` on the IIgs -- and reads never block. Call `jlSerialPoll` once
|
||||||
|
per frame: it is a no-op where the interrupt or OS already fills the ring, and
|
||||||
|
the hardware drain where it does not, so calling it every frame is always
|
||||||
|
correct.
|
||||||
|
|
||||||
|
A failed `jlSerialOpen` is non-fatal: it returns `false` and every other call
|
||||||
|
becomes a safe no-op, exactly like a failed audio init.
|
||||||
|
|
||||||
|
=== Types
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
typedef enum {
|
||||||
|
JL_SERIAL_DEFAULT = 0, // the platform's natural RS-232 port
|
||||||
|
JL_SERIAL_MODEM, // IIgs modem port (SCC channel A)
|
||||||
|
JL_SERIAL_PRINTER, // IIgs printer port (SCC channel B)
|
||||||
|
JL_SERIAL_SLOT // card/port selected by config->unit
|
||||||
|
} jlSerialDeviceE;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
JL_SERIAL_PARITY_NONE = 0, JL_SERIAL_PARITY_ODD, JL_SERIAL_PARITY_EVEN
|
||||||
|
} jlSerialParityE;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
JL_SERIAL_FLOW_NONE = 0, // no handshaking
|
||||||
|
JL_SERIAL_FLOW_RTSCTS, // hardware RTS/CTS
|
||||||
|
JL_SERIAL_FLOW_XONXOFF // software XON/XOFF
|
||||||
|
} jlSerialFlowE;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t baud; // 300 .. 115200, clamped per port
|
||||||
|
uint8_t dataBits; // 5 .. 8
|
||||||
|
uint8_t stopBits; // 1 or 2
|
||||||
|
jlSerialParityE parity;
|
||||||
|
jlSerialFlowE flow;
|
||||||
|
uint8_t unit; // JL_SERIAL_SLOT only
|
||||||
|
} jlSerialConfigT;
|
||||||
|
----
|
||||||
|
|
||||||
|
The IIgs is the machine with three device choices; elsewhere `JL_SERIAL_MODEM`
|
||||||
|
and `JL_SERIAL_PRINTER` both mean the default port. `JL_SERIAL_DEFAULT` selects
|
||||||
|
the IIgs modem port, DOS COM1, the ST Modem port or Amiga unit 0.
|
||||||
|
|
||||||
|
`unit` disambiguates `JL_SERIAL_SLOT` and is ignored otherwise: an IIgs Super
|
||||||
|
Serial Card slot 1..7, a DOS COM number 1..4, or an Amiga `serial.device` unit.
|
||||||
|
|
||||||
|
=== jlSerialOpen
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config);
|
||||||
|
----
|
||||||
|
|
||||||
|
Opens and configures the port. Only one port may be open at a time; a second
|
||||||
|
open without an intervening close fails.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`device` |`jlSerialDeviceE` |Which interface.
|
||||||
|
|`config` |`const jlSerialConfigT *` |Line configuration.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* `true` on success. `false` if the port cannot be opened or the
|
||||||
|
platform has no serial support -- and then every other call no-ops.
|
||||||
|
|
||||||
|
=== jlSerialClose
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSerialClose(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Closes the port and releases its resources. Safe when not open.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlSerialPoll
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSerialPoll(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Pumps the receive and transmit buffers. Call once per game frame.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlSerialAvailable
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint16_t jlSerialAvailable(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* the number of bytes waiting in the receive buffer, readable now
|
||||||
|
without blocking.
|
||||||
|
|
||||||
|
=== jlSerialRead
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint16_t jlSerialRead(uint8_t *buf, uint16_t max);
|
||||||
|
----
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`buf` |`uint8_t *` |Destination buffer.
|
||||||
|
|`max` |`uint16_t` |Buffer capacity.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* the number of bytes copied, 0 to `max`. Never blocks.
|
||||||
|
|
||||||
|
=== jlSerialWrite
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len);
|
||||||
|
----
|
||||||
|
|
||||||
|
Sends bytes. Transmit is a bounded poll.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`buf` |`const uint8_t *` |Data to send.
|
||||||
|
|`len` |`uint16_t` |Byte count.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* the number of bytes accepted, which may be fewer than `len` if a
|
||||||
|
flow-controlled peer or a full transmit path stalls.
|
||||||
|
|
||||||
|
=== jlSerialWriteByte / jlSerialReadByte
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlSerialWriteByte(uint8_t b);
|
||||||
|
int16_t jlSerialReadByte(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Single-byte convenience calls.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`b` |`uint8_t` |Byte to send.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* `jlSerialWriteByte` returns `true` if the byte was accepted.
|
||||||
|
`jlSerialReadByte` returns the byte, or `-1` when none is ready.
|
||||||
|
|
||||||
|
=== jlSerialFlush
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSerialFlush(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Discards any buffered received bytes.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlSerialSetRts
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSerialSetRts(bool on);
|
||||||
|
----
|
||||||
|
|
||||||
|
Asserts or drops RTS, for a caller doing its own receive-side flow control.
|
||||||
|
|
||||||
|
`JL_SERIAL_FLOW_RTSCTS` asks the *port* to handshake; this is the other half --
|
||||||
|
the application saying "my buffer is nearly full, stop sending", and later "go
|
||||||
|
ahead". That is what a client with a small ring and a peer that can be told to
|
||||||
|
pause actually needs.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`on` |`bool` |`true` asserts RTS, meaning ready to receive.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing. A port with no controllable RTS line no-ops, and a closed
|
||||||
|
port no-ops; neither is an error, so this can be driven unconditionally.
|
||||||
406
docs/ApiSprite.adoc
Normal file
406
docs/ApiSprite.adoc
Normal file
|
|
@ -0,0 +1,406 @@
|
||||||
|
== Sprites
|
||||||
|
|
||||||
|
Declared in `<joey/sprite.h>`.
|
||||||
|
|
||||||
|
A sprite is a rectangle of 8x8 tiles drawn at an arbitrary pixel position with
|
||||||
|
colour-0 transparency. Sprite pixel data is `widthTiles * heightTiles * 32`
|
||||||
|
bytes of 4bpp packed data, laid out as a flat blob of tiles in row-major order.
|
||||||
|
|
||||||
|
The distinguishing feature is that *sprite blitters are compiled at run time*.
|
||||||
|
The library emits native 65816, 68000 or x86 code for each sprite, including
|
||||||
|
pre-shifted variants so smooth horizontal motion needs no runtime bit shifting.
|
||||||
|
|
||||||
|
[IMPORTANT]
|
||||||
|
====
|
||||||
|
`jlSpriteDraw` does *not* compile on demand. An uncompiled sprite draws through
|
||||||
|
an interpreter roughly *forty times slower*, and nothing reports this. Always
|
||||||
|
call `jlSpriteCompile` or `jlSpritePrewarm` during loading.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlSpriteT and jlSpriteBackupT
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
typedef struct jlSpriteT jlSpriteT; // opaque
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
jlSpriteT *sprite;
|
||||||
|
int16_t x;
|
||||||
|
int16_t y;
|
||||||
|
uint16_t width; // pixels
|
||||||
|
uint16_t height; // pixels
|
||||||
|
uint8_t *bytes; // caller-owned, capacity >= sizeBytes
|
||||||
|
uint16_t sizeBytes;
|
||||||
|
} jlSpriteBackupT;
|
||||||
|
----
|
||||||
|
|
||||||
|
You supply `bytes` and its capacity; the library fills in the rest to describe
|
||||||
|
what it actually captured. On planar ports the captured region is the
|
||||||
|
*16-pixel-aligned window* containing the sprite, not the sprite rectangle, and
|
||||||
|
the `x`, `width`, `height` and `sizeBytes` fields report that true geometry. It
|
||||||
|
is the recorded window you must not overwrite before restoring.
|
||||||
|
|
||||||
|
=== JOEY_SPRITE_BACKUP_BYTES
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
#define JOEY_SPRITE_BACKUP_BYTES(_wT, _hT) /* ... */
|
||||||
|
----
|
||||||
|
|
||||||
|
Worst-case backup capacity for a `widthTiles` by `heightTiles` sprite at any x
|
||||||
|
position, on every port. Use it to size your backing array.
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
// 2-byte aligned by construction -- see the warning below.
|
||||||
|
static uint16_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4) / 2];
|
||||||
|
----
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
Give the backing array *at least 2-byte alignment*. An odd buffer address
|
||||||
|
routes save and restore onto slow interpreted paths on the planar ports,
|
||||||
|
because the 68000 cannot do word accesses at odd addresses. Declaring it
|
||||||
|
`uint16_t`, as above, guarantees this.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlSpriteBankLoad
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels,
|
||||||
|
uint16_t maxCels, uint16_t *outPalette);
|
||||||
|
----
|
||||||
|
|
||||||
|
Loads cels from a baked `.spr` file. Each cel becomes a freshly allocated
|
||||||
|
`jlSpriteT`, released with `jlSpriteDestroy`. `.spr` banks are cross-target --
|
||||||
|
the same file works on every platform.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`path` |`const char *` |File path.
|
||||||
|
|`outCels` |`jlSpriteT **` |Receives the sprites. Caller-owned array.
|
||||||
|
|`maxCels` |`uint16_t` |Capacity of `outCels`; the file's cel count is capped
|
||||||
|
to this.
|
||||||
|
|`outPalette` |`uint16_t *` |Optional, may be `NULL`. Receives the embedded
|
||||||
|
16-entry `$0RGB` palette if present; otherwise left unchanged.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* the number of cels written. Returns 0 if the file is missing, the
|
||||||
|
magic or target byte is wrong, or no cel could be allocated.
|
||||||
|
|
||||||
|
=== jlSpriteBankLoadPrecompiled
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels,
|
||||||
|
uint16_t maxCels, uint16_t *outPalette);
|
||||||
|
----
|
||||||
|
|
||||||
|
Loads cels from a `.spc` pre-compiled bank built for *this* platform. Identical
|
||||||
|
contract to `jlSpriteBankLoad`, except each cel arrives already compiled: its
|
||||||
|
native routines are copied straight into the codegen arena and it draws at full
|
||||||
|
speed with no runtime compilation.
|
||||||
|
|
||||||
|
A cel that does not fit the arena loads uncompiled and falls back to the
|
||||||
|
interpreter -- never an error. This makes it a drop-in, faster-starting
|
||||||
|
replacement for `jlSpriteBankLoad` plus a compile pass.
|
||||||
|
|
||||||
|
*Parameters:* as `jlSpriteBankLoad`.
|
||||||
|
|
||||||
|
*Returns:* the number of cels written, or 0 if the file is missing or its
|
||||||
|
magic, target or shift count does not match this build.
|
||||||
|
|
||||||
|
=== jlSpriteBankSavePrecompiled
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels,
|
||||||
|
uint16_t count, const uint16_t *palette);
|
||||||
|
----
|
||||||
|
|
||||||
|
Writes an already-compiled bank to a `.spc` cache file -- the runtime twin of
|
||||||
|
the offline `spritebake`. Call it after loading and compiling so a later launch
|
||||||
|
can skip the compile. This lets you ship the small cross-platform `.spr` and
|
||||||
|
cache the large per-platform compiled form on first run.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`path` |`const char *` |Destination path.
|
||||||
|
|`cels` |`jlSpriteT **` |The sprites to cache. Uncompiled ones are cached
|
||||||
|
as-is and will compile again next run.
|
||||||
|
|`count` |`uint16_t` |How many sprites.
|
||||||
|
|`palette` |`const uint16_t *` |Optional, may be `NULL`. Embedded if given.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* `true` on success; `false` if the destination is not writable, which
|
||||||
|
is harmless -- the program simply keeps compiling at startup.
|
||||||
|
|
||||||
|
=== jlSpriteCreate
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles,
|
||||||
|
uint8_t heightTiles);
|
||||||
|
----
|
||||||
|
|
||||||
|
Wraps a tile-data blob in a sprite.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`tileData` |`const uint8_t *` |`widthTiles * heightTiles * 32` bytes. *Not
|
||||||
|
copied* -- it must outlive the sprite, and you keep ownership.
|
||||||
|
|`widthTiles` |`uint8_t` |Width in tiles. Must be non-zero.
|
||||||
|
|`heightTiles` |`uint8_t` |Height in tiles. Must be non-zero.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* the sprite, or `NULL` if either dimension is 0 or the codegen arena
|
||||||
|
cannot fit a placeholder entry.
|
||||||
|
|
||||||
|
=== jlSpriteDestroy
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSpriteDestroy(jlSpriteT *sp);
|
||||||
|
----
|
||||||
|
|
||||||
|
Releases a sprite and any codegen entries cached for it. The tile data it was
|
||||||
|
built from is *not* freed -- that buffer is yours.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`sp` |`jlSpriteT *` |Sprite to release.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlSpriteCompile
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlSpriteCompile(jlSpriteT *sp);
|
||||||
|
----
|
||||||
|
|
||||||
|
Compiles the sprite's draw routines into the codegen arena. Idempotent --
|
||||||
|
compiling an already-compiled sprite is a no-op that returns `true`.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`sp` |`jlSpriteT *` |Sprite to compile.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* `true` if the sprite is compiled. `false` if the arena is full (you
|
||||||
|
may call `jlSpriteCompact` and retry) or the sprite has no source tile data.
|
||||||
|
|
||||||
|
=== jlSpriteCompiledSize
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint32_t jlSpriteCompiledSize(const jlSpriteT *sp);
|
||||||
|
----
|
||||||
|
|
||||||
|
Diagnostic. The right way to size `jlConfigT.codegenBytes` for real artwork on
|
||||||
|
the current platform, rather than guessing.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`sp` |`const jlSpriteT *` |Sprite to measure.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* bytes occupied in the codegen arena, summed over all compiled shift
|
||||||
|
variants. Returns 0 if the sprite was never compiled.
|
||||||
|
|
||||||
|
=== jlSpritePrewarm
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSpritePrewarm(jlSpriteT *sp);
|
||||||
|
----
|
||||||
|
|
||||||
|
Hints that a sprite will be drawn soon. A wrapper around `jlSpriteCompile` that
|
||||||
|
ignores the return value, for callers that do not need to handle failure.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`sp` |`jlSpriteT *` |Sprite to compile.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlSpriteDraw
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
|
||||||
|
----
|
||||||
|
|
||||||
|
Draws the sprite at a pixel position. Source pixels of colour 0 are skipped.
|
||||||
|
Off-surface portions are clipped.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Destination surface.
|
||||||
|
|`sp` |`jlSpriteT *` |Sprite to draw.
|
||||||
|
|`x`, `y` |`int16_t` |Top-left pixel position.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlSpriteSaveUnder
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp,
|
||||||
|
int16_t x, int16_t y, jlSpriteBackupT *backup);
|
||||||
|
----
|
||||||
|
|
||||||
|
Captures the destination region that a `jlSpriteDraw` at the same position
|
||||||
|
would write, so it can be restored later.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`const jlSurfaceT *` |Source surface.
|
||||||
|
|`sp` |`jlSpriteT *` |Sprite whose footprint to capture.
|
||||||
|
|`x`, `y` |`int16_t` |Position the sprite will be drawn at.
|
||||||
|
|`backup` |`jlSpriteBackupT *` |Receives the capture. You must set `bytes` to
|
||||||
|
a buffer of at least `JOEY_SPRITE_BACKUP_BYTES(w, h)` capacity for fully
|
||||||
|
in-bounds draws. For clipped draws only the visible bytes are stored.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing. The captured geometry is reported in the backup's `x`,
|
||||||
|
`width`, `height` and `sizeBytes` fields.
|
||||||
|
|
||||||
|
=== jlSpriteRestoreUnder
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup);
|
||||||
|
----
|
||||||
|
|
||||||
|
Repaints the destination from a previous capture.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Destination surface.
|
||||||
|
|`backup` |`const jlSpriteBackupT *` |A backup from `jlSpriteSaveUnder`, not
|
||||||
|
invalidated by other writes overlapping its recorded window.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlSpriteSaveAndDraw
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
|
||||||
|
jlSpriteBackupT *backup);
|
||||||
|
----
|
||||||
|
|
||||||
|
Captures then draws, in one call. Identical in effect to `jlSpriteSaveUnder`
|
||||||
|
followed by `jlSpriteDraw`, but validation is shared, the destination pointer
|
||||||
|
is computed once, and one dirty-rectangle mark covers both. Saves roughly a
|
||||||
|
full dispatcher chain per sprite per frame.
|
||||||
|
|
||||||
|
*Parameters:* as `jlSpriteSaveUnder`. *Returns:* nothing.
|
||||||
|
|
||||||
|
=== The Trusted Variants
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSpriteDrawTrusted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
|
||||||
|
void jlSpriteSaveUnderTrusted(const jlSurfaceT *s, jlSpriteT *sp,
|
||||||
|
int16_t x, int16_t y, jlSpriteBackupT *backup);
|
||||||
|
void jlSpriteRestoreUnderTrusted(jlSurfaceT *s, const jlSpriteBackupT *backup);
|
||||||
|
void jlSpriteSaveAndDrawTrusted(jlSurfaceT *s, jlSpriteT *sp,
|
||||||
|
int16_t x, int16_t y, jlSpriteBackupT *backup);
|
||||||
|
----
|
||||||
|
|
||||||
|
Expert inner-loop entry points. Each has the same effect as its plain
|
||||||
|
counterpart but *skips the per-call geometry validation* -- roughly eleven
|
||||||
|
range and alignment checks plus the NULL and sprite-slot gates.
|
||||||
|
|
||||||
|
Use them only in a game that pre-compiles its sprites and already clamps
|
||||||
|
positions on screen. Anything the fast path does not recognise falls back to
|
||||||
|
the fully validated call, so a conservative caller degrades safely rather than
|
||||||
|
corrupting -- but the guarantees below are still yours to keep.
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
Behaviour is *undefined* -- out-of-bounds writes, crashes -- unless all of the
|
||||||
|
following hold:
|
||||||
|
|
||||||
|
* `s`, `backup`, `backup->sprite` and `backup->bytes` are non-`NULL`;
|
||||||
|
* the sprite is fully on-surface at `(x, y)`, because *no clipping happens*;
|
||||||
|
* for the restore, the backup came from a save that took the compiled path:
|
||||||
|
16-pixel-aligned x and width on planar ports, even on chunky, height equal
|
||||||
|
to the sprite's pixel height, and a buffer at least 2-byte aligned;
|
||||||
|
* the recorded window is fully on the destination surface.
|
||||||
|
|
||||||
|
Use the plain calls anywhere those invariants are not statically guaranteed.
|
||||||
|
====
|
||||||
|
|
||||||
|
The canonical pattern is a trusted save-and-draw this frame, a trusted restore
|
||||||
|
next frame.
|
||||||
|
|
||||||
|
=== jlSpriteCreateFromSurface
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y,
|
||||||
|
uint8_t widthTiles, uint8_t heightTiles);
|
||||||
|
----
|
||||||
|
|
||||||
|
Snapshots a tile-aligned region of a surface into a new sprite. The pixels are
|
||||||
|
copied into a sprite-owned buffer, so the source may be modified afterwards.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`src` |`const jlSurfaceT *` |Source surface.
|
||||||
|
|`x`, `y` |`int16_t` |Top-left in *pixels*, and both must be multiples of 8.
|
||||||
|
|`widthTiles`, `heightTiles` |`uint8_t` |Size in *tiles*, each 8x8 pixels.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* the new sprite, or `NULL` if the coordinates are not tile-aligned.
|
||||||
|
|
||||||
|
=== jlSpriteCompact
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSpriteCompact(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Defragments the codegen arena, consolidating free space and reclaiming holes
|
||||||
|
left by destroyed sprites. Costs time proportional to the bytes in use, so call
|
||||||
|
it between levels rather than per frame.
|
||||||
|
|
||||||
|
Sprite pointers you hold are *not* invalidated: internal indirection means draw
|
||||||
|
calls pick up new code addresses automatically.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlSpriteCodegenBytesUsed / jlSpriteCodegenBytesTotal
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint32_t jlSpriteCodegenBytesUsed(void);
|
||||||
|
uint32_t jlSpriteCodegenBytesTotal(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Arena introspection, for judging whether to compact or to raise
|
||||||
|
`codegenBytes`.
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* bytes in use, and total arena size. Free space is the difference,
|
||||||
|
though it may be fragmented across holes until `jlSpriteCompact` runs.
|
||||||
229
docs/ApiSurface.adoc
Normal file
229
docs/ApiSurface.adoc
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
== Surfaces and Presenting
|
||||||
|
|
||||||
|
Declared in `<joey/surface.h>` and `<joey/present.h>`.
|
||||||
|
|
||||||
|
A surface is a 320x200 16-colour image with its own 200-entry SCB table and its
|
||||||
|
own 16x16 palette set. Storage is target-native -- chunky 4bpp on IIgs and DOS,
|
||||||
|
planar on Amiga, ST and X68000 -- but every entry point speaks in colour
|
||||||
|
indices, so your code never sees the difference.
|
||||||
|
|
||||||
|
=== Constants
|
||||||
|
|
||||||
|
[cols="3,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Name |Value |Meaning
|
||||||
|
|
||||||
|
|`SURFACE_WIDTH` |320 |Surface width in pixels.
|
||||||
|
|`SURFACE_HEIGHT` |200 |Surface height in pixels.
|
||||||
|
|`SURFACE_BYTES_PER_ROW` |160 |Bytes per row in chunky 4bpp form.
|
||||||
|
|`SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes.
|
||||||
|
|`SURFACE_PALETTE_COUNT` |16 |Palettes per surface.
|
||||||
|
|`SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette.
|
||||||
|
|`SURFACE_PALETTE_ENTRIES` |256 |Total palette entries per surface.
|
||||||
|
|===
|
||||||
|
|
||||||
|
=== jlSurfaceT
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
typedef struct jlSurfaceT jlSurfaceT;
|
||||||
|
----
|
||||||
|
|
||||||
|
Opaque. Obtain one from `jlSurfaceCreate`, `jlStageGet` or a loader.
|
||||||
|
|
||||||
|
=== jlSurfaceCreate
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
jlSurfaceT *jlSurfaceCreate(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Allocates a new offscreen surface.
|
||||||
|
|
||||||
|
*Parameters:* none.
|
||||||
|
|
||||||
|
*Returns:* the surface, or `NULL` on failure (`jlLastError()` explains why).
|
||||||
|
|
||||||
|
=== jlSurfaceDestroy
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSurfaceDestroy(jlSurfaceT *s);
|
||||||
|
----
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Surface to release. `NULL` is a no-op, and passing the
|
||||||
|
stage is also a no-op -- the library owns it.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlStageGet
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
jlSurfaceT *jlStageGet(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Returns the library-owned stage: the back buffer that `jlStagePresent` flips to
|
||||||
|
the display. Always valid between `jlInit` and `jlShutdown`.
|
||||||
|
|
||||||
|
On the IIgs the stage's pixel buffer is pinned to bank `$01` Super Hi-Res space
|
||||||
|
with shadowing inhibited, so writes run at full speed and stay isolated from
|
||||||
|
the displayed framebuffer until the next present.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* the stage surface.
|
||||||
|
|
||||||
|
=== jlSurfaceCopy
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src);
|
||||||
|
----
|
||||||
|
|
||||||
|
Copies pixels, SCBs and palettes from `src` into `dst`.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`dst` |`jlSurfaceT *` |Destination. Must be valid.
|
||||||
|
|`src` |`const jlSurfaceT *` |Source. Must be valid.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlSurfaceCopyRect
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src,
|
||||||
|
int16_t x, int16_t y, uint16_t w, uint16_t h);
|
||||||
|
----
|
||||||
|
|
||||||
|
Copies a pixel rectangle from `src` to `dst` *at the same position* -- there is
|
||||||
|
no repositioning, so the planar ports never have to shift bits. This is the
|
||||||
|
erase half of the clean-buffer pattern: keep a pristine copy of the scene, and
|
||||||
|
restore rectangles from it before redrawing sprites.
|
||||||
|
|
||||||
|
Copies pixels only. SCBs and palettes are untouched.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`dst` |`jlSurfaceT *` |Destination surface.
|
||||||
|
|`src` |`const jlSurfaceT *` |Source surface.
|
||||||
|
|`x`, `y` |`int16_t` |Top-left of the rectangle, clipped to the surface.
|
||||||
|
|`w`, `h` |`uint16_t` |Size in pixels.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
The rectangle is clipped to the surface and then *widened outward to the
|
||||||
|
16-pixel group grid on every port*: `x` rounds down and `x + w` rounds up to
|
||||||
|
multiples of 16. Two consequences you own:
|
||||||
|
|
||||||
|
* up to 15 extra pixels each side are copied, so a snapped erase can clobber
|
||||||
|
on-screen content outside the rectangle you asked for;
|
||||||
|
* it silently invalidates any live `jlSpriteBackupT` whose recorded window it
|
||||||
|
touches.
|
||||||
|
|
||||||
|
Erase *all* windows, then draw *all* sprites. Never mix this with per-sprite
|
||||||
|
save and restore in the same region.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlSurfaceSaveFile
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path);
|
||||||
|
----
|
||||||
|
|
||||||
|
Writes a surface to disk in target-native form: 32000 pixel bytes, then 200 SCB
|
||||||
|
bytes, then 512 bytes of `$0RGB` palette data.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`src` |`const jlSurfaceT *` |Surface to write.
|
||||||
|
|`path` |`const char *` |Destination path.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* `true` on success; `false` if the file could not be opened or the
|
||||||
|
write failed.
|
||||||
|
|
||||||
|
[IMPORTANT]
|
||||||
|
====
|
||||||
|
The format is *not portable*. No endianness or layout conversion is performed,
|
||||||
|
so the same platform that wrote a file must be the one that reads it. Use this
|
||||||
|
for runtime caches, not for shipped assets.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlSurfaceLoadFile
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path);
|
||||||
|
----
|
||||||
|
|
||||||
|
Reads a surface from disk, overwriting `dst`. The surface keeps its identity --
|
||||||
|
nothing is reallocated.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`dst` |`jlSurfaceT *` |Surface to overwrite.
|
||||||
|
|`path` |`const char *` |Source path.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* `true` on success; `false` if the file is missing, the wrong size,
|
||||||
|
or unreadable.
|
||||||
|
|
||||||
|
=== jlSurfaceHash
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint32_t jlSurfaceHash(const jlSurfaceT *s);
|
||||||
|
----
|
||||||
|
|
||||||
|
Computes a 32-bit content hash over the surface: the packed chunky pixel byte
|
||||||
|
stream, then the 200 SCB bytes, then the 256 palette entries.
|
||||||
|
|
||||||
|
The important property is that *the same logical pixels hash the same on every
|
||||||
|
port*, whatever the internal storage format. That makes it the tool for proving
|
||||||
|
two platforms render identically -- which is exactly how the library's own
|
||||||
|
cross-port validation works.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`const jlSurfaceT *` |Surface to hash.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* the hash value.
|
||||||
|
|
||||||
|
=== jlStagePresent
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlStagePresent(void);
|
||||||
|
----
|
||||||
|
|
||||||
|
Flips the dirty regions of the stage to the display and clears the dirty state.
|
||||||
|
On chunky platforms this is a direct copy; on planar platforms it is a
|
||||||
|
chunky-to-planar conversion.
|
||||||
|
|
||||||
|
Drawing primitives mark per-row dirty ranges as a side effect, so you only need
|
||||||
|
to call this at the end of a frame and everything you drew appears. Rows
|
||||||
|
nothing touched since the last present are skipped.
|
||||||
|
|
||||||
|
*Parameters:* none. *Returns:* nothing.
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
If you modify pixels by a route other than the library's primitives, the dirty
|
||||||
|
tracking does not know, and those rows will not be presented.
|
||||||
|
====
|
||||||
328
docs/ApiTile.adoc
Normal file
328
docs/ApiTile.adoc
Normal file
|
|
@ -0,0 +1,328 @@
|
||||||
|
== Tiles and Text
|
||||||
|
|
||||||
|
Declared in `<joey/tile.h>`.
|
||||||
|
|
||||||
|
A tile is not a separate object. It is the 8x8 region of a surface at block
|
||||||
|
coordinates `(bx, by)`, where `bx` runs 0..39 and `by` runs 0..24 -- 40x25
|
||||||
|
blocks across a 320x200 surface. The tile API moves 32-byte blocks between
|
||||||
|
surfaces or fills them.
|
||||||
|
|
||||||
|
The consequence is that everything else works on tiles too. You can paint tile
|
||||||
|
content with `jlDrawPixel` and `jlFillRect`, generate it procedurally, or edit
|
||||||
|
it at run time, because a "tileset" is just a surface you agree to treat as
|
||||||
|
one. Fonts, terrain sets and spritesheets are all the same thing.
|
||||||
|
|
||||||
|
=== Constants and Types
|
||||||
|
|
||||||
|
[cols="3,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Name |Value |Meaning
|
||||||
|
|
||||||
|
|`TILE_PIXELS_PER_SIDE` |8 |Tile edge in pixels.
|
||||||
|
|`TILE_BYTES_PER_ROW` |4 |Bytes per tile row (8 pixels at 4bpp).
|
||||||
|
|`TILE_BYTES` |32 |Bytes in one tile.
|
||||||
|
|`TILE_BLOCKS_PER_ROW` |40 |Tile columns on a surface.
|
||||||
|
|`TILE_BLOCKS_PER_COL` |25 |Tile rows on a surface.
|
||||||
|
|`TILE_NO_GLYPH` |0xFFFF |`jlDrawText` map entry meaning "skip this character".
|
||||||
|
|`TILE_MAP_SKIP` |0xFF |`jlTileMapPaste` entry meaning "leave this cell alone".
|
||||||
|
|===
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
typedef struct jlTileT {
|
||||||
|
uint8_t pixels[TILE_BYTES];
|
||||||
|
} jlTileT;
|
||||||
|
----
|
||||||
|
|
||||||
|
A stack-allocatable 32-byte tile snapshot, so save-under style work does not
|
||||||
|
need a whole scratch surface.
|
||||||
|
|
||||||
|
[IMPORTANT]
|
||||||
|
====
|
||||||
|
A `jlTileT` is *target-native*: plane-major on the planar ports, chunky on the
|
||||||
|
others. Treat it as opaque. In particular, do not feed a tile captured by
|
||||||
|
`jlTileSnap` to `jlTilePasteMono`, which wants a chunky silhouette -- that is a
|
||||||
|
format mismatch and draws the wrong shape. Use `jlTilePasteGlyph` for 1-bit
|
||||||
|
glyph data instead.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlTileCopy
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
|
||||||
|
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
|
||||||
|
----
|
||||||
|
|
||||||
|
Copies one 8x8 block between surfaces, opaquely.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`dst` |`jlSurfaceT *` |Destination surface.
|
||||||
|
|`dstBx`, `dstBy` |`uint8_t` |Destination block coordinates.
|
||||||
|
|`src` |`const jlSurfaceT *` |Source surface. May be the same as `dst`.
|
||||||
|
|`srcBx`, `srcBy` |`uint8_t` |Source block coordinates.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing. Out-of-range block coordinates on either side are silent
|
||||||
|
no-ops.
|
||||||
|
|
||||||
|
=== jlTileCopyMasked
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
|
||||||
|
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy,
|
||||||
|
uint8_t transparentIndex);
|
||||||
|
----
|
||||||
|
|
||||||
|
As `jlTileCopy`, but source pixels equal to `transparentIndex` are skipped and
|
||||||
|
the destination keeps its original value. This is the call for fonts and for
|
||||||
|
any tileset with see-through pixels.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`dst` |`jlSurfaceT *` |Destination surface.
|
||||||
|
|`dstBx`, `dstBy` |`uint8_t` |Destination block coordinates.
|
||||||
|
|`src` |`const jlSurfaceT *` |Source surface.
|
||||||
|
|`srcBx`, `srcBy` |`uint8_t` |Source block coordinates.
|
||||||
|
|`transparentIndex` |`uint8_t` |Source colour index treated as transparent.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlTileFill
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex);
|
||||||
|
----
|
||||||
|
|
||||||
|
Fills one block with a solid colour. Equivalent to
|
||||||
|
`jlFillRect(s, bx*8, by*8, 8, 8, colorIndex)` but skips the clipping maths,
|
||||||
|
since block coordinates are already known to be in range.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`s` |`jlSurfaceT *` |Target surface.
|
||||||
|
|`bx`, `by` |`uint8_t` |Block coordinates.
|
||||||
|
|`colorIndex` |`uint8_t` |Colour index 0..15.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlTileBankLoad
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
|
||||||
|
bool *outValid, uint16_t *outPalette);
|
||||||
|
----
|
||||||
|
|
||||||
|
Loads tiles from a baked `.tbk` file. The file's per-target byte is checked
|
||||||
|
against the build platform and mismatches are refused.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`path` |`const char *` |File path.
|
||||||
|
|`outTiles` |`jlTileT *` |Receives the tiles. Caller-owned.
|
||||||
|
|`maxTiles` |`uint16_t` |Capacity of `outTiles`.
|
||||||
|
|`outValid` |`bool *` |Optional, may be `NULL`. Set `true` per loaded slot, so
|
||||||
|
loaded tiles can be told from leftovers in a fixed-size array.
|
||||||
|
|`outPalette` |`uint16_t *` |Optional, may be `NULL`. Receives the embedded
|
||||||
|
16-entry `$0RGB` palette if the file has one.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* the number of tiles written. Returns 0 if the file is missing, the
|
||||||
|
magic or target byte is wrong, or `maxTiles` is 0.
|
||||||
|
|
||||||
|
=== jlTileBankLoadToSurface
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint16_t jlTileBankLoadToSurface(const char *path, jlSurfaceT *dst,
|
||||||
|
uint16_t *outPalette);
|
||||||
|
----
|
||||||
|
|
||||||
|
Streams a baked `.tbk` straight onto a surface's block grid: tile `i` lands at
|
||||||
|
block `(i % 40, i / 40)`, the row-major layout the bake tools author against.
|
||||||
|
|
||||||
|
Use this instead of `jlTileBankLoad` for anything large. A 1000-tile font sheet
|
||||||
|
is 32000 bytes, which no IIgs program can hold in an array or in bank-0 BSS;
|
||||||
|
this needs only a small internal scratch buffer and one pass over the file.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`path` |`const char *` |File path.
|
||||||
|
|`dst` |`jlSurfaceT *` |Destination surface.
|
||||||
|
|`outPalette` |`uint16_t *` |Optional, may be `NULL`. As `jlTileBankLoad`.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* the number of tiles pasted. Blocks beyond the file's tile count keep
|
||||||
|
their previous content.
|
||||||
|
|
||||||
|
=== jlTileSnap
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, jlTileT *out);
|
||||||
|
----
|
||||||
|
|
||||||
|
Captures one block into a caller-supplied `jlTileT`.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`src` |`const jlSurfaceT *` |Source surface.
|
||||||
|
|`bx`, `by` |`uint8_t` |Block coordinates.
|
||||||
|
|`out` |`jlTileT *` |Receives the 32-byte snapshot.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlTilePaste
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in);
|
||||||
|
----
|
||||||
|
|
||||||
|
Pastes a `jlTileT` back onto a surface, always opaquely. Use
|
||||||
|
`jlTileCopyMasked` if you need transparency.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`dst` |`jlSurfaceT *` |Destination surface.
|
||||||
|
|`bx`, `by` |`uint8_t` |Block coordinates.
|
||||||
|
|`in` |`const jlTileT *` |Tile to paste.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlTilePasteMono
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by,
|
||||||
|
const jlTileT *in, uint8_t fgColor, uint8_t bgColor);
|
||||||
|
----
|
||||||
|
|
||||||
|
Pastes a tile as a silhouette: any non-zero source pixel draws `fgColor`, zero
|
||||||
|
pixels draw `bgColor`. Useful when the bitmap is a fixed glyph but the colour
|
||||||
|
comes from a separate map, as in C64-style colour RAM. Opaque.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`dst` |`jlSurfaceT *` |Destination surface.
|
||||||
|
|`bx`, `by` |`uint8_t` |Block coordinates.
|
||||||
|
|`in` |`const jlTileT *` |Source tile, read as a silhouette.
|
||||||
|
|`fgColor` |`uint8_t` |Colour for non-zero pixels. Masked to 4 bits.
|
||||||
|
|`bgColor` |`uint8_t` |Colour for zero pixels. Masked to 4 bits.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
This call wants a *chunky* silhouette, but a `jlTileT` from `jlTileSnap` is
|
||||||
|
target-native. Feeding one to this function on a planar port is a format
|
||||||
|
mismatch. For glyphs, use `jlTilePasteGlyph`, which takes unambiguous 1-bit
|
||||||
|
input.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== jlTilePasteGlyph
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by,
|
||||||
|
const uint8_t *bits, uint8_t fgColor, uint8_t bgColor);
|
||||||
|
----
|
||||||
|
|
||||||
|
Pastes an 8x8 one-bit-per-pixel glyph: `bits[row]` is that row, most
|
||||||
|
significant bit leftmost. A set bit draws `fgColor`, a clear bit `bgColor`.
|
||||||
|
Opaque.
|
||||||
|
|
||||||
|
This is the shape a character generator already has -- a font ROM, a C64
|
||||||
|
charset, anything 1bpp -- so nothing needs expanding into a `jlTileT` first.
|
||||||
|
*Prefer this over `jlTilePasteMono` for glyphs.*
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`dst` |`jlSurfaceT *` |Destination surface.
|
||||||
|
|`bx`, `by` |`uint8_t` |Block coordinates.
|
||||||
|
|`bits` |`const uint8_t *` |8 bytes, one per glyph row, MSB leftmost.
|
||||||
|
|`fgColor` |`uint8_t` |Set-bit colour. Masked to 4 bits.
|
||||||
|
|`bgColor` |`uint8_t` |Clear-bit colour. Masked to 4 bits.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlDrawText
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by,
|
||||||
|
const jlSurfaceT *fontSurface, const uint16_t *asciiMap,
|
||||||
|
const char *str);
|
||||||
|
----
|
||||||
|
|
||||||
|
Draws a NUL-terminated ASCII string using glyphs from a font surface. Glyph
|
||||||
|
colour 0 is transparent, so the background shows through. The cursor wraps at
|
||||||
|
the right edge and truncates at the bottom.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`dst` |`jlSurfaceT *` |Destination surface.
|
||||||
|
|`bx`, `by` |`uint8_t` |Starting block coordinates.
|
||||||
|
|`fontSurface` |`const jlSurfaceT *` |Surface holding the glyphs.
|
||||||
|
|`asciiMap` |`const uint16_t *` |256 entries mapping ASCII code to glyph
|
||||||
|
position: low byte is source block X, high byte is source block Y. An entry of
|
||||||
|
`TILE_NO_GLYPH` skips that character, advancing the cursor without drawing.
|
||||||
|
|`str` |`const char *` |NUL-terminated string.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing.
|
||||||
|
|
||||||
|
=== jlTileMapPaste
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by,
|
||||||
|
uint8_t wTiles, uint8_t hTiles,
|
||||||
|
const jlTileT *tiles, const uint8_t *map);
|
||||||
|
----
|
||||||
|
|
||||||
|
Pastes a whole rectangle of tiles in one call, so validation, dispatch and the
|
||||||
|
dirty-rectangle union are paid once for the rectangle instead of once per tile.
|
||||||
|
For anything larger than a few tiles this is substantially faster than looping
|
||||||
|
over `jlTilePaste`.
|
||||||
|
|
||||||
|
[cols="1,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Parameter |Type |Description
|
||||||
|
|`dst` |`jlSurfaceT *` |Destination surface.
|
||||||
|
|`bx`, `by` |`uint8_t` |Top-left block coordinates.
|
||||||
|
|`wTiles`, `hTiles` |`uint8_t` |Rectangle size in tiles. Zero is a no-op.
|
||||||
|
|`tiles` |`const jlTileT *` |The tileset.
|
||||||
|
|`map` |`const uint8_t *` |`wTiles * hTiles` tileset indices, row-major, so
|
||||||
|
`map[row * wTiles + col]`. `TILE_MAP_SKIP` leaves that cell untouched.
|
||||||
|
|===
|
||||||
|
|
||||||
|
*Returns:* nothing. A rectangle whose edge crosses the surface boundary is
|
||||||
|
rejected as a no-op.
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
Any map index other than `TILE_MAP_SKIP` must be a valid tileset index. The
|
||||||
|
batch does not know the tileset length and does *not* range-check it -- an
|
||||||
|
out-of-range index reads past the end of your array.
|
||||||
|
====
|
||||||
197
docs/Appendix.adoc
Normal file
197
docs/Appendix.adoc
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
[appendix]
|
||||||
|
== Constants Reference
|
||||||
|
|
||||||
|
=== Surface
|
||||||
|
|
||||||
|
[cols="3,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Name |Value |Meaning
|
||||||
|
|`SURFACE_WIDTH` |320 |Surface width in pixels.
|
||||||
|
|`SURFACE_HEIGHT` |200 |Surface height in pixels.
|
||||||
|
|`SURFACE_BYTES_PER_ROW` |160 |Bytes per row, chunky 4bpp.
|
||||||
|
|`SURFACE_PIXELS_SIZE` |32000 |Total pixel bytes.
|
||||||
|
|`SURFACE_PALETTE_COUNT` |16 |Palettes per surface.
|
||||||
|
|`SURFACE_COLORS_PER_PALETTE` |16 |Colours per palette.
|
||||||
|
|`SURFACE_PALETTE_ENTRIES` |256 |Total palette entries.
|
||||||
|
|===
|
||||||
|
|
||||||
|
=== Tiles
|
||||||
|
|
||||||
|
[cols="3,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Name |Value |Meaning
|
||||||
|
|`TILE_PIXELS_PER_SIDE` |8 |Tile edge in pixels.
|
||||||
|
|`TILE_BYTES_PER_ROW` |4 |Bytes per tile row.
|
||||||
|
|`TILE_BYTES` |32 |Bytes per tile.
|
||||||
|
|`TILE_BLOCKS_PER_ROW` |40 |Tile columns per surface.
|
||||||
|
|`TILE_BLOCKS_PER_COL` |25 |Tile rows per surface.
|
||||||
|
|`TILE_NO_GLYPH` |0xFFFF |`jlDrawText`: skip this character.
|
||||||
|
|`TILE_MAP_SKIP` |0xFF |`jlTileMapPaste`: leave this cell untouched.
|
||||||
|
|===
|
||||||
|
|
||||||
|
=== Input
|
||||||
|
|
||||||
|
[cols="3,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Name |Value |Meaning
|
||||||
|
|`JOYSTICK_AXIS_MAX` |127 |Maximum axis value.
|
||||||
|
|`JOYSTICK_AXIS_MIN` |-127 |Minimum axis value.
|
||||||
|
|`JL_CHAR_QUEUE_SIZE` |32 |Queue capacity; 31 characters can be pending.
|
||||||
|
|`JL_CHAR_BACKSPACE` |0x08 |Backspace, deletes before the caret.
|
||||||
|
|`JL_CHAR_TAB` |0x09 |Tab.
|
||||||
|
|`JL_CHAR_RETURN` |0x0D |Return.
|
||||||
|
|`JL_CHAR_ESCAPE` |0x1B |Escape.
|
||||||
|
|`JL_CHAR_DELETE` |0x7F |Forward delete, removes the character under the caret.
|
||||||
|
|===
|
||||||
|
|
||||||
|
=== Audio
|
||||||
|
|
||||||
|
[cols="3,1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Name |Value |Meaning
|
||||||
|
|`JOEY_AUDIO_SFX_SLOTS` |5 |Sound effect slots, 0..4.
|
||||||
|
|`JOEY_AUDIO_VOICES` |3 |Tone voices, 0..2.
|
||||||
|
|`JL_SFX_RAW_SIGNED8` |0 |Raw stream format: signed 8-bit PCM.
|
||||||
|
|`JL_SFX_RAW_UNSIGNED8_NOZERO` |1 |Raw stream format: unsigned biased 0x80.
|
||||||
|
|===
|
||||||
|
|
||||||
|
[appendix]
|
||||||
|
== Function Index by Header
|
||||||
|
|
||||||
|
[cols="2,5", options="header"]
|
||||||
|
|===
|
||||||
|
|Header |Functions
|
||||||
|
|
||||||
|
|`core.h`
|
||||||
|
|`jlInit`, `jlShutdown`, `jlLastError`, `jlPlatformName`, `jlVersionString`,
|
||||||
|
`jlAlloc`, `jlFree`, `jlMemCopy`, `jlWaitVBL`, `jlFrameCount`, `jlFrameHz`,
|
||||||
|
`jlMillisElapsed`, `jlRandom`, `jlRandomRange`, `jlRandomSeed`
|
||||||
|
|
||||||
|
|`surface.h`
|
||||||
|
|`jlSurfaceCreate`, `jlSurfaceDestroy`, `jlStageGet`, `jlSurfaceCopy`,
|
||||||
|
`jlSurfaceCopyRect`, `jlSurfaceSaveFile`, `jlSurfaceLoadFile`,
|
||||||
|
`jlSurfaceHash`
|
||||||
|
|
||||||
|
|`present.h`
|
||||||
|
|`jlStagePresent`
|
||||||
|
|
||||||
|
|`palette.h`
|
||||||
|
|`jlPaletteSet`, `jlPaletteGet`, `jlScbSet`, `jlScbSetRange`, `jlScbGet`
|
||||||
|
|
||||||
|
|`draw.h`
|
||||||
|
|`jlSurfaceClear`, `jlDrawPixel`, `jlSamplePixel`, `jlDrawLine`, `jlDrawRect`,
|
||||||
|
`jlFillRect`, `jlDrawCircle`, `jlFillCircle`, `jlFloodFill`,
|
||||||
|
`jlFloodFillBounded`
|
||||||
|
|
||||||
|
|`tile.h`
|
||||||
|
|`jlTileCopy`, `jlTileCopyMasked`, `jlTileFill`, `jlTileBankLoad`,
|
||||||
|
`jlTileBankLoadToSurface`, `jlTileSnap`, `jlTilePaste`, `jlTilePasteMono`,
|
||||||
|
`jlTilePasteGlyph`, `jlDrawText`, `jlTileMapPaste`
|
||||||
|
|
||||||
|
|`sprite.h`
|
||||||
|
|`jlSpriteBankLoad`, `jlSpriteBankLoadPrecompiled`,
|
||||||
|
`jlSpriteBankSavePrecompiled`, `jlSpriteCreate`, `jlSpriteDestroy`,
|
||||||
|
`jlSpriteCompile`, `jlSpriteCompiledSize`, `jlSpritePrewarm`, `jlSpriteDraw`,
|
||||||
|
`jlSpriteDrawTrusted`, `jlSpriteSaveUnder`, `jlSpriteSaveUnderTrusted`,
|
||||||
|
`jlSpriteRestoreUnder`, `jlSpriteRestoreUnderTrusted`, `jlSpriteSaveAndDraw`,
|
||||||
|
`jlSpriteSaveAndDrawTrusted`, `jlSpriteCreateFromSurface`, `jlSpriteCompact`,
|
||||||
|
`jlSpriteCodegenBytesUsed`, `jlSpriteCodegenBytesTotal`
|
||||||
|
|
||||||
|
|`input.h`
|
||||||
|
|`jlInputPoll`, `jlInputGetChar`, `jlWaitForAnyKey`, `jlKeyDown`,
|
||||||
|
`jlKeyPressed`, `jlKeyReleased`, `jlMousePresent`, `jlMouseX`, `jlMouseY`,
|
||||||
|
`jlMouseDown`, `jlMousePressed`, `jlMouseReleased`, `jlJoystickConnected`,
|
||||||
|
`jlJoystickX`, `jlJoystickY`, `jlJoyDown`, `jlJoyPressed`, `jlJoyReleased`,
|
||||||
|
`jlJoystickReset`
|
||||||
|
|
||||||
|
|`audio.h`
|
||||||
|
|`jlAudioInit`, `jlAudioShutdown`, `jlAudioPlayMod`, `jlAudioStopMod`,
|
||||||
|
`jlAudioIsPlayingMod`, `jlAudioPlaySfx`, `jlAudioPlaySfxStream`,
|
||||||
|
`jlAudioSfxRawFormat`, `jlAudioPlaySfxStreamRaw`, `jlAudioSetPan`,
|
||||||
|
`jlAudioStopSfx`, `jlAudioTone`, `jlAudioTickRegister`,
|
||||||
|
`jlAudioCriticalEnter`, `jlAudioCriticalExit`, `jlAudioVoice`, `jlAudioNoise`,
|
||||||
|
`jlMusicPlay`, `jlMusicStop`, `jlMusicIsPlaying`, `jlMusicPause`,
|
||||||
|
`jlMusicResume`, `jlMusicSetAtten`, `jlAudioFrameTick`
|
||||||
|
|
||||||
|
|`file.h`
|
||||||
|
|`jlDataOpen`, `jlSaveOpen`, `jlSaveWrite`, `jlSaveRead`, `jlSaveExists`,
|
||||||
|
`jlSaveDelete`, `jlDiskFree`
|
||||||
|
|
||||||
|
|`serial.h` (opt-in)
|
||||||
|
|`jlSerialOpen`, `jlSerialClose`, `jlSerialPoll`, `jlSerialAvailable`,
|
||||||
|
`jlSerialRead`, `jlSerialWrite`, `jlSerialWriteByte`, `jlSerialReadByte`,
|
||||||
|
`jlSerialFlush`, `jlSerialSetRts`
|
||||||
|
|
||||||
|
|`debug.h`
|
||||||
|
|`jlLog`, `jlLogF`, `jlLogFlush`, `jlLogReset`
|
||||||
|
|===
|
||||||
|
|
||||||
|
[appendix]
|
||||||
|
== Troubleshooting
|
||||||
|
|
||||||
|
[cols="2,5", options="header"]
|
||||||
|
|===
|
||||||
|
|Symptom |Likely cause
|
||||||
|
|
||||||
|
|Build produces stale or missing binaries, `Error 127 (ignored)`
|
||||||
|
|`toolchains/env.sh` was not sourced in this shell.
|
||||||
|
|
||||||
|
|Sprites draw but the game crawls
|
||||||
|
|The sprites were never compiled. Call `jlSpritePrewarm` during loading. Check
|
||||||
|
`jlSpriteCompiledSize` returns non-zero.
|
||||||
|
|
||||||
|
|Sprites compile on some machines and not others
|
||||||
|
|`codegenBytes` is too small for the 68000 or x86 emitters, which need four to
|
||||||
|
eight times the IIgs figure. Measure with `jlSpriteCompiledSize`.
|
||||||
|
|
||||||
|
|Works everywhere except the IIgs, failing at startup
|
||||||
|
|A `malloc` too large for the bank-0 C heap. Use `jlAlloc`.
|
||||||
|
|
||||||
|
|Crashes on IIgs deep in a call chain
|
||||||
|
|A large function-local array overflowing the soft stack. Make it `static`.
|
||||||
|
|
||||||
|
|Nothing appears on screen
|
||||||
|
|`jlStagePresent` was never called, or drawing went to a surface that is not
|
||||||
|
the stage.
|
||||||
|
|
||||||
|
|Part of the screen never updates
|
||||||
|
|Pixels were written by something other than the library primitives, so the
|
||||||
|
dirty tracking did not see them.
|
||||||
|
|
||||||
|
|Runs at different speeds on different machines
|
||||||
|
|Per-frame steps are hard-coded. Scale by `jlFrameHz()` or use
|
||||||
|
`jlMillisElapsed()`.
|
||||||
|
|
||||||
|
|Edge predicates never fire
|
||||||
|
|`jlInputPoll` is being called more than once per frame, or not at all.
|
||||||
|
|
||||||
|
|Typed text misses characters
|
||||||
|
|The character queue holds 31 entries and drops the rest. Pop it every frame.
|
||||||
|
|
||||||
|
|Music plays but `jlAudioVoice` does nothing
|
||||||
|
|On ST and Amiga the module player owns those channels. Use one layer per
|
||||||
|
scene.
|
||||||
|
|
||||||
|
|A note sounds for only a fraction of a second on the IIgs
|
||||||
|
|`jlAudioVoice` is a roughly 80 ms one-shot there. Re-assert it about once a
|
||||||
|
frame.
|
||||||
|
|
||||||
|
|A streaming sound effect stops after its first chunk
|
||||||
|
|`jlAudioFrameTick` is not being called every frame. IIgs and Amiga fire the
|
||||||
|
next chunk from it.
|
||||||
|
|
||||||
|
|Sprite trails or corruption around moving objects
|
||||||
|
|Save-under and clean-buffer erasing are being mixed in one region, or a
|
||||||
|
`jlSurfaceCopyRect` invalidated a live backup by snapping outward to the
|
||||||
|
16-pixel grid.
|
||||||
|
|
||||||
|
|Save and restore are unexpectedly slow on Amiga or ST
|
||||||
|
|The backup buffer is at an odd address. Declare it `uint16_t`.
|
||||||
|
|
||||||
|
|A tile bank refuses to load
|
||||||
|
|`.tbk` files are per-target. Bake one for each platform.
|
||||||
|
|
||||||
|
|Your program is missing from the IIgs disk image
|
||||||
|
|The 800 KB floppy is full. Build with `JOEY_DISK_EXAMPLES` naming it, and
|
||||||
|
check the packager's `added:` line.
|
||||||
|
|===
|
||||||
336
docs/Assets.adoc
Normal file
336
docs/Assets.adoc
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
== Creating Assets
|
||||||
|
|
||||||
|
JoeyLib does not load PNGs, WAVs or MODs at run time. Every asset is converted
|
||||||
|
on the build host into a compact binary the target can use directly, which is
|
||||||
|
what keeps loading fast and the runtime small. This chapter covers each asset
|
||||||
|
type: what you author, which tool bakes it, and which API call loads it.
|
||||||
|
|
||||||
|
=== The Pipeline at a Glance
|
||||||
|
|
||||||
|
[cols="2,2,2,3", options="header"]
|
||||||
|
|===
|
||||||
|
|You author |Tool |Baked file |Loaded by
|
||||||
|
|
||||||
|
|Indexed PNG tile sheet
|
||||||
|
|`assetbake.py --type tile`
|
||||||
|
|`.tbk`
|
||||||
|
|`jlTileBankLoad`, `jlTileBankLoadToSurface`
|
||||||
|
|
||||||
|
|Indexed PNG sprite sheet
|
||||||
|
|`assetbake.py --type sprite`
|
||||||
|
|`.spr`
|
||||||
|
|`jlSpriteBankLoad`
|
||||||
|
|
||||||
|
|A `.spr`
|
||||||
|
|`spritebake`
|
||||||
|
|`.spc`
|
||||||
|
|`jlSpriteBankLoadPrecompiled`
|
||||||
|
|
||||||
|
|Text note notation
|
||||||
|
|`songbake.py`
|
||||||
|
|`.jym` or a C header
|
||||||
|
|`jlMusicPlay`
|
||||||
|
|
||||||
|
|Protracker `.MOD`
|
||||||
|
|`joeymod`
|
||||||
|
|`.mod`, `.amod`, `.ntp`
|
||||||
|
|`jlAudioPlayMod`
|
||||||
|
|
||||||
|
|Raw signed 8-bit PCM
|
||||||
|
|(none -- copied)
|
||||||
|
|`.sfx`
|
||||||
|
|`jlAudioPlaySfx`
|
||||||
|
|===
|
||||||
|
|
||||||
|
Baked assets are staged into `DATA/` beside the executable, and the program
|
||||||
|
opens them with bare names.
|
||||||
|
|
||||||
|
=== Graphics: Tiles and Sprites
|
||||||
|
|
||||||
|
==== Authoring the PNG
|
||||||
|
|
||||||
|
Both tile and sprite sheets start as an *indexed* PNG with strict rules:
|
||||||
|
|
||||||
|
* Mode `P` (indexed / palette), *not* RGB or RGBA.
|
||||||
|
* At most *16 colours*.
|
||||||
|
* Width and height both *multiples of 8*.
|
||||||
|
* Palette entry 0 is the transparent / background colour. Remember the library
|
||||||
|
forces it to black on screen.
|
||||||
|
|
||||||
|
Any decent pixel editor can produce this. In GIMP: *Image > Mode > Indexed*,
|
||||||
|
"Generate optimum palette" with maximum 16 colours, and no dithering --
|
||||||
|
dithering invents colours and will push you over the limit.
|
||||||
|
|
||||||
|
Lay the artwork out on a grid. A tile sheet is read as 8x8 cells row by row; a
|
||||||
|
sprite sheet is read as cells of whatever size you declare with `--cell`, also
|
||||||
|
row by row.
|
||||||
|
|
||||||
|
==== Baking Tiles
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ python3 tools/assetbake/assetbake.py --type tile --target atarist \
|
||||||
|
font.png build/atarist/bin/DATA/font.tbk
|
||||||
|
----
|
||||||
|
|
||||||
|
Tile banks are *per-target*, because the 8x8 block layout is the platform's
|
||||||
|
native pixel format -- plane-major on Amiga, row-major-with-planes-per-row on
|
||||||
|
ST, chunky 4bpp on DOS and IIgs. You must bake one `.tbk` per platform, and the
|
||||||
|
loader refuses a file baked for a different target rather than drawing garbage.
|
||||||
|
|
||||||
|
`--target` is required for tiles, and accepts `amiga`, `atarist`, `dos` or
|
||||||
|
`iigs`.
|
||||||
|
|
||||||
|
==== Baking Sprites
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ python3 tools/assetbake/assetbake.py --type sprite --cell 2x3 \
|
||||||
|
player.png build/atarist/bin/DATA/player.spr
|
||||||
|
----
|
||||||
|
|
||||||
|
`--cell` gives the cel size *in tiles, not pixels*, so `2x3` means 16x24
|
||||||
|
pixels. The sheet is cut into cels row by row.
|
||||||
|
|
||||||
|
Sprite `.spr` banks are *cross-target*: the same file works on every platform,
|
||||||
|
because the sprite walker reads chunky 4bpp and converts at draw time. Bake
|
||||||
|
once, ship everywhere.
|
||||||
|
|
||||||
|
Both formats can embed the 16-entry `$0RGB` palette, and both loaders will hand
|
||||||
|
it back through their `outPalette` parameter so artwork carries its own colours
|
||||||
|
rather than depending on whatever the stage happens to have set.
|
||||||
|
|
||||||
|
==== Pre-Compiling Sprites
|
||||||
|
|
||||||
|
A `.spr` still needs compiling into native blitter code at run time. For a
|
||||||
|
large bank that is a visible startup pause. `spritebake` does it offline:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ build/tools/spritebake-atarist player.spr player.spc
|
||||||
|
----
|
||||||
|
|
||||||
|
The result is a *per-platform* `.spc` holding pre-emitted native routines.
|
||||||
|
`jlSpriteBankLoadPrecompiled` copies them straight into the codegen arena and
|
||||||
|
skips compilation entirely. A cel too big for the arena silently loads
|
||||||
|
uncompiled rather than failing, so this is a safe drop-in replacement.
|
||||||
|
|
||||||
|
The `spritebake` binaries are built per target by `make/tools.mk`, because each
|
||||||
|
one links the emitter for its own CPU while running on the build host.
|
||||||
|
|
||||||
|
There is a third option if you would rather ship only the small cross-platform
|
||||||
|
`.spr`: compile once on first run and cache the result with
|
||||||
|
`jlSpriteBankSavePrecompiled`, then load the cache on later runs. That trades a
|
||||||
|
one-time startup cost for a much smaller distribution.
|
||||||
|
|
||||||
|
=== Music: the Portable Chip Tracker
|
||||||
|
|
||||||
|
This is the recommended music path. One authored song plays on all five ports
|
||||||
|
at register-write cost, with no CPU mixing -- which matters enormously on the
|
||||||
|
ST, where `.MOD` playback is only marginally viable.
|
||||||
|
|
||||||
|
Songs are plain text, one row per line:
|
||||||
|
|
||||||
|
[source]
|
||||||
|
----
|
||||||
|
; Four bars of Am - F - C - G.
|
||||||
|
tickms 110
|
||||||
|
loop
|
||||||
|
; voice0 voice1 voice2 noise
|
||||||
|
A-2:2 A-4:5 --- N16:5
|
||||||
|
--- C-5:5 E-5:3 off
|
||||||
|
A-3:3 E-4:5 off N2:7
|
||||||
|
--- A-4:5 C-5:3 off
|
||||||
|
----
|
||||||
|
|
||||||
|
The columns are three tone voices plus an optional fourth noise column.
|
||||||
|
|
||||||
|
[cols="1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Cell |Meaning
|
||||||
|
|
||||||
|
|`C-4:3`
|
||||||
|
|Note on: note name, octave, then `:` and an attenuation 0..14 where 0 is
|
||||||
|
loudest. Sharps use `#`, as in `C#4:3`. Range `C-0` to `B-7`. The `:atten`
|
||||||
|
part may be omitted and defaults to 0.
|
||||||
|
|
||||||
|
|`@440:3`
|
||||||
|
|Note on at an exact frequency in Hz, for chip-accurate transcription where
|
||||||
|
equal-tempered note names would round the pitch.
|
||||||
|
|
||||||
|
|`---`
|
||||||
|
|Hold: this voice is unchanged for this row.
|
||||||
|
|
||||||
|
|`off`
|
||||||
|
|Silence this voice.
|
||||||
|
|
||||||
|
|`N4:3`
|
||||||
|
|Noise on (fourth column only): pitch 0..31, where 0 is the brightest hiss
|
||||||
|
and 31 the lowest rumble, plus attenuation.
|
||||||
|
|===
|
||||||
|
|
||||||
|
Directives: `tickms N` sets milliseconds per row and is required before the
|
||||||
|
first row (later occurrences change tempo mid-song); `loop` marks the next row
|
||||||
|
as the loop-back target; `repeat N` ... `endrepeat` repeats a block, and may be
|
||||||
|
nested.
|
||||||
|
|
||||||
|
Bake it either as a binary blob or directly as a C array:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ python3 tools/songbake/songbake.py song.song song.jym
|
||||||
|
$ python3 tools/songbake/songbake.py -c gDemoSong song.song song.h
|
||||||
|
----
|
||||||
|
|
||||||
|
The C header form is usually what you want for a small song: it compiles into
|
||||||
|
the program and needs no file I/O at all.
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
On the Atari ST the noise generator shares channel C's volume register with
|
||||||
|
tone voice 2. Author noise hits and voice-2 notes so they do not sound at the
|
||||||
|
same time; `songbake` warns when they overlap.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== Music: Protracker Modules
|
||||||
|
|
||||||
|
For richer music, author a four-channel `.MOD` in any tracker (ProTracker,
|
||||||
|
MilkyTracker, OpenMPT). Then convert per target:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ tools/joeymod/joeymod song.mod song.mod # DOS and ST: validated copy
|
||||||
|
$ tools/joeymod/joeymod song.mod song.amod # Amiga
|
||||||
|
$ tools/joeymod/joeymod song.mod song.ntp # Apple IIgs
|
||||||
|
----
|
||||||
|
|
||||||
|
The output extension selects the conversion:
|
||||||
|
|
||||||
|
* *`.mod`* -- a passthrough copy with the signature validated. DOS and ST
|
||||||
|
detect song end inside libxmp-lite, so the file is used as-is.
|
||||||
|
* *`.amod`* -- the same, plus an `E8FF` stop marker injected at song end. The
|
||||||
|
Amiga replayer, PTPlayer, has no native song-end signal, so without this
|
||||||
|
marker `jlAudioPlayMod(..., loop=false)` loops anyway. Other replayers ignore
|
||||||
|
`E8FF`, so an `.amod` is harmless if it reaches the wrong port.
|
||||||
|
* *`.ntp`* -- NinjaTrackerPlus format for the IIgs, produced by the staged
|
||||||
|
converter under `toolchains/iigs/ntp/`.
|
||||||
|
|
||||||
|
Keep modules modest. A four-channel mix is real CPU work on DOS and the ST, and
|
||||||
|
the sample data has to fit in memory alongside your game.
|
||||||
|
|
||||||
|
=== Sound Effects
|
||||||
|
|
||||||
|
Sound effects are *raw signed 8-bit PCM* with no header at all -- just sample
|
||||||
|
bytes. Convert from a WAV with any audio editor, or on the command line:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ sox input.wav -r 11025 -c 1 -b 8 -e signed-integer output.sfx
|
||||||
|
----
|
||||||
|
|
||||||
|
Then play it, telling the library what rate it was recorded at:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
jlAudioPlaySfx(4, sfxBytes, sfxLen, 11025);
|
||||||
|
----
|
||||||
|
|
||||||
|
Choose a rate that suits the machines: 8 to 11 kHz is plenty for game effects
|
||||||
|
and keeps the data small, which matters when the whole thing must fit on a
|
||||||
|
floppy.
|
||||||
|
|
||||||
|
Two things to know. Prefer *slot 4* if music is playing, because the IIgs maps
|
||||||
|
tracker tone voices onto slots 0 to 2 and the noise voice onto slot 3. And the
|
||||||
|
IIgs plays a sample value of -128 as -127, because its DOC reads the
|
||||||
|
corresponding byte as a wave-end marker -- one level out of 256, inaudible in
|
||||||
|
practice, but it explains a tiny waveform difference if you ever measure one.
|
||||||
|
|
||||||
|
For long sounds such as speech, use `jlAudioPlaySfxStream` with a fill callback
|
||||||
|
instead of holding the whole sample in memory.
|
||||||
|
|
||||||
|
=== Screens and Backgrounds
|
||||||
|
|
||||||
|
A whole 320x200 screen can be saved and reloaded in target-native form:
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
jlSurfaceSaveFile(surface, "title.sur");
|
||||||
|
jlSurfaceLoadFile(surface, "title.sur");
|
||||||
|
----
|
||||||
|
|
||||||
|
The file is 32000 bytes of pixels, 200 SCB bytes and 512 bytes of palette. It
|
||||||
|
is *not* portable between platforms -- no format conversion happens, so the
|
||||||
|
machine that wrote it must be the machine that reads it. It is best used as a
|
||||||
|
runtime cache (a generated title screen, say), not as a shipped asset. For
|
||||||
|
shipped artwork, bake a tile bank and compose the screen with
|
||||||
|
`jlTileBankLoadToSurface`.
|
||||||
|
|
||||||
|
=== Staging Assets into the Build
|
||||||
|
|
||||||
|
Baked files must end up in `DATA/` next to the executable. Each port's makefile
|
||||||
|
does this with an explicit rule. The simple copy case, from `make/atarist.mk`:
|
||||||
|
|
||||||
|
[source,makefile]
|
||||||
|
----
|
||||||
|
DATA_DIR := $(BINDIR)/DATA
|
||||||
|
DATA_FILES := $(DATA_DIR)/test.mod $(DATA_DIR)/test.sfx
|
||||||
|
|
||||||
|
$(DATA_DIR)/test.mod: $(REPO_DIR)/assets/test.mod
|
||||||
|
@mkdir -p $(DATA_DIR)
|
||||||
|
cp $< $@
|
||||||
|
----
|
||||||
|
|
||||||
|
And a bake rule looks like:
|
||||||
|
|
||||||
|
[source,makefile]
|
||||||
|
----
|
||||||
|
$(DATA_DIR)/font.tbk: $(MY_ASSETS)/font.png
|
||||||
|
@mkdir -p $(DATA_DIR)
|
||||||
|
python3 $(ASSETBAKE_BIN) --type tile --target atarist $< $@
|
||||||
|
----
|
||||||
|
|
||||||
|
Add the baked files to the port's `all` target so they are built alongside the
|
||||||
|
binaries. Remember that tile banks and `.spc` files need one rule *per
|
||||||
|
platform*, while `.spr`, `.jym` and `.sfx` files are shared.
|
||||||
|
|
||||||
|
=== Loading Assets at Run Time
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
// Tiles into a caller-owned array.
|
||||||
|
jlTileT tiles[256];
|
||||||
|
bool valid[256];
|
||||||
|
uint16_t palette[SURFACE_COLORS_PER_PALETTE];
|
||||||
|
uint16_t n = jlTileBankLoad("font.tbk", tiles, 256, valid, palette);
|
||||||
|
|
||||||
|
// Or stream a big bank straight onto a surface, with no caller buffer.
|
||||||
|
uint16_t m = jlTileBankLoadToSurface("font.tbk", fontSurface, palette);
|
||||||
|
|
||||||
|
// Sprites, pre-compiled.
|
||||||
|
jlSpriteT *cels[32];
|
||||||
|
uint16_t count = jlSpriteBankLoadPrecompiled("player.spc", cels, 32, palette);
|
||||||
|
|
||||||
|
// Apply the palette the artwork shipped with.
|
||||||
|
jlPaletteSet(stage, 0, palette);
|
||||||
|
----
|
||||||
|
|
||||||
|
Prefer `jlTileBankLoadToSurface` for anything large. A 1000-tile font sheet is
|
||||||
|
32000 bytes, which no IIgs program can hold in a local array or in bank-0 BSS,
|
||||||
|
and the streaming loader needs only a small internal scratch buffer.
|
||||||
|
|
||||||
|
=== The Logo
|
||||||
|
|
||||||
|
The JoeyLib logo lives in `assets/` as both the original 320x200 PNG and a
|
||||||
|
traced SVG. If the source art ever changes, regenerate the vector with:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ python3 -m venv /tmp/tracevenv
|
||||||
|
$ /tmp/tracevenv/bin/pip install potracer pillow numpy
|
||||||
|
$ /tmp/tracevenv/bin/python tools/traceLogo.py \
|
||||||
|
assets/joeylib_logo_-_320x200.png assets/joeylibLogo.svg --colors 5
|
||||||
|
----
|
||||||
|
|
||||||
|
Five colour clusters measured as the closest match to the original artwork;
|
||||||
|
more clusters start carving the antialiased edges into separate layers, which
|
||||||
|
renders as noise rather than detail.
|
||||||
190
docs/BuildIntegration.adoc
Normal file
190
docs/BuildIntegration.adoc
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
== Adding Your Program to the Build
|
||||||
|
|
||||||
|
JoeyLib does not install as a system library. Programs are built from inside
|
||||||
|
the repository by the same makefiles that build the library, because each
|
||||||
|
target needs its own cross compiler, link script, packaging step and disk
|
||||||
|
image. Adding a program means adding it to each port's makefile.
|
||||||
|
|
||||||
|
=== The Shape of a Program Directory
|
||||||
|
|
||||||
|
Put your sources under `examples/`, one directory per program:
|
||||||
|
|
||||||
|
----
|
||||||
|
examples/mygame/
|
||||||
|
mygame.c
|
||||||
|
mygame.h
|
||||||
|
assets/
|
||||||
|
tiles.png
|
||||||
|
sprites.png
|
||||||
|
music.song
|
||||||
|
----
|
||||||
|
|
||||||
|
Keep the authoring assets next to the code and let the makefiles bake them into
|
||||||
|
`DATA/`, as described in <<_creating_assets>>.
|
||||||
|
|
||||||
|
=== Adding It to One Port
|
||||||
|
|
||||||
|
Each port's makefile declares programs explicitly. The pattern in
|
||||||
|
`make/atarist.mk`, for a single-source program:
|
||||||
|
|
||||||
|
[source,makefile]
|
||||||
|
----
|
||||||
|
MYGAME_SRC := $(EXAMPLES)/mygame/mygame.c
|
||||||
|
MYGAME_BIN := $(BINDIR)/MYGAME.PRG
|
||||||
|
----
|
||||||
|
|
||||||
|
Add the binary to the port's `all` target so it gets built:
|
||||||
|
|
||||||
|
[source,makefile]
|
||||||
|
----
|
||||||
|
all atarist: $(LIB) ... $(MYGAME_BIN) $(DATA_FILES)
|
||||||
|
----
|
||||||
|
|
||||||
|
And give it a build rule, copying the shape of an existing example in that
|
||||||
|
file. Do not invent your own compiler invocation: the flags, include paths and
|
||||||
|
link order in those rules encode real constraints, several of them the result
|
||||||
|
of long debugging.
|
||||||
|
|
||||||
|
For a multi-file program, list the sources:
|
||||||
|
|
||||||
|
[source,makefile]
|
||||||
|
----
|
||||||
|
MYGAME_SRCS := $(EXAMPLES)/mygame/mygame.c \
|
||||||
|
$(EXAMPLES)/mygame/world.c \
|
||||||
|
$(EXAMPLES)/mygame/render.c
|
||||||
|
----
|
||||||
|
|
||||||
|
Repeat for each port you want to support: `make/iigs.mk`, `make/amiga.mk`,
|
||||||
|
`make/atarist.mk`, `make/dos.mk` and `make/x68000.mk`.
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
Yes, that is five makefile edits for one program. The ports genuinely differ:
|
||||||
|
the IIgs needs OMF segmentation and a disk image, the Amiga a startup-sequence
|
||||||
|
entry, DOS a DPMI stub, and the X68000 a Human68k floppy layout. There is no
|
||||||
|
single rule that covers all five.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== Naming
|
||||||
|
|
||||||
|
File naming conventions differ per platform and the makefiles reflect that:
|
||||||
|
|
||||||
|
[cols="2,2,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Platform |Convention |Example
|
||||||
|
|
||||||
|
|Apple IIgs |Uppercase, 8 characters |`MYGAME`
|
||||||
|
|Atari ST |Uppercase 8.3, `.PRG` |`MYGAME.PRG`
|
||||||
|
|MS-DOS |Uppercase 8.3, `.EXE` |`MYGAME.EXE`
|
||||||
|
|Amiga |Mixed case, no extension |`MyGame`
|
||||||
|
|Sharp X68000 |Uppercase 8.3, `.X` |`MYGAME.X`
|
||||||
|
|===
|
||||||
|
|
||||||
|
Keep the base name to 8 characters and letters and digits only, so the same
|
||||||
|
name works everywhere.
|
||||||
|
|
||||||
|
=== Disk Images and Packaging
|
||||||
|
|
||||||
|
Some ports need a packaging step beyond compiling.
|
||||||
|
|
||||||
|
*Apple IIgs.* Programs are placed on an 800 KB ProDOS disk image:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ make iigs-disk
|
||||||
|
$ JOEY_DISK_EXAMPLES="MYGAME" make iigs-disk # just yours
|
||||||
|
----
|
||||||
|
|
||||||
|
The floppy cannot hold every example at once, so the packager drops some. If
|
||||||
|
your program is missing from the image, build it with `JOEY_DISK_EXAMPLES`
|
||||||
|
naming it explicitly, and check the `added:` line the packager prints. A
|
||||||
|
program plus large assets may not fit 800 KB at all, in which case use the
|
||||||
|
bigger game-disk image on a CFFA2 card:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ JOEY_GAME_DISK_SIZE=2048KB scripts/make-iigs-game-disk.sh \
|
||||||
|
build/iigs/bin/mygame.2mg MYGAME
|
||||||
|
----
|
||||||
|
|
||||||
|
*Sharp X68000.* Programs ship on a Human68k floppy. Be aware that a stale image
|
||||||
|
can hide hundreds of kilobytes in subdirectories that a directory listing does
|
||||||
|
not show; rebuild from empty rather than deleting files if space seems to
|
||||||
|
vanish.
|
||||||
|
|
||||||
|
*Amiga.* The runner writes an `s/startup-sequence` that invokes your binary.
|
||||||
|
|
||||||
|
=== Verifying It Actually Runs
|
||||||
|
|
||||||
|
Build success is weak evidence on retro targets. Run it:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ scripts/run-iigs.sh MYGAME
|
||||||
|
$ scripts/run-amiga.sh MyGame
|
||||||
|
$ scripts/run-atarist.sh MYGAME
|
||||||
|
$ scripts/run-dos.sh MYGAME
|
||||||
|
$ scripts/run-x68000.sh MYGAME
|
||||||
|
----
|
||||||
|
|
||||||
|
There are also headless runners that capture a screenshot and the program's log
|
||||||
|
without opening a window, which is what you want when iterating:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ scripts/run-atarist-headless.sh MYGAME 90
|
||||||
|
$ scripts/run-amiga-headless.sh MyGame 90
|
||||||
|
----
|
||||||
|
|
||||||
|
=== The Gates
|
||||||
|
|
||||||
|
The repository carries automated checks. They exist because several classes of
|
||||||
|
bug on these machines are invisible until measured, and they are worth running
|
||||||
|
against your own program too.
|
||||||
|
|
||||||
|
[cols="2,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Command |What it proves
|
||||||
|
|
||||||
|
|`make verify-audio`
|
||||||
|
|Every port's audio gate: the IIgs DOC and X68000 chips are read directly,
|
||||||
|
the ST's YM2149 register writes are traced, and the Amiga and DOS output is
|
||||||
|
recorded and graded. Exits 3, not 0, if a gate had to be skipped.
|
||||||
|
|
||||||
|
|`make iigs-verify`
|
||||||
|
|The IIgs actually reached the program and drew something.
|
||||||
|
|
||||||
|
|`scripts/bench-<port>.sh`
|
||||||
|
|Per-operation throughput for that port.
|
||||||
|
|
||||||
|
|`tools/diff-uber-hashes`
|
||||||
|
|Compares a port's rendered output against the IIgs golden reference,
|
||||||
|
operation by operation, by surface hash.
|
||||||
|
|===
|
||||||
|
|
||||||
|
That last one is the strongest tool in the repository. `jlSurfaceHash` produces
|
||||||
|
the same value for the same logical pixels on every platform regardless of
|
||||||
|
internal storage format, so a hash mismatch means one port is genuinely drawing
|
||||||
|
different pixels from the others.
|
||||||
|
|
||||||
|
=== Logging from the Target
|
||||||
|
|
||||||
|
Debugging a program running on an emulated 1988 machine is awkward. JoeyLib
|
||||||
|
provides a logger that writes to a file next to the executable:
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
jlLogReset();
|
||||||
|
jlLogF("level %d loaded, %u cels", levelNum, celCount);
|
||||||
|
jlLogFlush();
|
||||||
|
----
|
||||||
|
|
||||||
|
The headless runners retrieve that file automatically.
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
The logger needs a multi-kilobyte ring buffer, which some targets cannot
|
||||||
|
afford. Space Taxi compiles it out entirely on the IIgs and X68000 for that
|
||||||
|
reason. If you are debugging a memory-tight program, expect to do the same, and
|
||||||
|
fall back to observing hardware state through an emulator instead.
|
||||||
|
====
|
||||||
204
docs/GettingStarted.adoc
Normal file
204
docs/GettingStarted.adoc
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
== Getting Started
|
||||||
|
|
||||||
|
=== Setting Up the Toolchains
|
||||||
|
|
||||||
|
JoeyLib cross-compiles for five machines, and every cross toolchain lives under
|
||||||
|
`toolchains/` in the repository. They are fetched and built once:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ ./toolchains/install.sh
|
||||||
|
----
|
||||||
|
|
||||||
|
Then, *in every shell session before you build anything*:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ source toolchains/env.sh
|
||||||
|
----
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
Forgetting `source toolchains/env.sh` is the single most common build problem.
|
||||||
|
The makefiles reference the cross compilers only through the variables that
|
||||||
|
script sets, so without it a build can appear to succeed while producing stale
|
||||||
|
or missing binaries, and errors are reported as `Error 127 (ignored)`. If a
|
||||||
|
build behaves strangely, source the script and try again.
|
||||||
|
====
|
||||||
|
|
||||||
|
Check what the build can see:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ make status
|
||||||
|
Toolchain availability:
|
||||||
|
iigs: yes
|
||||||
|
amiga: yes
|
||||||
|
atarist: yes
|
||||||
|
dos: yes
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Building
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ make # every target whose toolchain is present
|
||||||
|
$ make iigs # Apple IIgs only
|
||||||
|
$ make amiga # Commodore Amiga only
|
||||||
|
$ make atarist # Atari ST only
|
||||||
|
$ make dos # MS-DOS only
|
||||||
|
$ make clean # remove all build outputs
|
||||||
|
----
|
||||||
|
|
||||||
|
The X68000 port has its own makefile:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ make -f make/x68000.mk
|
||||||
|
----
|
||||||
|
|
||||||
|
Built programs land under `build/<platform>/bin/`. The IIgs additionally
|
||||||
|
produces a bootable disk image:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ make iigs-disk # build/iigs/bin/joey.2mg
|
||||||
|
----
|
||||||
|
|
||||||
|
[TIP]
|
||||||
|
====
|
||||||
|
Never judge a retro build by its exit status alone. Check that the file you
|
||||||
|
expect actually appeared and has a recent timestamp. `make iigs` builds the
|
||||||
|
library but *no examples* -- `make iigs-disk` is what builds and packages them.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== Hello, JoeyLib
|
||||||
|
|
||||||
|
The smallest complete program. This is `examples/hello/hello.c`:
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <joey/joey.h>
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
jlConfigT config;
|
||||||
|
|
||||||
|
config.codegenBytes = 8 * 1024;
|
||||||
|
config.audioBytes = 64UL * 1024;
|
||||||
|
|
||||||
|
if (!jlInit(&config)) {
|
||||||
|
fprintf(stderr, "jlInit failed: %s\n", jlLastError());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("JoeyLib %s\n", jlVersionString());
|
||||||
|
printf("Platform: %s\n", jlPlatformName());
|
||||||
|
printf("Hello from JoeyLib.\n");
|
||||||
|
|
||||||
|
jlShutdown();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
Three things to notice, because they apply to every JoeyLib program:
|
||||||
|
|
||||||
|
. *One header.* `#include <joey/joey.h>` pulls in everything except the
|
||||||
|
optional serial add-on.
|
||||||
|
. *`jlInit` can fail*, and when it does `jlLastError()` tells you why. Always
|
||||||
|
check it.
|
||||||
|
. *`jlShutdown` is not optional.* It restores the display mode, releases the
|
||||||
|
Memory Manager blocks or DOS memory the library took, and on several ports
|
||||||
|
un-hooks interrupt vectors. Skipping it can leave the machine in an unusable
|
||||||
|
state.
|
||||||
|
|
||||||
|
=== Something on the Screen
|
||||||
|
|
||||||
|
`hello.c` prints text but never draws. Here is the smallest program that puts
|
||||||
|
graphics on screen and waits for a keypress:
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
#include <joey/joey.h>
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
jlConfigT config;
|
||||||
|
jlSurfaceT *stage;
|
||||||
|
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||||
|
uint8_t i;
|
||||||
|
|
||||||
|
config.codegenBytes = 8 * 1024;
|
||||||
|
config.audioBytes = 0;
|
||||||
|
if (!jlInit(&config)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
stage = jlStageGet(); // the library-owned back buffer
|
||||||
|
|
||||||
|
// Build palette 0. Entry 0 is forced black whatever we put here.
|
||||||
|
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
|
||||||
|
colors[i] = 0x000;
|
||||||
|
}
|
||||||
|
colors[1] = 0xFFF; // white
|
||||||
|
colors[2] = 0xF00; // red
|
||||||
|
colors[3] = 0x00F; // blue
|
||||||
|
jlPaletteSet(stage, 0, colors);
|
||||||
|
|
||||||
|
// Every scanline uses palette 0 (this is also the default).
|
||||||
|
jlScbSetRange(stage, 0, SURFACE_HEIGHT - 1, 0);
|
||||||
|
|
||||||
|
jlSurfaceClear(stage, 0);
|
||||||
|
jlFillRect(stage, 40, 30, 240, 140, 3);
|
||||||
|
jlDrawRect(stage, 40, 30, 240, 140, 1);
|
||||||
|
jlDrawLine(stage, 0, 0, SURFACE_WIDTH - 1, SURFACE_HEIGHT - 1, 2);
|
||||||
|
|
||||||
|
jlStagePresent(); // nothing is visible until this runs
|
||||||
|
jlWaitForAnyKey();
|
||||||
|
|
||||||
|
jlShutdown();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
The critical line is `jlStagePresent()`. All drawing goes to an off-screen
|
||||||
|
back buffer called the *stage*; nothing reaches the display until you present.
|
||||||
|
|
||||||
|
=== Running What You Built
|
||||||
|
|
||||||
|
Helper scripts boot each target in an emulator:
|
||||||
|
|
||||||
|
[source,console]
|
||||||
|
----
|
||||||
|
$ scripts/run-iigs.sh DRAW
|
||||||
|
$ scripts/run-amiga.sh Draw
|
||||||
|
$ scripts/run-atarist.sh DRAW
|
||||||
|
$ scripts/run-dos.sh DRAW
|
||||||
|
$ scripts/run-x68000.sh DRAW
|
||||||
|
----
|
||||||
|
|
||||||
|
Note the capitalisation differences: the IIgs, ST and DOS use uppercase names
|
||||||
|
(ProDOS, GEMDOS and DOS 8.3 conventions), while the Amiga keeps mixed case.
|
||||||
|
|
||||||
|
=== The Example Programs
|
||||||
|
|
||||||
|
The `examples/` directory is the best available reference, and every example is
|
||||||
|
built for every platform:
|
||||||
|
|
||||||
|
[cols="1,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Example |What it shows
|
||||||
|
|
||||||
|
|`hello` |Minimal init, version query, shutdown.
|
||||||
|
|`draw` |Every drawing primitive, laid out in four quadrants.
|
||||||
|
|`pattern` |Palette and per-scanline palette (SCB) work.
|
||||||
|
|`keys` |Typed-character input and the key predicates.
|
||||||
|
|`joy` |Joystick axes, buttons and recalibration.
|
||||||
|
|`sprite` |Sprite loading, compiling, save-under and restore.
|
||||||
|
|`audio` |Tracker music, `.MOD` playback and digital sound effects.
|
||||||
|
|`save` |Save files and the disk-space query.
|
||||||
|
|`serial` |The optional RS-232 add-on.
|
||||||
|
|`adventure` |A small complete game.
|
||||||
|
|`agi` |A Sierra AGI interpreter, showing the library under real load.
|
||||||
|
|`spacetaxi` |A full port of Space Taxi: the largest example.
|
||||||
|
|`uber` |The validation and benchmark harness.
|
||||||
|
|===
|
||||||
61
docs/Manual.adoc
Normal file
61
docs/Manual.adoc
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
= JoeyLib Manual
|
||||||
|
Scott Duensing <scott.duensing@gmail.com>
|
||||||
|
:revnumber: 1.0
|
||||||
|
:revdate: 2026
|
||||||
|
:doctype: book
|
||||||
|
:title-logo-image: image:../assets/joeylibLogo.svg[JoeyLib, pdfwidth=4.5in, align=center]
|
||||||
|
:toc: left
|
||||||
|
:toclevels: 3
|
||||||
|
:sectnums:
|
||||||
|
:sectnumlevels: 3
|
||||||
|
:source-highlighter: rouge
|
||||||
|
:icons: font
|
||||||
|
:experimental:
|
||||||
|
:xrefstyle: short
|
||||||
|
|
||||||
|
// One book: the guide chapters first, then the API reference, then the
|
||||||
|
// appendices. Every chapter is its own .adoc in docs/ so a chapter can be
|
||||||
|
// edited without touching the rest. Build with scripts/build-docs.sh, or
|
||||||
|
// `make docs`, which writes build/docs/JoeyLib-Manual.html and .pdf.
|
||||||
|
|
||||||
|
= Guide
|
||||||
|
|
||||||
|
include::Overview.adoc[]
|
||||||
|
|
||||||
|
include::GettingStarted.adoc[]
|
||||||
|
|
||||||
|
include::ProgramLayout.adoc[]
|
||||||
|
|
||||||
|
include::Portability.adoc[]
|
||||||
|
|
||||||
|
include::Assets.adoc[]
|
||||||
|
|
||||||
|
include::BuildIntegration.adoc[]
|
||||||
|
|
||||||
|
= API Reference
|
||||||
|
|
||||||
|
include::ApiCore.adoc[]
|
||||||
|
|
||||||
|
include::ApiSurface.adoc[]
|
||||||
|
|
||||||
|
include::ApiPalette.adoc[]
|
||||||
|
|
||||||
|
include::ApiDraw.adoc[]
|
||||||
|
|
||||||
|
include::ApiTile.adoc[]
|
||||||
|
|
||||||
|
include::ApiSprite.adoc[]
|
||||||
|
|
||||||
|
include::ApiInput.adoc[]
|
||||||
|
|
||||||
|
include::ApiAudio.adoc[]
|
||||||
|
|
||||||
|
include::ApiFile.adoc[]
|
||||||
|
|
||||||
|
include::ApiSerial.adoc[]
|
||||||
|
|
||||||
|
include::ApiDebug.adoc[]
|
||||||
|
|
||||||
|
= Appendices
|
||||||
|
|
||||||
|
include::Appendix.adoc[]
|
||||||
118
docs/Overview.adoc
Normal file
118
docs/Overview.adoc
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
== Introduction
|
||||||
|
|
||||||
|
=== What JoeyLib Is
|
||||||
|
|
||||||
|
JoeyLib is a single-codebase C game library for five 16-bit and 32-bit retro
|
||||||
|
machines. You write one program against one API, and the same source compiles
|
||||||
|
and runs on all of them:
|
||||||
|
|
||||||
|
[cols="2,2,2,4", options="header"]
|
||||||
|
|===
|
||||||
|
|Platform |CPU |RAM |Notes
|
||||||
|
|
||||||
|
|Apple IIgs
|
||||||
|
|65C816 at 2.8 MHz
|
||||||
|
|1 MB typical
|
||||||
|
|The reference platform. Accelerators run faster with no code changes.
|
||||||
|
|
||||||
|
|Commodore Amiga
|
||||||
|
|68000 (A500 class)
|
||||||
|
|512 KB chip, 512 KB fast recommended
|
||||||
|
|OCS chipset. Native planar bitplanes.
|
||||||
|
|
||||||
|
|Atari ST
|
||||||
|
|68000 (STF or STE)
|
||||||
|
|512 KB
|
||||||
|
|Word-interleaved planar. Shares 68000 assembly with the Amiga port.
|
||||||
|
|
||||||
|
|MS-DOS
|
||||||
|
|386 or later
|
||||||
|
|2 MB
|
||||||
|
|VGA required. DPMI supplied by an embedded CWSDPMI.
|
||||||
|
|
||||||
|
|Sharp X68000
|
||||||
|
|68000 at 10 MHz
|
||||||
|
|stock
|
||||||
|
|Draws into the text plane, 320x200 centred in a 512x512 screen.
|
||||||
|
|===
|
||||||
|
|
||||||
|
The Apple IIgs is deliberately the slowest target and the design reference. If
|
||||||
|
something performs acceptably there, it performs acceptably everywhere.
|
||||||
|
|
||||||
|
=== The Display Contract
|
||||||
|
|
||||||
|
Every JoeyLib program gets exactly the same screen on every platform:
|
||||||
|
|
||||||
|
* *320 x 200 pixels*, always. There is no other resolution.
|
||||||
|
* *16 colours on screen at once*, chosen per scanline from one of 16 palettes.
|
||||||
|
* *Colour 0 of every palette is black*, always, and is forced to black by the
|
||||||
|
library even if you try to set it to something else. This is not negotiable:
|
||||||
|
the Amiga and ST hardware border follows colour 0 of the active palette, and
|
||||||
|
colour 0 is the transparent colour for sprite drawing. You have 15 usable
|
||||||
|
colours per palette.
|
||||||
|
* Colours are *12-bit `$0RGB`*: bits 11..8 red, 7..4 green, 3..0 blue.
|
||||||
|
* Origin `(0, 0)` is *top-left*. X increases right, Y increases down.
|
||||||
|
Coordinates are signed `int16_t`, so off-screen positions are representable.
|
||||||
|
|
||||||
|
Every drawing primitive clips to the surface rectangle. An out-of-bounds draw
|
||||||
|
is a silent no-op, never an error. There are no "fast unclipped" variants in
|
||||||
|
the public API, so you cannot corrupt memory by drawing off the edge.
|
||||||
|
|
||||||
|
=== What the Library Hides
|
||||||
|
|
||||||
|
The five machines store pixels in genuinely incompatible ways. JoeyLib keeps
|
||||||
|
that out of your program:
|
||||||
|
|
||||||
|
[cols="2,3", options="header"]
|
||||||
|
|===
|
||||||
|
|Platform |In-memory pixel storage
|
||||||
|
|
||||||
|
|Apple IIgs, MS-DOS
|
||||||
|
|Chunky 4bpp packed: two pixels per byte, high nibble is the left pixel.
|
||||||
|
|
||||||
|
|Commodore Amiga
|
||||||
|
|Native planar: four separate bitplanes.
|
||||||
|
|
||||||
|
|Atari ST
|
||||||
|
|Native planar, word-interleaved: four plane words per 16 pixels, per row.
|
||||||
|
|
||||||
|
|Sharp X68000
|
||||||
|
|Text-plane planes, four planes 128 KB apart.
|
||||||
|
|===
|
||||||
|
|
||||||
|
Every public entry point speaks in *colour indices, 0 to 15*. Your code never
|
||||||
|
sees the storage format. The one place the difference leaks through is
|
||||||
|
documented and flagged in this manual -- see <<_writing_portable_code>>.
|
||||||
|
|
||||||
|
=== What You Get
|
||||||
|
|
||||||
|
* Surfaces, palettes and per-scanline palette selection
|
||||||
|
* Drawing primitives: pixels, lines, rectangles, circles, flood fill
|
||||||
|
* 8x8 tiles, tile banks, tile maps and bitmap text
|
||||||
|
* Sprites with colour-0 transparency and *runtime-compiled blitters* -- the
|
||||||
|
library emits native 65816, 68000 or x86 code for each sprite at run time
|
||||||
|
* Keyboard (both game-style predicates and a typed-character queue), mouse
|
||||||
|
and joystick
|
||||||
|
* Four-channel Protracker `.MOD` music, a portable chip-tracker music format,
|
||||||
|
digital one-shot and streaming sound effects, and a PSG-style tone and
|
||||||
|
noise layer
|
||||||
|
* Read-only game data files and writable save files
|
||||||
|
* An optional RS-232 serial add-on
|
||||||
|
* A deterministic, seedable random generator that produces *bit-identical*
|
||||||
|
output on every port for a given seed
|
||||||
|
|
||||||
|
=== How to Read This Manual
|
||||||
|
|
||||||
|
If you are new to JoeyLib, read <<_getting_started>>, then
|
||||||
|
<<_anatomy_of_a_joeylib_program>>, then <<_writing_portable_code>>. Those three
|
||||||
|
chapters are the whole programming model.
|
||||||
|
|
||||||
|
<<_creating_assets>> covers getting graphics, music and sound into a form the
|
||||||
|
library can load.
|
||||||
|
|
||||||
|
<<_adding_your_program_to_the_build>> explains how to get your own program
|
||||||
|
compiling for all five targets.
|
||||||
|
|
||||||
|
The API Reference documents every public function: its exact signature, each
|
||||||
|
parameter, what it returns, and any per-platform behaviour you need to know
|
||||||
|
about.
|
||||||
278
docs/Portability.adoc
Normal file
278
docs/Portability.adoc
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
== Writing Portable Code
|
||||||
|
|
||||||
|
A JoeyLib program that follows the rules in this chapter compiles and behaves
|
||||||
|
the same on all five machines. Most of the rules exist because one particular
|
||||||
|
target cannot do something the others can, and the Apple IIgs is usually that
|
||||||
|
target.
|
||||||
|
|
||||||
|
=== The Rules, in Short
|
||||||
|
|
||||||
|
. Use `jlAlloc` for anything large, never `malloc`.
|
||||||
|
. Never assume a frame rate. Ask `jlFrameHz()` or use `jlMillisElapsed()`.
|
||||||
|
. Never assume colour 0 is anything but black.
|
||||||
|
. Compile your sprites before you draw them.
|
||||||
|
. Keep large objects out of function-local variables on the IIgs.
|
||||||
|
. Use `jlRandom`, not `rand`, when identical behaviour across ports matters.
|
||||||
|
. Keep file names short, uppercase-safe and 8.3-compatible.
|
||||||
|
. Do not mix save-under with clean-buffer erasing in the same screen region.
|
||||||
|
. Pick one of the audio layers per scene, not two.
|
||||||
|
|
||||||
|
The rest of this chapter explains each one.
|
||||||
|
|
||||||
|
=== Memory
|
||||||
|
|
||||||
|
This is the rule that catches everyone. *The C heap on the IIgs is tiny.* It
|
||||||
|
lives in bank 0, and a single `malloc` is capped at roughly 32 KB -- often far
|
||||||
|
less in practice, because the clang libc heap on that port can be a few hundred
|
||||||
|
bytes. A program that `malloc`s a 32000-byte buffer works on four targets and
|
||||||
|
fails on the reference one.
|
||||||
|
|
||||||
|
Use the library allocator, which is backed by each platform's native allocator
|
||||||
|
(the Memory Manager on the IIgs):
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint8_t *buffer = (uint8_t *)jlAlloc(32000);
|
||||||
|
if (buffer == NULL) {
|
||||||
|
// handle it -- this genuinely can fail on a stock machine
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
jlFree(buffer);
|
||||||
|
----
|
||||||
|
|
||||||
|
`jlAlloc` does *not* zero the memory it returns.
|
||||||
|
|
||||||
|
For bulk copies, prefer `jlMemCopy` over `memcpy`. It has the same contract
|
||||||
|
(no overlap) but the IIgs implements it as `MVN` block moves at 7 cycles per
|
||||||
|
byte instead of a libc byte loop, split correctly at bank boundaries.
|
||||||
|
|
||||||
|
==== Large Local Variables
|
||||||
|
|
||||||
|
On the IIgs, a large function-local variable in a deep call chain can silently
|
||||||
|
overflow the soft stack and crash -- roughly 2 KB is enough to do it, and the
|
||||||
|
trigger is call depth rather than anything the compiler warns about. Make big
|
||||||
|
buffers `static`, or allocate them with `jlAlloc`:
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
// DANGEROUS on the IIgs, especially deep in a call chain:
|
||||||
|
void renderScene(void) {
|
||||||
|
uint8_t scratch[2048];
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe everywhere:
|
||||||
|
void renderScene(void) {
|
||||||
|
static uint8_t scratch[2048];
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Timing
|
||||||
|
|
||||||
|
Covered in <<_frame_rate_independence>>, and worth repeating because it is the
|
||||||
|
second most common portability bug: the targets run at 50, 60 and 70 Hz. Scale
|
||||||
|
by `jlFrameHz()` or work from `jlMillisElapsed()`.
|
||||||
|
|
||||||
|
`jlFrameCount()` is a 16-bit counter, so you must poll it faster than twice the
|
||||||
|
frame rate or you will miss edges, and it wraps roughly every 18 minutes at
|
||||||
|
60 Hz.
|
||||||
|
|
||||||
|
=== Colour and the Palette
|
||||||
|
|
||||||
|
Colour 0 is black in every palette on every platform, and `jlPaletteSet`
|
||||||
|
silently rewrites entry 0 to `$000` no matter what you pass. Do not design
|
||||||
|
artwork that needs colour 0 to be something else -- you have 15 free colours per
|
||||||
|
palette, and 16 palettes to switch between per scanline.
|
||||||
|
|
||||||
|
Colour 0 is also the transparent colour for sprites. A sprite pixel of colour 0
|
||||||
|
is never drawn. If you need an opaque rectangle, use the tile operations, which
|
||||||
|
have both opaque and masked forms.
|
||||||
|
|
||||||
|
=== Sprites
|
||||||
|
|
||||||
|
Two rules.
|
||||||
|
|
||||||
|
*Compile before drawing.* `jlSpriteDraw` never compiles on demand. An
|
||||||
|
uncompiled sprite runs through the interpreter at roughly 1/40th the speed, and
|
||||||
|
nothing warns you.
|
||||||
|
|
||||||
|
*Align your backup buffers.* Give any `jlSpriteBackupT` backing array 2-byte
|
||||||
|
alignment, by declaring it `uint16_t` or with an alignment attribute. An odd
|
||||||
|
buffer address routes save and restore onto slow interpreted paths on the
|
||||||
|
planar ports, because the 68000 cannot perform word accesses at odd addresses.
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
// Right: 2-byte aligned by construction.
|
||||||
|
static uint16_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4) / 2];
|
||||||
|
|
||||||
|
// Risky: may land on an odd address.
|
||||||
|
static uint8_t gBacking[JOEY_SPRITE_BACKUP_BYTES(4, 4)];
|
||||||
|
----
|
||||||
|
|
||||||
|
Note also that on the planar ports the region a save-under actually captures is
|
||||||
|
the *16-pixel-aligned window* containing the sprite, not the sprite rectangle.
|
||||||
|
`jlSpriteSaveUnder` reports the true geometry in the backup structure's
|
||||||
|
`x`, `width`, `height` and `sizeBytes` fields. It is that recorded window you
|
||||||
|
must not overwrite before restoring.
|
||||||
|
|
||||||
|
=== Randomness
|
||||||
|
|
||||||
|
`rand()` differs wildly between C libraries, so a procedurally generated level
|
||||||
|
would not match across ports. `jlRandom` is xorshift32 and produces
|
||||||
|
*bit-identical* output on every platform for a given seed:
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
jlRandomSeed(12345);
|
||||||
|
uint16_t roll = jlRandomRange(6); // 0..5, same everywhere
|
||||||
|
----
|
||||||
|
|
||||||
|
A seed of 0 is remapped to 1, because xorshift cannot escape the all-zero
|
||||||
|
state. This generator is fine for games and useless for cryptography.
|
||||||
|
|
||||||
|
=== Files
|
||||||
|
|
||||||
|
Two directories sit beside the executable, and the library prefixes them for
|
||||||
|
you:
|
||||||
|
|
||||||
|
* `DATA/` -- read-only game content, opened with `jlDataOpen`
|
||||||
|
* `SAVES/` -- writable saves and preferences, via the `jlSave*` calls
|
||||||
|
|
||||||
|
Pass bare names: `jlDataOpen("levels/title.dat", "rb")`, never
|
||||||
|
`"DATA/levels/title.dat"`.
|
||||||
|
|
||||||
|
Keep names portable across the retro filesystems this library targets: *at most
|
||||||
|
8 characters plus a short extension, letters and digits only*, so they satisfy
|
||||||
|
both ProDOS and DOS 8.3. Do not rely on case being preserved.
|
||||||
|
|
||||||
|
A failed save is not fatal -- the calls report failure and your program should
|
||||||
|
degrade gracefully, because on some volumes `SAVES/` simply is not writable.
|
||||||
|
|
||||||
|
=== Audio
|
||||||
|
|
||||||
|
JoeyLib gives you three sound layers, and on two platforms they share physical
|
||||||
|
hardware:
|
||||||
|
|
||||||
|
[cols="2,5", options="header"]
|
||||||
|
|===
|
||||||
|
|Layer |What it is
|
||||||
|
|
||||||
|
|Module playback
|
||||||
|
|`jlAudioPlayMod` -- four-channel Protracker `.MOD`, mixed in software on
|
||||||
|
DOS and ST, hardware-replayed on Amiga and IIgs.
|
||||||
|
|
||||||
|
|Chip tracker
|
||||||
|
|`jlMusicPlay` -- a portable event stream driving three tone voices plus
|
||||||
|
noise, at register-write cost with no CPU mixing.
|
||||||
|
|
||||||
|
|Voices and effects
|
||||||
|
|`jlAudioVoice`, `jlAudioNoise`, `jlAudioPlaySfx` and the streaming forms.
|
||||||
|
|===
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
On the Atari ST and Amiga, the tone voices *are* the same hardware the module
|
||||||
|
player drives. While a `.MOD` is playing, `jlAudioVoice` writes fight the
|
||||||
|
player's register writes and lose. Choose the module layer or the voice and
|
||||||
|
tracker layer *per scene*, not both at once.
|
||||||
|
====
|
||||||
|
|
||||||
|
The chip tracker is the portable choice for music. It costs almost nothing on
|
||||||
|
every port -- unlike `.MOD` playback, which needs real CPU mixing and is
|
||||||
|
marginal on the ST.
|
||||||
|
|
||||||
|
On the IIgs, `jlAudioVoice` and `jlAudioNoise` require `jlAudioInit` (they ride
|
||||||
|
the tracker engine's DOC path) and behave as retriggered one-shots of about
|
||||||
|
80 ms, so a note held longer must be re-asserted roughly once a frame. On DOS,
|
||||||
|
ST and Amiga those two calls are direct hardware programming and work without
|
||||||
|
`jlAudioInit`.
|
||||||
|
|
||||||
|
A failed `jlAudioInit` is *not* fatal. `jlInit` still succeeds and every audio
|
||||||
|
call becomes a silent no-op, so a program need not treat missing sound as an
|
||||||
|
error.
|
||||||
|
|
||||||
|
=== Per-Platform Behaviour Summary
|
||||||
|
|
||||||
|
The API is uniform; these are the places where what you get differs.
|
||||||
|
|
||||||
|
[cols="2,2,2,2,2,2", options="header"]
|
||||||
|
|===
|
||||||
|
|Feature |IIgs |Amiga |Atari ST |MS-DOS |X68000
|
||||||
|
|
||||||
|
|Frame rate
|
||||||
|
|60 Hz
|
||||||
|
|50 Hz (PAL)
|
||||||
|
|60 Hz (NTSC)
|
||||||
|
|70 Hz
|
||||||
|
|60 Hz
|
||||||
|
|
||||||
|
|Pixel storage
|
||||||
|
|chunky 4bpp
|
||||||
|
|planar
|
||||||
|
|planar, interleaved
|
||||||
|
|chunky 4bpp
|
||||||
|
|text-plane planar
|
||||||
|
|
||||||
|
|Stereo pan
|
||||||
|
|all three
|
||||||
|
|left and right
|
||||||
|
|ignored (mono)
|
||||||
|
|ignored (mono)
|
||||||
|
|ignored
|
||||||
|
|
||||||
|
|`jlAudioTone`
|
||||||
|
|DOC oscillator
|
||||||
|
|Paula
|
||||||
|
|YM2149
|
||||||
|
|PIT counter 2
|
||||||
|
|-
|
||||||
|
|
||||||
|
|Joystick
|
||||||
|
|analog, auto-disables
|
||||||
|
|digital
|
||||||
|
|digital
|
||||||
|
|digital
|
||||||
|
|digital
|
||||||
|
|
||||||
|
|Keyboard layout
|
||||||
|
|machine's own
|
||||||
|
|machine's own
|
||||||
|
|machine's own
|
||||||
|
|fixed US
|
||||||
|
|machine's own
|
||||||
|
|
||||||
|
|Serial ports
|
||||||
|
|modem, printer, slot card
|
||||||
|
|`serial.device`
|
||||||
|
|Modem port
|
||||||
|
|COM1..COM4
|
||||||
|
|-
|
||||||
|
|===
|
||||||
|
|
||||||
|
The joystick row deserves a note: the IIgs port *auto-disables* joystick
|
||||||
|
polling after a short window with no stick detected, because polling costs
|
||||||
|
about 3 ms per frame there. It does not re-probe on its own. If a user plugs a
|
||||||
|
stick in while the program is running, you must call `jlJoystickReset` to
|
||||||
|
resume polling.
|
||||||
|
|
||||||
|
=== A Portability Checklist
|
||||||
|
|
||||||
|
Before you call a program portable, confirm:
|
||||||
|
|
||||||
|
* [ ] No `malloc` of anything over a few hundred bytes -- use `jlAlloc`.
|
||||||
|
* [ ] No large function-local arrays; they are `static` or heap.
|
||||||
|
* [ ] Movement and animation scale with `jlFrameHz()` or `jlMillisElapsed()`.
|
||||||
|
* [ ] `jlInputPoll()` is called exactly once per frame.
|
||||||
|
* [ ] `jlAudioFrameTick()` is called every frame.
|
||||||
|
* [ ] Every sprite is compiled during loading.
|
||||||
|
* [ ] Sprite backup buffers are 2-byte aligned and sized with the macro.
|
||||||
|
* [ ] Save file names are 8.3-safe.
|
||||||
|
* [ ] Only one audio layer is active per scene.
|
||||||
|
* [ ] `jlShutdown()` runs on every exit path.
|
||||||
|
* [ ] The program has actually been run on all five targets, not just built.
|
||||||
|
|
||||||
|
That last item is the one that matters. All five ports have emulator run
|
||||||
|
scripts under `scripts/`, and building successfully proves considerably less
|
||||||
|
than running.
|
||||||
359
docs/ProgramLayout.adoc
Normal file
359
docs/ProgramLayout.adoc
Normal file
|
|
@ -0,0 +1,359 @@
|
||||||
|
== Anatomy of a JoeyLib Program
|
||||||
|
|
||||||
|
Every JoeyLib program has the same four-part shape: configure and initialise,
|
||||||
|
set up the screen and load assets, run a frame loop, shut down. This chapter
|
||||||
|
walks through each part and ends with a complete annotated skeleton you can
|
||||||
|
copy.
|
||||||
|
|
||||||
|
=== Part 1: Configure and Initialise
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
jlConfigT config;
|
||||||
|
|
||||||
|
config.codegenBytes = 32UL * 1024; // sprite blitter cache
|
||||||
|
config.audioBytes = 0; // reserved, not yet consulted
|
||||||
|
|
||||||
|
if (!jlInit(&config)) {
|
||||||
|
// jlLastError() explains why. There is no display yet, so on a
|
||||||
|
// real machine the only place to report this is stderr or a log.
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
`codegenBytes` sizes the *codegen arena*: the scratch memory the library emits
|
||||||
|
native sprite blitter code into. Getting it wrong is the most common cause of
|
||||||
|
sprites drawing slowly, so it is worth understanding.
|
||||||
|
|
||||||
|
* `0` selects a per-platform default: 8 KB on the IIgs, 32 KB on
|
||||||
|
Amiga, ST and DOS.
|
||||||
|
* The 68000 and x86 emitters produce code roughly four to eight times less
|
||||||
|
dense than the 65816 one, so the same sprite needs far more arena space on
|
||||||
|
those ports. A 32x32 sprite can need 8 to 16 KB there.
|
||||||
|
* Measure, do not guess: after compiling a sprite, `jlSpriteCompiledSize()`
|
||||||
|
reports its exact cost on the current platform.
|
||||||
|
* On the IIgs the arena is one Memory Manager block that may not cross a 64 KB
|
||||||
|
bank boundary, so requests above 65536 are clamped to 65536 -- and even a
|
||||||
|
clamped request can fail if no free bank exists.
|
||||||
|
|
||||||
|
If the arena is too small, sprites still draw: they fall back to an
|
||||||
|
interpreter that is roughly *forty times slower*. Nothing reports this as an
|
||||||
|
error, so a program that never checks can be silently crawling.
|
||||||
|
|
||||||
|
=== Part 2: The Stage, the Palette and Assets
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
jlSurfaceT *stage = jlStageGet();
|
||||||
|
----
|
||||||
|
|
||||||
|
The *stage* is the library-owned back buffer. You never create or destroy it;
|
||||||
|
it is valid from `jlInit` to `jlShutdown`. All drawing goes here, and
|
||||||
|
`jlStagePresent()` copies it to the real display.
|
||||||
|
|
||||||
|
Set up colour before drawing:
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||||
|
// ... fill in 16 entries of $0RGB ...
|
||||||
|
jlPaletteSet(stage, 0, colors);
|
||||||
|
jlScbSetRange(stage, 0, SURFACE_HEIGHT - 1, 0);
|
||||||
|
----
|
||||||
|
|
||||||
|
Each surface carries *16 palettes of 16 colours* plus a 200-entry table (one
|
||||||
|
per scanline) selecting which palette that line uses. That table is the *SCB*,
|
||||||
|
or scanline control byte table. Using one palette everywhere is the simple
|
||||||
|
case; assigning different palettes to different bands is how a JoeyLib program
|
||||||
|
shows more than 16 colours at once.
|
||||||
|
|
||||||
|
Load assets after init, because sprite loading needs the codegen arena:
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
jlSpriteT *cels[16];
|
||||||
|
uint16_t palette[SURFACE_COLORS_PER_PALETTE];
|
||||||
|
uint16_t count = jlSpriteBankLoad("player.spr", cels, 16, palette);
|
||||||
|
uint16_t i;
|
||||||
|
|
||||||
|
for (i = 0; i < count; i++) {
|
||||||
|
jlSpritePrewarm(cels[i]); // compile NOW, not mid-frame
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
[IMPORTANT]
|
||||||
|
====
|
||||||
|
`jlSpriteDraw` does *not* compile a sprite on demand. An uncompiled sprite
|
||||||
|
draws through the interpreter every single time. Call `jlSpriteCompile` or
|
||||||
|
`jlSpritePrewarm` during loading -- otherwise the first draw of each sprite
|
||||||
|
either stalls a frame (if you compile lazily yourself) or never gets fast at
|
||||||
|
all.
|
||||||
|
====
|
||||||
|
|
||||||
|
=== Part 3: The Frame Loop
|
||||||
|
|
||||||
|
The library does not schedule anything. You own the loop. The canonical shape:
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
while (running) {
|
||||||
|
jlWaitVBL(); // pace to the display's refresh
|
||||||
|
jlInputPoll(); // refresh keyboard, mouse, joystick
|
||||||
|
jlAudioFrameTick(); // pump the audio engine
|
||||||
|
|
||||||
|
if (jlKeyPressed(KEY_ESCAPE)) {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGame(); // your logic
|
||||||
|
renderFrame(stage); // your drawing
|
||||||
|
|
||||||
|
jlStagePresent(); // make it visible
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
Four library calls, in that order, and everything else is yours.
|
||||||
|
|
||||||
|
`jlWaitVBL()` blocks until the next vertical blank. It is cheap on every port
|
||||||
|
because the underlying mechanism is always a hardware wait, never a software
|
||||||
|
timer.
|
||||||
|
|
||||||
|
`jlInputPoll()` must be called exactly once per frame. The edge predicates
|
||||||
|
(`jlKeyPressed`, `jlKeyReleased`, and the mouse and joystick equivalents) are
|
||||||
|
defined relative to the *previous poll*, so calling it twice per frame makes
|
||||||
|
edges disappear, and not calling it means nothing ever changes.
|
||||||
|
|
||||||
|
`jlAudioFrameTick()` is a no-op on ports whose audio runs from a hardware
|
||||||
|
interrupt, but it is required on ports that mix in user context, and it is what
|
||||||
|
advances tracker music and keeps streaming sound effects fed. Call it
|
||||||
|
unconditionally, once per frame.
|
||||||
|
|
||||||
|
`jlStagePresent()` flips the dirty parts of the stage to the display. The
|
||||||
|
drawing primitives record which rows they touched, so presenting only copies
|
||||||
|
what actually changed. You do not need to tell it what you drew.
|
||||||
|
|
||||||
|
[NOTE]
|
||||||
|
====
|
||||||
|
If you write pixels by some route other than the library's primitives, the
|
||||||
|
dirty tracking does not know about it, and `jlStagePresent` will skip those
|
||||||
|
rows. That is the one case where you have to mark the region yourself.
|
||||||
|
====
|
||||||
|
|
||||||
|
==== Frame-Rate Independence
|
||||||
|
|
||||||
|
The five targets do not share a refresh rate: 50 Hz (Amiga PAL), 60 Hz (IIgs,
|
||||||
|
ST NTSC, X68000) and 70 Hz (VGA mode 13h). A loop that moves a sprite "two
|
||||||
|
pixels per frame" runs 40 percent faster on DOS than on a PAL Amiga.
|
||||||
|
|
||||||
|
Two ways to handle it. Ask the frame rate and scale your per-frame step:
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint16_t hz = jlFrameHz(); // 50, 60 or 70
|
||||||
|
----
|
||||||
|
|
||||||
|
Or drive from the millisecond clock, which is the more robust choice for
|
||||||
|
anything that must feel the same everywhere:
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint32_t now = jlMillisElapsed();
|
||||||
|
uint32_t step = now - previous;
|
||||||
|
previous = now;
|
||||||
|
----
|
||||||
|
|
||||||
|
`jlMillisElapsed()` reports true elapsed wall-clock time, so a frame that took
|
||||||
|
a long time to render is accounted for correctly rather than counted as one
|
||||||
|
tick.
|
||||||
|
|
||||||
|
=== Part 4: Shut Down
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
jlAudioShutdown(); // if you called jlAudioInit
|
||||||
|
jlShutdown();
|
||||||
|
return 0;
|
||||||
|
----
|
||||||
|
|
||||||
|
`jlShutdown` restores the display mode, frees the library's allocations, and
|
||||||
|
un-hooks interrupt vectors. Sprites and surfaces you created are released with
|
||||||
|
it, but the buffers *you* allocated (including tile data you handed to
|
||||||
|
`jlSpriteCreate`, which is never copied) remain yours to free.
|
||||||
|
|
||||||
|
=== The Complete Skeleton
|
||||||
|
|
||||||
|
This compiles and runs on all five targets as-is.
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
// A minimal but complete JoeyLib program.
|
||||||
|
|
||||||
|
#include <joey/joey.h>
|
||||||
|
|
||||||
|
#define PLAYER_SPEED 2
|
||||||
|
|
||||||
|
static jlSurfaceT *gStage;
|
||||||
|
static bool gRunning = true;
|
||||||
|
static int16_t gPlayerX = 160;
|
||||||
|
static int16_t gPlayerY = 100;
|
||||||
|
|
||||||
|
static void buildPalette(void);
|
||||||
|
static void handleInput(void);
|
||||||
|
static void renderFrame(void);
|
||||||
|
|
||||||
|
|
||||||
|
static void buildPalette(void) {
|
||||||
|
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||||
|
uint8_t i;
|
||||||
|
|
||||||
|
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
|
||||||
|
colors[i] = 0x000;
|
||||||
|
}
|
||||||
|
colors[1] = 0xFFF; // white
|
||||||
|
colors[2] = 0x0C0; // green
|
||||||
|
colors[3] = 0x00C; // blue
|
||||||
|
|
||||||
|
jlPaletteSet(gStage, 0, colors);
|
||||||
|
jlScbSetRange(gStage, 0, SURFACE_HEIGHT - 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void handleInput(void) {
|
||||||
|
if (jlKeyPressed(KEY_ESCAPE)) {
|
||||||
|
gRunning = false;
|
||||||
|
}
|
||||||
|
if (jlKeyDown(KEY_LEFT)) {
|
||||||
|
gPlayerX -= PLAYER_SPEED;
|
||||||
|
}
|
||||||
|
if (jlKeyDown(KEY_RIGHT)) {
|
||||||
|
gPlayerX += PLAYER_SPEED;
|
||||||
|
}
|
||||||
|
if (jlKeyDown(KEY_UP)) {
|
||||||
|
gPlayerY -= PLAYER_SPEED;
|
||||||
|
}
|
||||||
|
if (jlKeyDown(KEY_DOWN)) {
|
||||||
|
gPlayerY += PLAYER_SPEED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A joystick, if one is attached, drives the same movement.
|
||||||
|
if (jlJoystickConnected(JOYSTICK_0)) {
|
||||||
|
if (jlJoystickX(JOYSTICK_0) < -64) {
|
||||||
|
gPlayerX -= PLAYER_SPEED;
|
||||||
|
}
|
||||||
|
if (jlJoystickX(JOYSTICK_0) > 64) {
|
||||||
|
gPlayerX += PLAYER_SPEED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the player on screen: the primitives would clip safely, but
|
||||||
|
// clamping keeps the object whole.
|
||||||
|
if (gPlayerX < 0) {
|
||||||
|
gPlayerX = 0;
|
||||||
|
}
|
||||||
|
if (gPlayerX > SURFACE_WIDTH - 16) {
|
||||||
|
gPlayerX = SURFACE_WIDTH - 16;
|
||||||
|
}
|
||||||
|
if (gPlayerY < 0) {
|
||||||
|
gPlayerY = 0;
|
||||||
|
}
|
||||||
|
if (gPlayerY > SURFACE_HEIGHT - 16) {
|
||||||
|
gPlayerY = SURFACE_HEIGHT - 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void renderFrame(void) {
|
||||||
|
jlSurfaceClear(gStage, 0);
|
||||||
|
jlDrawRect(gStage, 0, 0, SURFACE_WIDTH, SURFACE_HEIGHT, 1);
|
||||||
|
jlFillRect(gStage, gPlayerX, gPlayerY, 16, 16, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
jlConfigT config;
|
||||||
|
|
||||||
|
config.codegenBytes = 32UL * 1024;
|
||||||
|
config.audioBytes = 0;
|
||||||
|
|
||||||
|
if (!jlInit(&config)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
gStage = jlStageGet();
|
||||||
|
if (gStage == NULL) {
|
||||||
|
jlShutdown();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPalette();
|
||||||
|
jlRandomSeed(12345); // deterministic on every port
|
||||||
|
|
||||||
|
while (gRunning) {
|
||||||
|
jlWaitVBL();
|
||||||
|
jlInputPoll();
|
||||||
|
jlAudioFrameTick();
|
||||||
|
|
||||||
|
handleInput();
|
||||||
|
renderFrame();
|
||||||
|
|
||||||
|
jlStagePresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
jlShutdown();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Two Drawing Strategies
|
||||||
|
|
||||||
|
Once you have moving objects over a background, you need to erase them. There
|
||||||
|
are two patterns, and *mixing them in the same region corrupts the screen*.
|
||||||
|
|
||||||
|
==== Save-Under and Restore
|
||||||
|
|
||||||
|
Before drawing a sprite, capture what is underneath; next frame, put it back.
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
uint16_t backing[JOEY_SPRITE_BACKUP_BYTES(2, 2) / 2];
|
||||||
|
jlSpriteBackupT backup;
|
||||||
|
|
||||||
|
backup.bytes = (uint8_t *)backing;
|
||||||
|
backup.sizeBytes = sizeof(backing);
|
||||||
|
|
||||||
|
// each frame:
|
||||||
|
jlSpriteRestoreUnder(stage, &backup); // erase last frame's sprite
|
||||||
|
jlSpriteSaveAndDraw(stage, sprite, x, y, &backup);
|
||||||
|
----
|
||||||
|
|
||||||
|
Size the buffer with the `JOEY_SPRITE_BACKUP_BYTES` macro, and declare it as
|
||||||
|
`uint16_t` (or otherwise 2-byte aligned) -- an odd address pushes save and
|
||||||
|
restore onto slow interpreted paths on the planar ports, because the 68000
|
||||||
|
cannot do word accesses at odd addresses.
|
||||||
|
|
||||||
|
==== The Clean-Buffer Pattern
|
||||||
|
|
||||||
|
Keep a pristine copy of the background, and restore rectangles from it.
|
||||||
|
|
||||||
|
[source,c]
|
||||||
|
----
|
||||||
|
jlSurfaceT *clean = jlSurfaceCreate();
|
||||||
|
// ... draw the static scene into clean, once ...
|
||||||
|
|
||||||
|
// each frame:
|
||||||
|
jlSurfaceCopyRect(stage, clean, x, y, w, h); // erase EVERY window first
|
||||||
|
// ... then draw ALL sprites ...
|
||||||
|
----
|
||||||
|
|
||||||
|
This scales better with many sprites, and it is the right choice when sprites
|
||||||
|
overlap. Erase every window first, then draw every sprite -- never interleave
|
||||||
|
erase and draw.
|
||||||
|
|
||||||
|
[WARNING]
|
||||||
|
====
|
||||||
|
`jlSurfaceCopyRect` widens the copied region outward to the 16-pixel group grid
|
||||||
|
on *every* port, so it can touch up to 15 pixels either side of the rectangle
|
||||||
|
you asked for. That means it can overwrite content just outside your rectangle,
|
||||||
|
and it silently invalidates any live `jlSpriteBackupT` whose recorded window it
|
||||||
|
touches. This is exactly why the two patterns must not be mixed in one region.
|
||||||
|
====
|
||||||
|
|
@ -141,7 +141,7 @@ static unsigned char gBackupBytes[JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__(
|
||||||
|
|
||||||
static jlTileT gTileScratch;
|
static jlTileT gTileScratch;
|
||||||
|
|
||||||
// Phase 0 workload rows (NATIVE-PERF-PLAN.md item 3): patterned tiles
|
// Phase 0 workload rows (claudeDocs/NATIVE-PERF-PLAN.md item 3): patterned tiles
|
||||||
// seeded via draw+snap (tile pixels are platform-private; only a snap
|
// seeded via draw+snap (tile pixels are platform-private; only a snap
|
||||||
// round-trips portably) and dedicated gameFrame sprite backups so the
|
// round-trips portably) and dedicated gameFrame sprite backups so the
|
||||||
// correctness-check gBackup state stays untouched.
|
// correctness-check gBackup state stays untouched.
|
||||||
|
|
@ -338,7 +338,7 @@ static void op_surfaceMarkDirty(void) { /* jlDrawPixel already marks; use fill i
|
||||||
jlFillRect(gStage, 0, 0, 32, 32, 0); }
|
jlFillRect(gStage, 0, 0, 32, 32, 0); }
|
||||||
|
|
||||||
|
|
||||||
// ----- Phase 0 workload ops (NATIVE-PERF-PLAN.md item 3) -----
|
// ----- Phase 0 workload ops (claudeDocs/NATIVE-PERF-PLAN.md item 3) -----
|
||||||
|
|
||||||
// Worst-case unaligned x on every port at once: odd -> IIgs/DOS
|
// Worst-case unaligned x on every port at once: odd -> IIgs/DOS
|
||||||
// compiled shift-1 (widened RMW row); x % 8 == 7 -> ST shift index 2
|
// compiled shift-1 (widened RMW row); x % 8 == 7 -> ST shift index 2
|
||||||
|
|
@ -1387,7 +1387,7 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) {
|
||||||
// Each check draws a deterministic scene and logs the surface hash so
|
// Each check draws a deterministic scene and logs the surface hash so
|
||||||
// tools/diff-uber-hashes can compare ports against each other and against
|
// tools/diff-uber-hashes can compare ports against each other and against
|
||||||
// the frozen golden logs. Several checks intentionally capture the CURRENT
|
// the frozen golden logs. Several checks intentionally capture the CURRENT
|
||||||
// behavior of known bugs (PERF-AUDIT.md #1, #11, #12, #72): their hash
|
// behavior of known bugs (claudeDocs/PERF-AUDIT.md #1, #11, #12, #72): their hash
|
||||||
// lines are EXPECTED to change when the Phase 1 fixes land -- re-golden
|
// lines are EXPECTED to change when the Phase 1 fixes land -- re-golden
|
||||||
// exactly those lines then, nothing else.
|
// exactly those lines then, nothing else.
|
||||||
//
|
//
|
||||||
|
|
@ -1729,7 +1729,7 @@ static void __attribute__((noinline)) runAllTests(void) {
|
||||||
// Surface mark dirty (via jlFillRect's mark step).
|
// Surface mark dirty (via jlFillRect's mark step).
|
||||||
timeOp("surfaceMarkDirtyRect (via jlFillRect 32x32)", op_surfaceMarkDirty);
|
timeOp("surfaceMarkDirtyRect (via jlFillRect 32x32)", op_surfaceMarkDirty);
|
||||||
|
|
||||||
// ----- Phase 0 workload rows (NATIVE-PERF-PLAN.md item 3). Appended
|
// ----- Phase 0 workload rows (claudeDocs/NATIVE-PERF-PLAN.md item 3). Appended
|
||||||
// last on purpose: timed hashes are cumulative and the correctness
|
// last on purpose: timed hashes are cumulative and the correctness
|
||||||
// checks clear the stage first, so the frozen lines above stay
|
// checks clear the stage first, so the frozen lines above stay
|
||||||
// bit-identical (Phase-7 golden-extension precedent).
|
// bit-identical (Phase-7 golden-extension precedent).
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ void jlInputPoll(void);
|
||||||
// the keyboard layout are already applied by the backend, so
|
// the keyboard layout are already applied by the backend, so
|
||||||
// punctuation and shifted symbols arrive correctly on every port.
|
// punctuation and shifted symbols arrive correctly on every port.
|
||||||
// (The layout is the machine's own on IIgs/Amiga/ST/X68000; the DOS
|
// (The layout is the machine's own on IIgs/Amiga/ST/X68000; the DOS
|
||||||
// port translates with a fixed US layout -- see docs/input.md.)
|
// port translates with a fixed US layout -- see claudeDocs/input.md.)
|
||||||
//
|
//
|
||||||
// The queue is refilled by jlInputPoll() -- no extra bring-up is
|
// The queue is refilled by jlInputPoll() -- no extra bring-up is
|
||||||
// needed. It holds JL_CHAR_QUEUE_SIZE - 1 characters; when full,
|
// needed. It holds JL_CHAR_QUEUE_SIZE - 1 characters; when full,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
// Colors are 12-bit $0RGB (bits 11..8 red, 7..4 green, 3..0 blue).
|
// Colors are 12-bit $0RGB (bits 11..8 red, 7..4 green, 3..0 blue).
|
||||||
// The library forces color 0 of every palette to black; jlPaletteSet
|
// The library forces color 0 of every palette to black; jlPaletteSet
|
||||||
// silently masks that entry to $000 regardless of what the caller
|
// silently masks that entry to $000 regardless of what the caller
|
||||||
// passed. See docs/DESIGN.md sections 5 and 10.
|
// passed. See claudeDocs/DESIGN.md sections 5 and 10.
|
||||||
|
|
||||||
#ifndef JOEYLIB_PALETTE_H
|
#ifndef JOEYLIB_PALETTE_H
|
||||||
#define JOEYLIB_PALETTE_H
|
#define JOEYLIB_PALETTE_H
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@
|
||||||
// under MAME with the wait-state patch: TVRAM costs 1.92 cycles/access
|
// under MAME with the wait-state patch: TVRAM costs 1.92 cycles/access
|
||||||
// against GVRAM's 1.03, but needs only 0.25 word-writes per 16-colour pixel
|
// against GVRAM's 1.03, but needs only 0.25 word-writes per 16-colour pixel
|
||||||
// against GVRAM's 1.0 -- so the text plane still wins ~2x. See
|
// against GVRAM's 1.0 -- so the text plane still wins ~2x. See
|
||||||
// docs/x68000_mame_crtc_timing.md. Do not switch to JOEYLIB_NATIVE_PLANAR
|
// claudeDocs/x68000_mame_crtc_timing.md. Do not switch to JOEYLIB_NATIVE_PLANAR
|
||||||
// until that is settled and measured.
|
// until that is settled and measured.
|
||||||
#define JOEYLIB_CPU_68000
|
#define JOEYLIB_CPU_68000
|
||||||
#define JOEYLIB_ENDIAN_BIG
|
#define JOEYLIB_ENDIAN_BIG
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
// planar platforms (Amiga, Atari ST) this is a chunky-to-planar
|
// planar platforms (Amiga, Atari ST) this is a chunky-to-planar
|
||||||
// conversion. Drawing primitives mark per-row dirty ranges on the
|
// conversion. Drawing primitives mark per-row dirty ranges on the
|
||||||
// stage as a side effect, so jlStagePresent only touches rows that
|
// stage as a side effect, so jlStagePresent only touches rows that
|
||||||
// actually changed since the last present. See docs/DESIGN.md.
|
// actually changed since the last present. See claudeDocs/DESIGN.md.
|
||||||
|
|
||||||
#ifndef JOEYLIB_PRESENT_H
|
#ifndef JOEYLIB_PRESENT_H
|
||||||
#define JOEYLIB_PRESENT_H
|
#define JOEYLIB_PRESENT_H
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
// variants into the codegen arena and the draw/save/restore calls
|
// variants into the codegen arena and the draw/save/restore calls
|
||||||
// dispatch to the compiled routines, falling back to the interpreter
|
// dispatch to the compiled routines, falling back to the interpreter
|
||||||
// for clipped draws or uncompiled variants. On the IIgs the compiled
|
// for clipped draws or uncompiled variants. On the IIgs the compiled
|
||||||
// path is temporarily toolchain-gated off (PERF-AUDIT.md #77).
|
// path is temporarily toolchain-gated off (claudeDocs/PERF-AUDIT.md #77).
|
||||||
// jlSpritePrewarm compiles ahead of the first draw so the emit cost
|
// jlSpritePrewarm compiles ahead of the first draw so the emit cost
|
||||||
// does not land mid-frame.
|
// does not land mid-frame.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
// regardless, so game code does not see the storage format.
|
// regardless, so game code does not see the storage format.
|
||||||
//
|
//
|
||||||
// Each surface carries its own 200-entry SCB (scanline control byte)
|
// Each surface carries its own 200-entry SCB (scanline control byte)
|
||||||
// table and a 16-by-16 $0RGB palette set. See docs/DESIGN.md section 6.
|
// table and a 16-by-16 $0RGB palette set. See claudeDocs/DESIGN.md section 6.
|
||||||
|
|
||||||
#ifndef JOEYLIB_SURFACE_H
|
#ifndef JOEYLIB_SURFACE_H
|
||||||
#define JOEYLIB_SURFACE_H
|
#define JOEYLIB_SURFACE_H
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
#
|
#
|
||||||
# BRING-UP STATE. This builds libjoey.a and the serial example against the
|
# BRING-UP STATE. This builds libjoey.a and the serial example against the
|
||||||
# generic backend -- no native primitives, no audio, no sprite codegen yet.
|
# generic backend -- no native primitives, no audio, no sprite codegen yet.
|
||||||
# See docs/x68000_port.md for what is and is not real.
|
# See claudeDocs/x68000_port.md for what is and is not real.
|
||||||
#
|
#
|
||||||
# The toolchain is elf2x68k (BSD-3, gcc 13.4.0 / binutils / newlib), staged into
|
# The toolchain is elf2x68k (BSD-3, gcc 13.4.0 / binutils / newlib), staged into
|
||||||
# toolchains/x68000 by the tarball. m68k-xelf-gcc emits ELF; the driver's
|
# toolchains/x68000 by the tarball. m68k-xelf-gcc emits ELF; the driver's
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
# kills DOSBox. tools/uber-perf-table reads build/dos/bin/JOEYLOG.TXT.
|
# kills DOSBox. tools/uber-perf-table reads build/dos/bin/JOEYLOG.TXT.
|
||||||
#
|
#
|
||||||
# Uses the same dosbox-386sx16.conf as run-dos.sh so the cycle budget
|
# Uses the same dosbox-386sx16.conf as run-dos.sh so the cycle budget
|
||||||
# matches the PERF.md reference machine.
|
# matches the claudeDocs/PERF.md reference machine.
|
||||||
#
|
#
|
||||||
# scripts/bench-dos.sh # default 90s timeout
|
# scripts/bench-dos.sh # default 90s timeout
|
||||||
# TIMEOUT=120 scripts/bench-dos.sh # override
|
# TIMEOUT=120 scripts/bench-dos.sh # override
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# bench-iigs.sh - Headless UBER perf capture for the IIgs (the reference
|
# bench-iigs.sh - Headless UBER perf capture for the IIgs (the reference
|
||||||
# port). Until this script existed, PERF.md's IIgs column was carried over
|
# port). Until this script existed, claudeDocs/PERF.md's IIgs column was carried over
|
||||||
# from old captures because nothing could get joeylog.txt off the emulated
|
# from old captures because nothing could get joeylog.txt off the emulated
|
||||||
# disk; every "hashes identical x4" gate needs this.
|
# disk; every "hashes identical x4" gate needs this.
|
||||||
#
|
#
|
||||||
|
|
|
||||||
57
scripts/build-docs.sh
Executable file
57
scripts/build-docs.sh
Executable file
|
|
@ -0,0 +1,57 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# build-docs.sh - render the JoeyLib manual to HTML and PDF.
|
||||||
|
#
|
||||||
|
# Sources are docs/Manual.adoc plus the chapter files it includes, all in
|
||||||
|
# docs/. Output goes to build/docs/, which is gitignored along with the rest
|
||||||
|
# of build/ -- the .adoc sources are the tracked deliverable, the rendered
|
||||||
|
# books are regenerated on demand.
|
||||||
|
#
|
||||||
|
# The HTML is built with -a data-uri so pictures are embedded and the page is
|
||||||
|
# a single self-contained file that can be copied anywhere without an images
|
||||||
|
# folder beside it.
|
||||||
|
#
|
||||||
|
# Usage: scripts/build-docs.sh [html|pdf]
|
||||||
|
# with no argument, both.
|
||||||
|
# Requires: asciidoctor and asciidoctor-pdf
|
||||||
|
# gem install asciidoctor asciidoctor-pdf rouge
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
src=$repo/docs/Manual.adoc
|
||||||
|
out=$repo/build/docs
|
||||||
|
what=${1:-both}
|
||||||
|
|
||||||
|
case "$what" in
|
||||||
|
html|pdf|both) ;;
|
||||||
|
*) echo "usage: $0 [html|pdf]" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -f "$src" ] || { echo "build-docs: missing $src" >&2; exit 2; }
|
||||||
|
|
||||||
|
need=(asciidoctor)
|
||||||
|
[ "$what" = "html" ] || need+=(asciidoctor-pdf)
|
||||||
|
for tool in "${need[@]}"; do
|
||||||
|
command -v "$tool" >/dev/null || {
|
||||||
|
echo "build-docs: $tool not found (gem install asciidoctor asciidoctor-pdf rouge)" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p "$out"
|
||||||
|
|
||||||
|
# -a allow-uri-read lets the SVG cover image be read from assets/.
|
||||||
|
common=(-a "revdate=$(date +%Y-%m-%d)" -a allow-uri-read)
|
||||||
|
|
||||||
|
if [ "$what" = "html" ] || [ "$what" = "both" ]; then
|
||||||
|
asciidoctor "${common[@]}" -a data-uri \
|
||||||
|
"$src" -o "$out/JoeyLib-Manual.html"
|
||||||
|
echo "build-docs: $out/JoeyLib-Manual.html"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$what" = "pdf" ] || [ "$what" = "both" ]; then
|
||||||
|
asciidoctor-pdf "${common[@]}" \
|
||||||
|
"$src" -o "$out/JoeyLib-Manual.pdf"
|
||||||
|
echo "build-docs: $out/JoeyLib-Manual.pdf"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ls -la "$out"
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
#
|
#
|
||||||
# EXPECTED TO FAIL until the Phase 1 #23 fix lands (the clock runs
|
# EXPECTED TO FAIL until the Phase 1 #23 fix lands (the clock runs
|
||||||
# backward at the 65536-frame wrap); after that it must PASS and becomes
|
# backward at the 65536-frame wrap); after that it must PASS and becomes
|
||||||
# the regression gate. See PERF-AUDIT-PLAN.md Phase 0/Phase 1.
|
# the regression gate. See claudeDocs/PERF-AUDIT-PLAN.md Phase 0/Phase 1.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
|
|
||||||
53
scripts/pulseRecordLib.sh
Normal file
53
scripts/pulseRecordLib.sh
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# pulseRecordLib.sh - shared PulseAudio capture helpers. SOURCE, do not run.
|
||||||
|
#
|
||||||
|
# Used by verify-amiga-audio.sh and verify-dos-audio.sh, the two gates that
|
||||||
|
# grade recorded sound because Paula and the Sound Blaster cannot be tapped
|
||||||
|
# register-by-register under fs-uae and DOSBox.
|
||||||
|
#
|
||||||
|
# The recording scheme in both: create a null sink for THIS run, point only
|
||||||
|
# the emulator at it with PULSE_SINK, and record that sink's monitor with
|
||||||
|
# parec. The host's default sink is never touched, so the gates are safe to
|
||||||
|
# run on a desktop that is playing something else, and two gates can never
|
||||||
|
# collide because the sink name carries the pid.
|
||||||
|
|
||||||
|
# pulseEnsureServer -- locate the audio server, or fail with a real reason.
|
||||||
|
#
|
||||||
|
# A sanitised environment (cron, CI, `env -i`, a systemd unit without the
|
||||||
|
# user session) has no XDG_RUNTIME_DIR, and pactl then reports "no server"
|
||||||
|
# even though one is running -- measured: `env -i make verify-audio` skipped
|
||||||
|
# both recording gates for exactly this. The socket lives at a predictable
|
||||||
|
# path, so derive it before giving up.
|
||||||
|
pulseEnsureServer() {
|
||||||
|
if pactl info >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ -z "${XDG_RUNTIME_DIR:-}" ] && [ -S "/run/user/$(id -u)/pulse/native" ]; then
|
||||||
|
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
|
||||||
|
if pactl info >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -z "${PULSE_SERVER:-}" ] && [ -S "/run/user/$(id -u)/pulse/native" ]; then
|
||||||
|
export PULSE_SERVER="unix:/run/user/$(id -u)/pulse/native"
|
||||||
|
if pactl info >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# pulseCreateSink <name> -- echo the module id of a new null sink.
|
||||||
|
pulseCreateSink() {
|
||||||
|
pactl load-module module-null-sink \
|
||||||
|
sink_name="$1" \
|
||||||
|
sink_properties=device.description="$1" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# pulseUnloadSink <moduleId> -- always call this; a leaked null sink on the
|
||||||
|
# user's audio server is the one lasting side effect these gates can have.
|
||||||
|
pulseUnloadSink() {
|
||||||
|
[ -n "${1:-}" ] && pactl unload-module "$1" 2>/dev/null
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
@ -49,14 +49,18 @@ prog=${1:-Audio}
|
||||||
binDir=$repo/build/amiga/bin
|
binDir=$repo/build/amiga/bin
|
||||||
kickstart=$repo/toolchains/emulators/support/kickstart.rom
|
kickstart=$repo/toolchains/emulators/support/kickstart.rom
|
||||||
grader=$repo/tools/gradeAudioPhases.py
|
grader=$repo/tools/gradeAudioPhases.py
|
||||||
|
pulseLib=$repo/scripts/pulseRecordLib.sh
|
||||||
label=verify-amiga-audio
|
label=verify-amiga-audio
|
||||||
|
|
||||||
[ -f "$kickstart" ] || { echo "$label: missing $kickstart" >&2; exit 2; }
|
[ -f "$kickstart" ] || { echo "$label: missing $kickstart" >&2; exit 2; }
|
||||||
[ -f "$grader" ] || { echo "$label: missing $grader" >&2; exit 2; }
|
[ -f "$grader" ] || { echo "$label: missing $grader" >&2; exit 2; }
|
||||||
|
[ -f "$pulseLib" ] || { echo "$label: missing $pulseLib" >&2; exit 2; }
|
||||||
|
# shellcheck source=scripts/pulseRecordLib.sh
|
||||||
|
. "$pulseLib"
|
||||||
for tool in fs-uae Xvfb xdotool parec pactl python3; do
|
for tool in fs-uae Xvfb xdotool parec pactl python3; do
|
||||||
command -v "$tool" >/dev/null || { echo "$label: $tool not installed" >&2; exit 2; }
|
command -v "$tool" >/dev/null || { echo "$label: $tool not installed" >&2; exit 2; }
|
||||||
done
|
done
|
||||||
pactl info >/dev/null 2>&1 || { echo "$label: no PulseAudio/PipeWire server to record from" >&2; exit 2; }
|
pulseEnsureServer || { echo "$label: no PulseAudio/PipeWire server to record from (tried XDG_RUNTIME_DIR and /run/user/$(id -u)/pulse/native)" >&2; exit 2; }
|
||||||
|
|
||||||
if [ -d "$binDir/$prog" ]; then
|
if [ -d "$binDir/$prog" ]; then
|
||||||
progDir=$binDir/$prog
|
progDir=$binDir/$prog
|
||||||
|
|
@ -80,7 +84,7 @@ cleanup() {
|
||||||
[ -n "$xvfbPid" ] && kill "$xvfbPid" 2>/dev/null
|
[ -n "$xvfbPid" ] && kill "$xvfbPid" 2>/dev/null
|
||||||
# Always unload the sink: leaving null sinks behind on the user's
|
# Always unload the sink: leaving null sinks behind on the user's
|
||||||
# audio server is the one side effect this gate could have.
|
# audio server is the one side effect this gate could have.
|
||||||
[ -n "$sinkModule" ] && pactl unload-module "$sinkModule" 2>/dev/null
|
pulseUnloadSink "$sinkModule"
|
||||||
if [ -n "${AMIGA_AUDIO_KEEP:-}" ]; then
|
if [ -n "${AMIGA_AUDIO_KEEP:-}" ]; then
|
||||||
echo "$label: kept $work" >&2
|
echo "$label: kept $work" >&2
|
||||||
else
|
else
|
||||||
|
|
@ -101,9 +105,7 @@ fi
|
||||||
printf 'stack 32768\n:%s\n' "$prog" > "$work/hd/s/startup-sequence"
|
printf 'stack 32768\n:%s\n' "$prog" > "$work/hd/s/startup-sequence"
|
||||||
|
|
||||||
sinkName="joeyAudioGate$$"
|
sinkName="joeyAudioGate$$"
|
||||||
sinkModule=$(pactl load-module module-null-sink \
|
sinkModule=$(pulseCreateSink "$sinkName") || sinkModule=""
|
||||||
sink_name="$sinkName" \
|
|
||||||
sink_properties=device.description="$sinkName" 2>/dev/null) || sinkModule=""
|
|
||||||
[ -n "$sinkModule" ] || { echo "$label: could not create a null sink to record from" >&2; exit 2; }
|
[ -n "$sinkModule" ] || { echo "$label: could not create a null sink to record from" >&2; exit 2; }
|
||||||
|
|
||||||
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
|
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
|
||||||
|
|
|
||||||
110
scripts/verify-audio-all.sh
Executable file
110
scripts/verify-audio-all.sh
Executable file
|
|
@ -0,0 +1,110 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# verify-audio-all.sh - Run every port's audio gate and summarise.
|
||||||
|
#
|
||||||
|
# Driven by `make verify-audio`. Five gates, two kinds:
|
||||||
|
#
|
||||||
|
# iigs DOC wavetable + sound IRQ vector (register tap)
|
||||||
|
# x68000 YM2151 noise + PPI port C + ADPCM rate (register tap)
|
||||||
|
# atarist YM2149 write trace, with the pc (register tap)
|
||||||
|
# amiga recorded sound, four loudness phases (Paula has no tap)
|
||||||
|
# dos recorded sound, four loudness phases (SB has no tap)
|
||||||
|
#
|
||||||
|
# SEQUENTIAL, deliberately. The two recording gates drive the emulator on
|
||||||
|
# wall-clock sleeps and grade fixed time windows of the capture, so a
|
||||||
|
# second emulator competing for CPU can slide a phase boundary and fail a
|
||||||
|
# gate that is actually fine. Running five emulators at once also makes
|
||||||
|
# the timing-sensitive register gates noisier. The whole set is ~2.5
|
||||||
|
# minutes; parallelism is not worth the flakiness.
|
||||||
|
#
|
||||||
|
# Exit codes, distinguishing "it works" from "it did not run":
|
||||||
|
# 0 every selected gate PASSED
|
||||||
|
# 1 at least one gate FAILED (a real defect)
|
||||||
|
# 3 no failures, but at least one gate SKIPPED for a missing
|
||||||
|
# prerequisite -- an unbuilt port or absent emulator. Non-zero on
|
||||||
|
# purpose: a run that quietly tested nothing must not look green.
|
||||||
|
#
|
||||||
|
# Each gate already reports its own evidence line; this only adds the
|
||||||
|
# roll-up. Gate stdout/stderr is echoed as it happens, so a failure shows
|
||||||
|
# its diagnosis here and not just a verdict.
|
||||||
|
#
|
||||||
|
# Usage: scripts/verify-audio-all.sh [port ...]
|
||||||
|
# with no arguments, all five. Otherwise any of:
|
||||||
|
# iigs x68000 atarist amiga dos
|
||||||
|
# Env: AUDIO_ALL_LOG also append the full transcript to this file
|
||||||
|
# Requires: whatever each selected gate requires; missing ones SKIP.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
label=verify-audio-all
|
||||||
|
|
||||||
|
ALL_PORTS="iigs x68000 atarist amiga dos"
|
||||||
|
ports=${*:-$ALL_PORTS}
|
||||||
|
|
||||||
|
for p in $ports; do
|
||||||
|
case " $ALL_PORTS " in
|
||||||
|
*" $p "*) ;;
|
||||||
|
*) echo "$label: unknown port '$p' (choose from: $ALL_PORTS)" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
gateFor() { # $1 = port -> script + args
|
||||||
|
case $1 in
|
||||||
|
iigs) echo "scripts/verify-iigs-audio.sh staxi" ;;
|
||||||
|
x68000) echo "scripts/verify-x68000-audio.sh" ;;
|
||||||
|
atarist) echo "scripts/verify-atarist-audio.sh" ;;
|
||||||
|
amiga) echo "scripts/verify-amiga-audio.sh" ;;
|
||||||
|
dos) echo "scripts/verify-dos-audio.sh" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
passed=""
|
||||||
|
failed=""
|
||||||
|
skipped=""
|
||||||
|
declare -A elapsed
|
||||||
|
|
||||||
|
for port in $ports; do
|
||||||
|
gate=$(gateFor "$port")
|
||||||
|
echo
|
||||||
|
echo "=== $label: $port ($gate) ==="
|
||||||
|
start=$SECONDS
|
||||||
|
if [ -n "${AUDIO_ALL_LOG:-}" ]; then
|
||||||
|
( cd "$repo" && $gate ) 2>&1 | tee -a "$AUDIO_ALL_LOG"
|
||||||
|
rc=${PIPESTATUS[0]}
|
||||||
|
else
|
||||||
|
( cd "$repo" && $gate )
|
||||||
|
rc=$?
|
||||||
|
fi
|
||||||
|
elapsed[$port]=$((SECONDS - start))
|
||||||
|
case $rc in
|
||||||
|
0) passed="$passed $port" ;;
|
||||||
|
2) skipped="$skipped $port" ;;
|
||||||
|
*) failed="$failed $port" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
count() { set -- $1; echo $#; }
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "================ $label summary ================"
|
||||||
|
for port in $ports; do
|
||||||
|
case " $passed " in *" $port "*) verdict=PASS ;; *)
|
||||||
|
case " $skipped " in *" $port "*) verdict="SKIP (prerequisite missing)" ;; *)
|
||||||
|
verdict=FAIL ;;
|
||||||
|
esac ;;
|
||||||
|
esac
|
||||||
|
printf " %-8s %-28s %3ds\n" "$port" "$verdict" "${elapsed[$port]}"
|
||||||
|
done
|
||||||
|
echo " ----"
|
||||||
|
printf " %d passed, %d failed, %d skipped\n" \
|
||||||
|
"$(count "$passed")" "$(count "$failed")" "$(count "$skipped")"
|
||||||
|
|
||||||
|
if [ -n "$failed" ]; then
|
||||||
|
echo "$label: FAIL -$failed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -n "$skipped" ]; then
|
||||||
|
echo "$label: INCOMPLETE - skipped$skipped (build those ports, then re-run)" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
echo "$label: PASS (all $(count "$passed") audio gates)"
|
||||||
|
exit 0
|
||||||
|
|
@ -47,14 +47,18 @@ prog=${1:-AUDIO}
|
||||||
binDir=$repo/build/dos/bin
|
binDir=$repo/build/dos/bin
|
||||||
conf=$repo/scripts/dosbox-386sx16.conf
|
conf=$repo/scripts/dosbox-386sx16.conf
|
||||||
grader=$repo/tools/gradeAudioPhases.py
|
grader=$repo/tools/gradeAudioPhases.py
|
||||||
|
pulseLib=$repo/scripts/pulseRecordLib.sh
|
||||||
label=verify-dos-audio
|
label=verify-dos-audio
|
||||||
|
|
||||||
[ -f "$grader" ] || { echo "$label: missing $grader" >&2; exit 2; }
|
[ -f "$grader" ] || { echo "$label: missing $grader" >&2; exit 2; }
|
||||||
|
[ -f "$pulseLib" ] || { echo "$label: missing $pulseLib" >&2; exit 2; }
|
||||||
|
# shellcheck source=scripts/pulseRecordLib.sh
|
||||||
|
. "$pulseLib"
|
||||||
[ -f "$conf" ] || { echo "$label: missing $conf" >&2; exit 2; }
|
[ -f "$conf" ] || { echo "$label: missing $conf" >&2; exit 2; }
|
||||||
for tool in dosbox Xvfb xdotool parec pactl python3; do
|
for tool in dosbox Xvfb xdotool parec pactl python3; do
|
||||||
command -v "$tool" >/dev/null || { echo "$label: $tool not installed" >&2; exit 2; }
|
command -v "$tool" >/dev/null || { echo "$label: $tool not installed" >&2; exit 2; }
|
||||||
done
|
done
|
||||||
pactl info >/dev/null 2>&1 || { echo "$label: no PulseAudio/PipeWire server to record from" >&2; exit 2; }
|
pulseEnsureServer || { echo "$label: no PulseAudio/PipeWire server to record from (tried XDG_RUNTIME_DIR and /run/user/$(id -u)/pulse/native)" >&2; exit 2; }
|
||||||
|
|
||||||
# Space Taxi ships as its own folder; everything else is a bare EXE.
|
# Space Taxi ships as its own folder; everything else is a bare EXE.
|
||||||
if [ -f "$binDir/$prog/$prog.EXE" ]; then
|
if [ -f "$binDir/$prog/$prog.EXE" ]; then
|
||||||
|
|
@ -79,7 +83,7 @@ cleanup() {
|
||||||
[ -n "$xvfbPid" ] && kill "$xvfbPid" 2>/dev/null
|
[ -n "$xvfbPid" ] && kill "$xvfbPid" 2>/dev/null
|
||||||
# Always unload the sink: a leaked null sink on the user's audio
|
# Always unload the sink: a leaked null sink on the user's audio
|
||||||
# server is the one side effect this gate could have.
|
# server is the one side effect this gate could have.
|
||||||
[ -n "$sinkModule" ] && pactl unload-module "$sinkModule" 2>/dev/null
|
pulseUnloadSink "$sinkModule"
|
||||||
if [ -n "${DOS_AUDIO_KEEP:-}" ]; then
|
if [ -n "${DOS_AUDIO_KEEP:-}" ]; then
|
||||||
echo "$label: kept $work" >&2
|
echo "$label: kept $work" >&2
|
||||||
else
|
else
|
||||||
|
|
@ -90,9 +94,7 @@ cleanup() {
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
sinkName="joeyDosGate$$"
|
sinkName="joeyDosGate$$"
|
||||||
sinkModule=$(pactl load-module module-null-sink \
|
sinkModule=$(pulseCreateSink "$sinkName") || sinkModule=""
|
||||||
sink_name="$sinkName" \
|
|
||||||
sink_properties=device.description="$sinkName" 2>/dev/null) || sinkModule=""
|
|
||||||
[ -n "$sinkModule" ] || { echo "$label: could not create a null sink to record from" >&2; exit 2; }
|
[ -n "$sinkModule" ] || { echo "$label: could not create a null sink to record from" >&2; exit 2; }
|
||||||
|
|
||||||
Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp >/dev/null 2>&1 &
|
Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp >/dev/null 2>&1 &
|
||||||
|
|
|
||||||
|
|
@ -1210,7 +1210,7 @@ void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
|
||||||
// the rect to the surface, so x/y/w/h are on-surface. The shadow planes
|
// the rect to the surface, so x/y/w/h are on-surface. The shadow planes
|
||||||
// are off-screen, so this is invisible until jlStagePresent.
|
// are off-screen, so this is invisible until jlStagePresent.
|
||||||
//
|
//
|
||||||
// Layout reminder (see docs/amiga_planar.md): each plane word covers 16
|
// Layout reminder (see claudeDocs/amiga_planar.md): each plane word covers 16
|
||||||
// horizontal pixels; bit 15 = leftmost pixel of that word. The blitter
|
// horizontal pixels; bit 15 = leftmost pixel of that word. The blitter
|
||||||
// fills whole words and cuts the partial-pixel left/right edges with its
|
// fills whole words and cuts the partial-pixel left/right edges with its
|
||||||
// hardware first/last-word masks (fwm/lwm), so a single DEST(+C) blit per
|
// hardware first/last-word masks (fwm/lwm), so a single DEST(+C) blit per
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@
|
||||||
* math -- see spriteCompiledDraw + the prologue/epilogue in spriteEmitIigs.c.
|
* math -- see spriteCompiledDraw + the prologue/epilogue in spriteEmitIigs.c.
|
||||||
* Codegen is ON by default; set JOEY_IIGS_SPRITE_CODEGEN to 0 to force the
|
* Codegen is ON by default; set JOEY_IIGS_SPRITE_CODEGEN to 0 to force the
|
||||||
* interpreter. */
|
* interpreter. */
|
||||||
/* PERF-AUDIT.md #77 -- ROOT-CAUSED 2026-07-04, FIXED toolchain-side
|
/* claudeDocs/PERF-AUDIT.md #77 -- ROOT-CAUSED 2026-07-04, FIXED toolchain-side
|
||||||
* 2026-07-06 (LLVM816-ASKS.md item 5, JOEYLIB-REPLY.md): the GS/OS
|
* 2026-07-06 (claudeDocs/LLVM816-ASKS.md item 5, JOEYLIB-REPLY.md): the GS/OS
|
||||||
* loader used to Memory-Manager-claim only each segment's IMAGE bytes,
|
* loader used to Memory-Manager-claim only each segment's IMAGE bytes,
|
||||||
* so the BSS+heap span above the image was MM-free space and the codegen
|
* so the BSS+heap span above the image was MM-free space and the codegen
|
||||||
* arena's NewHandle could land INSIDE the app's own entry-bank BSS
|
* arena's NewHandle could land INSIDE the app's own entry-bank BSS
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,7 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCel
|
||||||
jlpBigFree(buf);
|
jlpBigFree(buf);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Shared init path (spriteInternal.h, PERF-AUDIT.md #32): sets
|
// Shared init path (spriteInternal.h, claudeDocs/PERF-AUDIT.md #32): sets
|
||||||
// the 0xFF/0 compiled-state sentinels and registers the sprite
|
// the 0xFF/0 compiled-state sentinels and registers the sprite
|
||||||
// for shutdown. ownsTileData true: the cel owns its buf copy.
|
// for shutdown. ownsTileData true: the cel owns its buf copy.
|
||||||
spriteInitFields(sp, buf, widthTiles, heightTiles, true);
|
spriteInitFields(sp, buf, widthTiles, heightTiles, true);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
// so far on disk.
|
// so far on disk.
|
||||||
//
|
//
|
||||||
// On the IIgs the per-line-flush design is a triple failure
|
// On the IIgs the per-line-flush design is a triple failure
|
||||||
// (PERF-AUDIT.md #80/#82): the libc fflush does not commit through the
|
// (claudeDocs/PERF-AUDIT.md #80/#82): the libc fflush does not commit through the
|
||||||
// GS/OS FST (nothing persists until fclose), each real FST write costs
|
// GS/OS FST (nothing persists until fclose), each real FST write costs
|
||||||
// seconds of emulated floppy time, and a write can wedge indefinitely
|
// seconds of emulated floppy time, and a write can wedge indefinitely
|
||||||
// inside SmartPort firmware retries -- one such wedge stalled the UBER
|
// inside SmartPort firmware retries -- one such wedge stalled the UBER
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
// Default codegen arena size when jlConfigT.codegenBytes == 0.
|
// Default codegen arena size when jlConfigT.codegenBytes == 0.
|
||||||
// Per-platform because emitter code density differs ~4-8x across CPUs
|
// Per-platform because emitter code density differs ~4-8x across CPUs
|
||||||
// (PERF-AUDIT.md #39):
|
// (claudeDocs/PERF-AUDIT.md #39):
|
||||||
//
|
//
|
||||||
// IIgs (65816): ~3-4 KB per all-opaque 32x32 sprite, so 8 KB fits
|
// IIgs (65816): ~3-4 KB per all-opaque 32x32 sprite, so 8 KB fits
|
||||||
// the typical working set and keeps the attrNoCross Memory Manager
|
// the typical working set and keeps the attrNoCross Memory Manager
|
||||||
|
|
@ -162,7 +162,7 @@ void jlShutdown(void) {
|
||||||
// Reset compiled-sprite state (slot pointers, routine offsets, patch
|
// Reset compiled-sprite state (slot pointers, routine offsets, patch
|
||||||
// caches) in every registered sprite BEFORE the arena teardown frees
|
// caches) in every registered sprite BEFORE the arena teardown frees
|
||||||
// the slot structs -- see spriteSystemShutdown in spriteInternal.h
|
// the slot structs -- see spriteSystemShutdown in spriteInternal.h
|
||||||
// (PERF-AUDIT.md #34).
|
// (claudeDocs/PERF-AUDIT.md #34).
|
||||||
spriteSystemShutdown();
|
spriteSystemShutdown();
|
||||||
codegenArenaShutdown();
|
codegenArenaShutdown();
|
||||||
stageFree();
|
stageFree();
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
// ----- Sprite registry (PERF-AUDIT.md #34) -----
|
// ----- Sprite registry (claudeDocs/PERF-AUDIT.md #34) -----
|
||||||
//
|
//
|
||||||
// jlShutdown frees the codegen arena (codegenArenaShutdown) while
|
// jlShutdown frees the codegen arena (codegenArenaShutdown) while
|
||||||
// user-owned sprites may still hold ArenaSlotT pointers. If such a
|
// user-owned sprites may still hold ArenaSlotT pointers. If such a
|
||||||
|
|
@ -524,7 +524,7 @@ jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t h
|
||||||
}
|
}
|
||||||
/* jlpBigAlloc, NOT the C heap: the IIgs C heap is a few hundred
|
/* jlpBigAlloc, NOT the C heap: the IIgs C heap is a few hundred
|
||||||
* bytes of unprotected bank-0 leftover; at exhaustion its malloc
|
* bytes of unprotected bank-0 leftover; at exhaustion its malloc
|
||||||
* returns garbage instead of NULL (PERF-AUDIT.md #81). Other ports
|
* returns garbage instead of NULL (claudeDocs/PERF-AUDIT.md #81). Other ports
|
||||||
* map jlpBigAlloc to malloc. Applies to every sprite-struct and
|
* map jlpBigAlloc to malloc. Applies to every sprite-struct and
|
||||||
* owned-tileData allocation in this file (finding #31). */
|
* owned-tileData allocation in this file (finding #31). */
|
||||||
sp = (jlSpriteT *)jlpBigAlloc((uint32_t)sizeof(jlSpriteT));
|
sp = (jlSpriteT *)jlpBigAlloc((uint32_t)sizeof(jlSpriteT));
|
||||||
|
|
@ -1330,7 +1330,7 @@ void jlSpriteSaveUnderTrusted(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int
|
||||||
// init path for every sprite: jlSpriteCreate, jlSpriteCreateFromSurface,
|
// init path for every sprite: jlSpriteCreate, jlSpriteCreateFromSurface,
|
||||||
// and jlSpriteBankLoad (assetLoad.c) all route through here so the
|
// and jlSpriteBankLoad (assetLoad.c) all route through here so the
|
||||||
// 0xFF/0 sentinel rules in spriteResetCompiledState have one source
|
// 0xFF/0 sentinel rules in spriteResetCompiledState have one source
|
||||||
// of truth (PERF-AUDIT.md #32).
|
// of truth (claudeDocs/PERF-AUDIT.md #32).
|
||||||
void spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData) {
|
void spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData) {
|
||||||
sp->tileData = tileData;
|
sp->tileData = tileData;
|
||||||
sp->widthTiles = widthTiles;
|
sp->widthTiles = widthTiles;
|
||||||
|
|
|
||||||
|
|
@ -206,10 +206,10 @@ struct jlSpriteT {
|
||||||
// init path for every sprite -- jlSpriteCreate and
|
// init path for every sprite -- jlSpriteCreate and
|
||||||
// jlSpriteCreateFromSurface in sprite.c plus jlSpriteBankLoad in
|
// jlSpriteCreateFromSurface in sprite.c plus jlSpriteBankLoad in
|
||||||
// assetLoad.c -- so the sentinel rules live in one place
|
// assetLoad.c -- so the sentinel rules live in one place
|
||||||
// (PERF-AUDIT.md #32). Defined in sprite.c.
|
// (claudeDocs/PERF-AUDIT.md #32). Defined in sprite.c.
|
||||||
void spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData);
|
void spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData);
|
||||||
|
|
||||||
// Sprite registry (sprite.c, PERF-AUDIT.md #34). spriteInitFields
|
// Sprite registry (sprite.c, claudeDocs/PERF-AUDIT.md #34). spriteInitFields
|
||||||
// registers every sprite it initializes; jlSpriteCompile re-registers
|
// registers every sprite it initializes; jlSpriteCompile re-registers
|
||||||
// on success (idempotent) as a belt-and-braces guard, since compile
|
// on success (idempotent) as a belt-and-braces guard, since compile
|
||||||
// is the moment the sprite first holds an arena slot.
|
// is the moment the sprite first holds an arena slot.
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ jlSurfaceT *jlSurfaceCreate(void) {
|
||||||
// jlpBigAlloc, NOT the C heap: on IIgs the C heap is a few hundred
|
// jlpBigAlloc, NOT the C heap: on IIgs the C heap is a few hundred
|
||||||
// bytes of bank-0 leftover that is NEITHER big enough for this
|
// bytes of bank-0 leftover that is NEITHER big enough for this
|
||||||
// struct NOR reserved from the Memory Manager (Phase 1 IIgs
|
// struct NOR reserved from the Memory Manager (Phase 1 IIgs
|
||||||
// stabilization; PERF-AUDIT.md #77/#81). Other ports map
|
// stabilization; claudeDocs/PERF-AUDIT.md #77/#81). Other ports map
|
||||||
// jlpBigAlloc to malloc, so behavior is unchanged there.
|
// jlpBigAlloc to malloc, so behavior is unchanged there.
|
||||||
s = (jlSurfaceT *)jlpBigAlloc((uint32_t)sizeof(jlSurfaceT));
|
s = (jlSurfaceT *)jlpBigAlloc((uint32_t)sizeof(jlSurfaceT));
|
||||||
if (s == NULL) {
|
if (s == NULL) {
|
||||||
|
|
|
||||||
|
|
@ -389,7 +389,7 @@ void jlpWaitVBL(void) {
|
||||||
// old implementation edge-polled $3DA bit 3 and only advanced when a
|
// old implementation edge-polled $3DA bit 3 and only advanced when a
|
||||||
// call happened to land DURING the ~1ms retrace pulse, so any caller
|
// call happened to land DURING the ~1ms retrace pulse, so any caller
|
||||||
// doing more than ~1ms of work between polls silently lost frames
|
// doing more than ~1ms of work between polls silently lost frames
|
||||||
// (PERF-AUDIT.md finding #73 -- UBER's batched timing loop stalled on
|
// (claudeDocs/PERF-AUDIT.md finding #73 -- UBER's batched timing loop stalled on
|
||||||
// it outright). This form is poll-rate-independent and monotonic;
|
// it outright). This form is poll-rate-independent and monotonic;
|
||||||
// jlpWaitVBL above still synchronizes to the real retrace.
|
// jlpWaitVBL above still synchronizes to the real retrace.
|
||||||
uint16_t jlpFrameCount(void) {
|
uint16_t jlpFrameCount(void) {
|
||||||
|
|
|
||||||
|
|
@ -480,7 +480,7 @@ void jlpShutdown(void) {
|
||||||
// post-main exit path is broken (a php/rtl thunk at segment offset
|
// post-main exit path is broken (a php/rtl thunk at segment offset
|
||||||
// $0072 mis-balances the stack and the machine COPs to the monitor
|
// $0072 mis-balances the stack and the machine COPs to the monitor
|
||||||
// at $00:0002 -- reproduced identically for KEYS and STAXI, see
|
// at $00:0002 -- reproduced identically for KEYS and STAXI, see
|
||||||
// LLVM816-ASKS.md #6). jlShutdown is the last thing every example
|
// claudeDocs/LLVM816-ASKS.md #6). jlShutdown is the last thing every example
|
||||||
// calls, so ending the process here is equivalent and safe; on
|
// calls, so ending the process here is equivalent and safe; on
|
||||||
// IIgs jlShutdown therefore does not return. If QuitGS itself
|
// IIgs jlShutdown therefore does not return. If QuitGS itself
|
||||||
// errors, fall through to the old (crashing) path rather than
|
// errors, fall through to the old (crashing) path rather than
|
||||||
|
|
@ -504,7 +504,7 @@ void jlpShutdown(void) {
|
||||||
// Memory Manager reservation for the pinned framebuffer. Without it the
|
// Memory Manager reservation for the pinned framebuffer. Without it the
|
||||||
// MM considers $01:2000-$9CFF free and can place ANY handle inside the
|
// MM considers $01:2000-$9CFF free and can place ANY handle inside the
|
||||||
// display -- jlpBigAlloc'd sprite/surface structs landed there and were
|
// display -- jlpBigAlloc'd sprite/surface structs landed there and were
|
||||||
// painted over by surface clears (PERF-AUDIT.md #85, the root of the
|
// painted over by surface clears (claudeDocs/PERF-AUDIT.md #85, the root of the
|
||||||
// #84 sprite pathology). attrAddr claims the exact address; the range
|
// #84 sprite pathology). attrAddr claims the exact address; the range
|
||||||
// is in special memory (bank $01) by definition. The claim MUST happen
|
// is in special memory (bank $01) by definition. The claim MUST happen
|
||||||
// in jlpInit, BEFORE the first jlpBigAlloc: stageAlloc allocates the
|
// in jlpInit, BEFORE the first jlpBigAlloc: stageAlloc allocates the
|
||||||
|
|
@ -517,7 +517,7 @@ static void *gStagePixelsHandle = NULL;
|
||||||
// pointer-scratch symbols -- the [pix],y pattern reads/writes bank 0 at the
|
// pointer-scratch symbols -- the [pix],y pattern reads/writes bank 0 at the
|
||||||
// symbol's OFFSET) plus the link-time C heap window. If an MM handle
|
// symbol's OFFSET) plus the link-time C heap window. If an MM handle
|
||||||
// (codegen arena, jlpBigAlloc scratch, a GS/OS file buffer) lands there,
|
// (codegen arena, jlpBigAlloc scratch, a GS/OS file buffer) lands there,
|
||||||
// every draw primitive plots through stomped pointers (PERF-AUDIT.md #88,
|
// every draw primitive plots through stomped pointers (claudeDocs/PERF-AUDIT.md #88,
|
||||||
// the codegen-on showcase hang).
|
// the codegen-on showcase hang).
|
||||||
// The span is COMPUTED from the link symbols, not hard-coded: the old
|
// The span is COMPUTED from the link symbols, not hard-coded: the old
|
||||||
// $B900-$BEFF constant was the DEMO layout's top slice, and the bigger
|
// $B900-$BEFF constant was the DEMO layout's top slice, and the bigger
|
||||||
|
|
@ -631,7 +631,7 @@ void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint
|
||||||
// Block until the next VBL boundary by waiting for the heartbeat tick
|
// Block until the next VBL boundary by waiting for the heartbeat tick
|
||||||
// (incremented by the VBL interrupt handler) to advance. The
|
// (incremented by the VBL interrupt handler) to advance. The
|
||||||
// old form polled $C019's VBL status bit, which never reads high under
|
// old form polled $C019's VBL status bit, which never reads high under
|
||||||
// MAME's apple2gs (PERF-AUDIT.md finding #76): the wait-for-scan loop
|
// MAME's apple2gs (claudeDocs/PERF-AUDIT.md finding #76): the wait-for-scan loop
|
||||||
// spun forever, hanging every program that paces with jlWaitVBL. UBER
|
// spun forever, hanging every program that paces with jlWaitVBL. UBER
|
||||||
// is the only example that calls it, so no verification gate ever
|
// is the only example that calls it, so no verification gate ever
|
||||||
// caught this. Tick granularity IS the VBL boundary, so semantics are
|
// caught this. Tick granularity IS the VBL boundary, so semantics are
|
||||||
|
|
|
||||||
|
|
@ -3576,7 +3576,7 @@ dlnPlotOdd:
|
||||||
; of A, so $FFF0 clears the target's low nibble and preserves the whole
|
; of A, so $FFF0 clears the target's low nibble and preserves the whole
|
||||||
; adjacent byte at Y+1; OR sets nib into the low nibble. (The old $F0FF
|
; adjacent byte at Y+1; OR sets nib into the low nibble. (The old $F0FF
|
||||||
; had the bytes swapped: it never cleared the target nibble and erased
|
; had the bytes swapped: it never cleared the target nibble and erased
|
||||||
; the neighbor's low nibble -- PERF-AUDIT.md #87.)
|
; the neighbor's low nibble -- claudeDocs/PERF-AUDIT.md #87.)
|
||||||
lda [pix],y
|
lda [pix],y
|
||||||
and #0xFFF0
|
and #0xFFF0
|
||||||
ora dlnNibLo
|
ora dlnNibLo
|
||||||
|
|
@ -3825,7 +3825,7 @@ dcLoopBody:
|
||||||
; target's high nibble for even x, $FFF0 clears the target's low
|
; target's high nibble for even x, $FFF0 clears the target's low
|
||||||
; nibble for odd x -- both preserve the whole adjacent byte), then
|
; nibble for odd x -- both preserve the whole adjacent byte), then
|
||||||
; writes both bytes back. (The old odd mask $F0FF had its bytes
|
; writes both bytes back. (The old odd mask $F0FF had its bytes
|
||||||
; swapped and corrupted the neighbor -- PERF-AUDIT.md #87.)
|
; swapped and corrupted the neighbor -- claudeDocs/PERF-AUDIT.md #87.)
|
||||||
|
|
||||||
; Octants 1-4: y-row pair (cx +/- x, cy +/- y).
|
; Octants 1-4: y-row pair (cx +/- x, cy +/- y).
|
||||||
; Plot 1: (cx+x, cy+y)
|
; Plot 1: (cx+x, cy+y)
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
// VERIFIED UNDER EMULATION (MAME 0.264 + the patches in patches/): this path
|
// VERIFIED UNDER EMULATION (MAME 0.264 + the patches in patches/): this path
|
||||||
// renders, and UBER's captured hashes are byte-identical to the Apple IIgs
|
// renders, and UBER's captured hashes are byte-identical to the Apple IIgs
|
||||||
// golden reference. Input, serial and audio are separately proven -- see
|
// golden reference. Input, serial and audio are separately proven -- see
|
||||||
// docs/x68000_port.md for the evidence for each.
|
// claudeDocs/x68000_port.md for the evidence for each.
|
||||||
|
|
||||||
#include <x68k/dos.h>
|
#include <x68k/dos.h>
|
||||||
#include <x68k/iocs.h>
|
#include <x68k/iocs.h>
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
|
||||||
// the rect to the surface, so x/y/w/h are on-surface. The shadow planes
|
// the rect to the surface, so x/y/w/h are on-surface. The shadow planes
|
||||||
// are off-screen, so this is invisible until jlStagePresent.
|
// are off-screen, so this is invisible until jlStagePresent.
|
||||||
//
|
//
|
||||||
// Layout reminder (see docs/amiga_planar.md): each plane word covers 16
|
// Layout reminder (see claudeDocs/amiga_planar.md): each plane word covers 16
|
||||||
// horizontal pixels; bit 15 = leftmost pixel of that word. The blitter
|
// horizontal pixels; bit 15 = leftmost pixel of that word. The blitter
|
||||||
// fills whole words and cuts the partial-pixel left/right edges with its
|
// fills whole words and cuts the partial-pixel left/right edges with its
|
||||||
// hardware first/last-word masks (fwm/lwm), so a single DEST(+C) blit per
|
// hardware first/last-word masks (fwm/lwm), so a single DEST(+C) blit per
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Sharp X68000 per-surface planar storage.
|
// Sharp X68000 per-surface planar storage.
|
||||||
//
|
//
|
||||||
// Step 1 of the storage-model conversion (see docs/x68000_port.md). Nothing
|
// Step 1 of the storage-model conversion (see claudeDocs/x68000_port.md). Nothing
|
||||||
// includes this yet -- the port still ships the chunky-plus-expand path, which
|
// includes this yet -- the port still ships the chunky-plus-expand path, which
|
||||||
// is hash-verified against the Apple IIgs golden reference. This header lands
|
// is hash-verified against the Apple IIgs golden reference. This header lands
|
||||||
// first so the conversion can proceed in verifiable increments instead of one
|
// first so the conversion can proceed in verifiable increments instead of one
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
// 4 planes x 200 rows x 40 bytes = 32,000 bytes per frame, against 128,000
|
// 4 planes x 200 rows x 40 bytes = 32,000 bytes per frame, against 128,000
|
||||||
// bytes of GVRAM word writes on the chunky path. Bus traffic is what the
|
// bytes of GVRAM word writes on the chunky path. Bus traffic is what the
|
||||||
// measured wait states punish (TVRAM 1.92 cycles/access, GVRAM 1.03 -- see
|
// measured wait states punish (TVRAM 1.92 cycles/access, GVRAM 1.03 -- see
|
||||||
// docs/x68000_mame_crtc_timing.md), so a 4x traffic cut beats TVRAM's higher
|
// claudeDocs/x68000_mame_crtc_timing.md), so a 4x traffic cut beats TVRAM's higher
|
||||||
// per-access cost by roughly 2x overall.
|
// per-access cost by roughly 2x overall.
|
||||||
//
|
//
|
||||||
// Unlike the Amiga there is no Chip RAM constraint: planes are plain malloc,
|
// Unlike the Amiga there is no Chip RAM constraint: planes are plain malloc,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,18 @@
|
||||||
|
# UBER reference log for the amiga port, captured 2026-07-07.
|
||||||
|
#
|
||||||
|
# ONLY THE hash= FIELDS ARE AUTHORITATIVE. tools/diff-uber-hashes reads those
|
||||||
|
# and ignores every other line, including these comments.
|
||||||
|
#
|
||||||
|
# The "N iters / N frames = N ops/sec" figures are a dated snapshot of the one
|
||||||
|
# run that captured this file. They are NOT a performance baseline and have
|
||||||
|
# drifted by more than 2x on some ports, so do not compare ports with them.
|
||||||
|
# For real throughput run scripts/bench-<port>.sh and render the comparison
|
||||||
|
# with tools/uber-perf-table, which reads a fresh build/<port> joeylog.txt and
|
||||||
|
# never reads this file.
|
||||||
|
#
|
||||||
|
# Regenerating: only do it deliberately, and only when a pixel change is
|
||||||
|
# intended -- these hashes are what every port is held to. The IIgs is the
|
||||||
|
# reference (DESIGN.md calls it the perf floor).
|
||||||
amiga: shadow plane 0 in CHIP (blitter, addr=$00015280)
|
amiga: shadow plane 0 in CHIP (blitter, addr=$00015280)
|
||||||
amiga: shadow plane 1 in CHIP (blitter, addr=$000171C0)
|
amiga: shadow plane 1 in CHIP (blitter, addr=$000171C0)
|
||||||
amiga: shadow plane 2 in CHIP (blitter, addr=$00019100)
|
amiga: shadow plane 2 in CHIP (blitter, addr=$00019100)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,18 @@
|
||||||
|
# UBER reference log for the atarist port, captured 2026-07-07.
|
||||||
|
#
|
||||||
|
# ONLY THE hash= FIELDS ARE AUTHORITATIVE. tools/diff-uber-hashes reads those
|
||||||
|
# and ignores every other line, including these comments.
|
||||||
|
#
|
||||||
|
# The "N iters / N frames = N ops/sec" figures are a dated snapshot of the one
|
||||||
|
# run that captured this file. They are NOT a performance baseline and have
|
||||||
|
# drifted by more than 2x on some ports, so do not compare ports with them.
|
||||||
|
# For real throughput run scripts/bench-<port>.sh and render the comparison
|
||||||
|
# with tools/uber-perf-table, which reads a fresh build/<port> joeylog.txt and
|
||||||
|
# never reads this file.
|
||||||
|
#
|
||||||
|
# Regenerating: only do it deliberately, and only when a pixel change is
|
||||||
|
# intended -- these hashes are what every port is held to. The IIgs is the
|
||||||
|
# reference (DESIGN.md calls it the perf floor).
|
||||||
UBER: jlSpriteCompile: 1 call in <= 1 frame
|
UBER: jlSpriteCompile: 1 call in <= 1 frame
|
||||||
|
|
||||||
UBER: audioInit OK
|
UBER: audioInit OK
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,18 @@
|
||||||
|
# UBER reference log for the dos port, captured 2026-07-07.
|
||||||
|
#
|
||||||
|
# ONLY THE hash= FIELDS ARE AUTHORITATIVE. tools/diff-uber-hashes reads those
|
||||||
|
# and ignores every other line, including these comments.
|
||||||
|
#
|
||||||
|
# The "N iters / N frames = N ops/sec" figures are a dated snapshot of the one
|
||||||
|
# run that captured this file. They are NOT a performance baseline and have
|
||||||
|
# drifted by more than 2x on some ports, so do not compare ports with them.
|
||||||
|
# For real throughput run scripts/bench-<port>.sh and render the comparison
|
||||||
|
# with tools/uber-perf-table, which reads a fresh build/<port> joeylog.txt and
|
||||||
|
# never reads this file.
|
||||||
|
#
|
||||||
|
# Regenerating: only do it deliberately, and only when a pixel change is
|
||||||
|
# intended -- these hashes are what every port is held to. The IIgs is the
|
||||||
|
# reference (DESIGN.md calls it the perf floor).
|
||||||
UBER: jlSpriteCompile: 1 call in <= 1 frame
|
UBER: jlSpriteCompile: 1 call in <= 1 frame
|
||||||
|
|
||||||
UBER: audioInit OK
|
UBER: audioInit OK
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,18 @@
|
||||||
|
# UBER reference log for the iigs port, captured 2026-07-07.
|
||||||
|
#
|
||||||
|
# ONLY THE hash= FIELDS ARE AUTHORITATIVE. tools/diff-uber-hashes reads those
|
||||||
|
# and ignores every other line, including these comments.
|
||||||
|
#
|
||||||
|
# The "N iters / N frames = N ops/sec" figures are a dated snapshot of the one
|
||||||
|
# run that captured this file. They are NOT a performance baseline and have
|
||||||
|
# drifted by more than 2x on some ports, so do not compare ports with them.
|
||||||
|
# For real throughput run scripts/bench-<port>.sh and render the comparison
|
||||||
|
# with tools/uber-perf-table, which reads a fresh build/<port> joeylog.txt and
|
||||||
|
# never reads this file.
|
||||||
|
#
|
||||||
|
# Regenerating: only do it deliberately, and only when a pixel change is
|
||||||
|
# intended -- these hashes are what every port is held to. The IIgs is the
|
||||||
|
# reference (DESIGN.md calls it the perf floor).
|
||||||
UBER: jlSpriteCompile: 1 call in <= 1 frame
|
UBER: jlSpriteCompile: 1 call in <= 1 frame
|
||||||
UBER: audioInit OK
|
UBER: audioInit OK
|
||||||
UBER: showcase cells: 0=pixels 1=lineH 2=lineV 3=diag 4=rect 5=fillRect 6=circle 7=fillCircle 8=tiles 9=sprite 10=flood 11=scene
|
UBER: showcase cells: 0=pixels 1=lineH 2=lineV 3=diag 4=rect 5=fillRect 6=circle 7=fillCircle 8=tiles 9=sprite 10=flood 11=scene
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
// implementations below instead of the generic stubs.
|
// implementations below instead of the generic stubs.
|
||||||
//
|
//
|
||||||
// TODAY this harness FAILS: jlpGenericMillisElapsed is a pure function of
|
// TODAY this harness FAILS: jlpGenericMillisElapsed is a pure function of
|
||||||
// the wrapping uint16_t frame count (PERF-AUDIT.md finding #23), so the
|
// the wrapping uint16_t frame count (claudeDocs/PERF-AUDIT.md finding #23), so the
|
||||||
// clock jumps backward every 65536 frames (~18 min at 60Hz). The Phase 1
|
// clock jumps backward every 65536 frames (~18 min at 60Hz). The Phase 1
|
||||||
// fix makes it accumulate deltas; this harness must then PASS and becomes
|
// fix makes it accumulate deltas; this harness must then PASS and becomes
|
||||||
// the regression gate for it.
|
// the regression gate for it.
|
||||||
|
|
|
||||||
173
tools/traceLogo.py
Executable file
173
tools/traceLogo.py
Executable file
|
|
@ -0,0 +1,173 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""traceLogo.py -- trace a flat-colour PNG logo into a layered SVG.
|
||||||
|
|
||||||
|
The JoeyLib logo ships as a 320x200 PNG, which is fine on screen and poor
|
||||||
|
everywhere else: the manual's cover wants it large, and a bitmap that small
|
||||||
|
either pixelates or blurs when scaled up. This produces a resolution-free SVG
|
||||||
|
of the same artwork so it can be set at any size.
|
||||||
|
|
||||||
|
How it works. The art is flat cartoon colour with antialiased edges, so:
|
||||||
|
|
||||||
|
1. k-means the opaque pixels down to `--colors` clusters, which recovers the
|
||||||
|
handful of real colours and throws away the antialiasing gradient,
|
||||||
|
2. build a one-bit mask per colour,
|
||||||
|
3. run each mask through potrace (the pure-Python `potracer`), which fits
|
||||||
|
Bezier curves to the region outlines,
|
||||||
|
4. emit one SVG <path> per colour.
|
||||||
|
|
||||||
|
Layer order matters: the paths are written largest-area first so big shapes sit
|
||||||
|
behind small ones, EXCEPT the darkest colour, which is written last. In cartoon
|
||||||
|
art the darkest colour is the ink outline, and it has to be on top or the fills
|
||||||
|
eat it.
|
||||||
|
|
||||||
|
Transparent pixels are left out of every mask, so the SVG keeps the PNG's
|
||||||
|
transparent background.
|
||||||
|
|
||||||
|
Needs `potracer`, `pillow` and `numpy`. They are not build dependencies -- the
|
||||||
|
checked-in SVG is the deliverable and this script only needs running if the
|
||||||
|
source art changes:
|
||||||
|
|
||||||
|
python3 -m venv /tmp/tracevenv
|
||||||
|
/tmp/tracevenv/bin/pip install potracer pillow numpy
|
||||||
|
/tmp/tracevenv/bin/python tools/traceLogo.py assets/logo.png assets/logo.svg
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import potrace
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def kmeans(samples, k, iterations=24, seed=1):
|
||||||
|
"""Tiny k-means over RGB samples. Returns (centres, labels)."""
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
# Seed the centres on well-separated samples (k-means++ style, simplified).
|
||||||
|
centres = [samples[rng.integers(len(samples))].astype(np.float64)]
|
||||||
|
for _ in range(k - 1):
|
||||||
|
d = np.min([np.sum((samples - c) ** 2, axis=1) for c in centres], axis=0)
|
||||||
|
total = d.sum()
|
||||||
|
if total <= 0:
|
||||||
|
centres.append(samples[rng.integers(len(samples))].astype(np.float64))
|
||||||
|
continue
|
||||||
|
centres.append(samples[np.searchsorted(np.cumsum(d / total),
|
||||||
|
rng.random())].astype(np.float64))
|
||||||
|
centres = np.array(centres)
|
||||||
|
|
||||||
|
labels = np.zeros(len(samples), dtype=np.int32)
|
||||||
|
for _ in range(iterations):
|
||||||
|
dist = ((samples[:, None, :] - centres[None, :, :]) ** 2).sum(axis=2)
|
||||||
|
newLabels = np.argmin(dist, axis=1)
|
||||||
|
if np.array_equal(newLabels, labels):
|
||||||
|
break
|
||||||
|
labels = newLabels
|
||||||
|
for i in range(k):
|
||||||
|
hit = samples[labels == i]
|
||||||
|
if len(hit):
|
||||||
|
centres[i] = hit.mean(axis=0)
|
||||||
|
return centres, labels
|
||||||
|
|
||||||
|
|
||||||
|
def pathData(path):
|
||||||
|
"""potrace path -> SVG 'd'. potracer already emits image-space
|
||||||
|
coordinates (y increasing downward), so no axis flip is applied -- adding
|
||||||
|
one renders the art upside down."""
|
||||||
|
out = []
|
||||||
|
for curve in path:
|
||||||
|
start = curve.start_point
|
||||||
|
out.append(f"M{start.x:.2f},{start.y:.2f}")
|
||||||
|
for seg in curve:
|
||||||
|
if seg.is_corner:
|
||||||
|
out.append(f"L{seg.c.x:.2f},{seg.c.y:.2f}")
|
||||||
|
out.append(f"L{seg.end_point.x:.2f},{seg.end_point.y:.2f}")
|
||||||
|
else:
|
||||||
|
out.append(
|
||||||
|
f"C{seg.c1.x:.2f},{seg.c1.y:.2f}"
|
||||||
|
f" {seg.c2.x:.2f},{seg.c2.y:.2f}"
|
||||||
|
f" {seg.end_point.x:.2f},{seg.end_point.y:.2f}")
|
||||||
|
out.append("Z")
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
ap = argparse.ArgumentParser(description="Trace a flat-colour PNG into SVG.")
|
||||||
|
ap.add_argument("input")
|
||||||
|
ap.add_argument("output")
|
||||||
|
ap.add_argument("--colors", type=int, default=5,
|
||||||
|
help="number of flat colours to recover (default 5)")
|
||||||
|
ap.add_argument("--alpha", type=int, default=128,
|
||||||
|
help="alpha above which a pixel counts as opaque (default 128)")
|
||||||
|
ap.add_argument("--turdsize", type=int, default=2,
|
||||||
|
help="drop traced specks smaller than this many pixels")
|
||||||
|
args = ap.parse_args(argv[1:])
|
||||||
|
|
||||||
|
im = Image.open(args.input).convert("RGBA")
|
||||||
|
width, height = im.size
|
||||||
|
data = np.array(im)
|
||||||
|
rgb = data[:, :, :3].astype(np.float64)
|
||||||
|
opaque = data[:, :, 3] > args.alpha
|
||||||
|
if not opaque.any():
|
||||||
|
print(f"traceLogo: {args.input} is fully transparent", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
centres, labels = kmeans(rgb[opaque], args.colors)
|
||||||
|
|
||||||
|
# Rebuild a full-size label image; -1 means transparent.
|
||||||
|
labelImage = np.full((height, width), -1, dtype=np.int32)
|
||||||
|
labelImage[opaque] = labels
|
||||||
|
|
||||||
|
layers = []
|
||||||
|
for i in range(args.colors):
|
||||||
|
mask = labelImage == i
|
||||||
|
area = int(mask.sum())
|
||||||
|
if area == 0:
|
||||||
|
continue
|
||||||
|
# Two polarity traps, both measured against this logo:
|
||||||
|
# * pass the BOOLEAN mask, not 0/1 bytes -- potracer compares against
|
||||||
|
# blacklevel=0.5, so a 0/1 uint8 array reads as entirely
|
||||||
|
# foreground and traces the whole canvas as one rectangle;
|
||||||
|
# * pass the INVERSE -- potracer treats False as the ink it traces,
|
||||||
|
# so handing it the mask directly traces the background and every
|
||||||
|
# layer comes out as a full-canvas block.
|
||||||
|
bitmap = potrace.Bitmap(~mask)
|
||||||
|
traced = bitmap.trace(turdsize=args.turdsize)
|
||||||
|
r, g, b = (int(round(v)) for v in centres[i])
|
||||||
|
layers.append({
|
||||||
|
"area": area,
|
||||||
|
"color": f"#{r:02X}{g:02X}{b:02X}",
|
||||||
|
"luma": 0.299 * r + 0.587 * g + 0.114 * b,
|
||||||
|
"d": pathData(traced),
|
||||||
|
})
|
||||||
|
|
||||||
|
if not layers:
|
||||||
|
print("traceLogo: nothing to trace", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Largest first so big shapes sit behind, then force the ink (darkest)
|
||||||
|
# layer to the very top.
|
||||||
|
layers.sort(key=lambda l: -l["area"])
|
||||||
|
ink = min(layers, key=lambda l: l["luma"])
|
||||||
|
layers.remove(ink)
|
||||||
|
layers.append(ink)
|
||||||
|
|
||||||
|
with open(args.output, "w") as f:
|
||||||
|
f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
|
||||||
|
f.write(f'<svg xmlns="http://www.w3.org/2000/svg" '
|
||||||
|
f'viewBox="0 0 {width} {height}" width="{width}" '
|
||||||
|
f'height="{height}">\n')
|
||||||
|
f.write(f' <title>{args.input}</title>\n')
|
||||||
|
for layer in layers:
|
||||||
|
f.write(f' <path fill="{layer["color"]}" '
|
||||||
|
f'fill-rule="evenodd" d="{layer["d"]}"/>\n')
|
||||||
|
f.write('</svg>\n')
|
||||||
|
|
||||||
|
print(f"traceLogo: {args.output} -- {len(layers)} colour layers, "
|
||||||
|
f"{width}x{height} viewBox")
|
||||||
|
for layer in sorted(layers, key=lambda l: -l["area"]):
|
||||||
|
print(f" {layer['color']} {layer['area']:6d} px")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
Loading…
Add table
Reference in a new issue