X68k fixes. Documentation updated.
This commit is contained in:
parent
2c54e60824
commit
b8b8c7dd93
6 changed files with 752 additions and 81 deletions
264
PERF.md
264
PERF.md
|
|
@ -5,6 +5,79 @@ same-day captures of all four ports. This supersedes the 2026-07-04
|
||||||
Phase 0 baseline; every phase of the audit fix plan (0-8) has landed,
|
Phase 0 baseline; every phase of the audit fix plan (0-8) has landed,
|
||||||
so these are the numbers the library actually ships with.
|
so these are the numbers the library actually ships with.
|
||||||
|
|
||||||
|
**2026-08-06 update -- Sharp X68000 moved to the 256-colour graphics
|
||||||
|
plane.** The port previously drew native-planar into the TEXT plane, which has a
|
||||||
|
single 16-entry hardware palette and therefore could not honour the SCB. To make
|
||||||
|
SCB per-band palettes work it now presents into the 256-colour GRAPHICS plane
|
||||||
|
(`_iocs_crtmod(13)`), expanding the four 1bpp planes to one byte per pixel in
|
||||||
|
GVRAM with the DOS port's encoding, `(scb[y] << 4) | nibble`, against the
|
||||||
|
256-entry palette at `$E82000`. Storage is UNCHANGED -- still
|
||||||
|
`JOEYLIB_NATIVE_PLANAR`; only the present target moved. No raster interrupt is
|
||||||
|
involved. All 51 golden hashes are unchanged (jlSurfaceHash hashes the surface
|
||||||
|
planes, not GVRAM) and the port stays at 51/51. (The "total wall time is
|
||||||
|
identical at 15945 ms" originally written here was an artifact of the broken
|
||||||
|
frame clock -- see the clock note at the end of this entry.)
|
||||||
|
|
||||||
|
Measured 2026-08-06 on the passing 256-colour run with both clocks fixed.
|
||||||
|
Honest UBER total wall time is **40890 ms (2249 frames)**. Two earlier figures
|
||||||
|
in this entry's history were wrong and are corrected below: 15945 ms came from a
|
||||||
|
broken frame clock, and 108309 ms was real but predated the present rewrite.
|
||||||
|
|
||||||
|
The "pre-LUT" column is the straightforward per-pixel expansion the graphics-plane
|
||||||
|
switch first shipped with; "now" is after the word-store expansion tables.
|
||||||
|
|
||||||
|
| op | pre-LUT | now | IIgs | now / IIgs |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `jlSurfaceClear` | 30 | 106 | 33 | 3.21x |
|
||||||
|
| `jlPaletteSet` | 16175 | 58516 | 1561 | 37.49x |
|
||||||
|
| `jlScbSetRange` | 5946 | 21206 | 163 | 130.10x |
|
||||||
|
| `jlDrawPixel` | 10225 | 36220 | 5576 | 6.50x |
|
||||||
|
| `jlDrawLine H` | 2632 | 9370 | 1322 | 7.09x |
|
||||||
|
| `jlFillRect 16x16` | 687 | 2414 | 785 | 3.08x |
|
||||||
|
| `jlFillRect 80x80` | 84 | 306 | 133 | 2.30x |
|
||||||
|
| `jlFillRect 320x200` | 29 | 103 | 22 | 4.68x |
|
||||||
|
| `jlDrawCircle r=16` | 165 | 591 | 303 | 1.95x |
|
||||||
|
| `jlDrawCircle r=80` | 35 | 125 | 70 | 1.79x |
|
||||||
|
| `jlSpriteDraw` | 2059 | 7397 | 1142 | 6.48x |
|
||||||
|
| `jlSpriteSaveUnder` | 3570 | 12803 | 602 | 21.27x |
|
||||||
|
| `jlTileMapPaste 10x5` | 295 | 1055 | 140 | 7.54x |
|
||||||
|
| `jlStagePresent full` | 392 | 1373 | 73 | 18.81x |
|
||||||
|
| `gameFrame composite` | 2 | 12 | 12 | 1.00x |
|
||||||
|
| `gameFrameClean composite` | 2 | 137 | 20 | 6.85x |
|
||||||
|
|
||||||
|
**All 39 shared ops now meet or exceed the Apple IIgs reference; median 7.54x,
|
||||||
|
slower on none.** The two composite rows are the ones that mattered -- they are
|
||||||
|
the closest thing UBER has to a real game frame, and they were the reason the
|
||||||
|
port missed the "within ~1.5x of the native ceiling" directive at 0.17x and
|
||||||
|
0.10x. `gameFrame composite` is now exactly at parity and `gameFrameClean` is
|
||||||
|
6.85x.
|
||||||
|
|
||||||
|
The fix was NOT a port of DOS's gExpandLut. DOS fuses two ADJACENT output bytes
|
||||||
|
into one 16-bit store (mode 13h pixels are neighbouring bytes); here each pixel
|
||||||
|
is a 16-bit WORD, so its bytes are two apart and that fusion is impossible.
|
||||||
|
Inverting it works: write whole words, and one 32-bit store covers exactly two
|
||||||
|
pixels. The high byte is free because the 256-colour index reads only
|
||||||
|
`$000F | $00F0` (x68k_v.cpp:339-340). Expansion tables indexed by plane and
|
||||||
|
nibble are pre-interleaved into word positions, so four planes OR together with a
|
||||||
|
replicated palBase and the inner loop issues four long stores per eight pixels
|
||||||
|
with no shifting at all -- against roughly 64 operations and eight byte-stores
|
||||||
|
before. Verified three ways, because each layer misses what the others catch:
|
||||||
|
51/51 golden (surface correct), a GVRAM dump under PATTERN.X byte-identical to
|
||||||
|
the pre-rewrite run (pixel encoding correct), and an earlier framebuffer capture
|
||||||
|
(the display actually renders the bands).
|
||||||
|
|
||||||
|
**Two clock bugs had to be fixed before any of these numbers meant anything, and
|
||||||
|
every X68000 figure published before 2026-08-06 was wrong.** The port had no
|
||||||
|
`JL_HAS_MILLIS_ELAPSED` override, so milliseconds derived from the frame counter;
|
||||||
|
and that counter was POLLED (`vdispPoll`), so an op spanning more than a frame
|
||||||
|
lost the V-DISP edges in between. The undercount scaled with the op's cost and
|
||||||
|
cancelled it, making every op report ~10,000 ops/sec regardless of workload --
|
||||||
|
`jlFillRect 320x200` (9735) matched `jlFillRect 16x16` (10505) despite 250x the
|
||||||
|
work. The fix chains Human68k's MFP Timer C, reading its rate from TCDCR/TCDR
|
||||||
|
rather than assuming it, and derives the frame counter from that tick.
|
||||||
|
`UBER-CLK` now agrees frame-vs-millisecond on every row, which it could not
|
||||||
|
before because both figures came from the one broken source.
|
||||||
|
|
||||||
**2026-07-06 update:** the IIgs sprite column is now COMPILED. Finding
|
**2026-07-06 update:** the IIgs sprite column is now COMPILED. Finding
|
||||||
#77 (the loader/segment-BSS + malloc-heap reservation that forced the
|
#77 (the loader/segment-BSS + malloc-heap reservation that forced the
|
||||||
codegen arena into the app's own BSS) was fixed toolchain-side and
|
codegen arena into the app's own BSS) was fixed toolchain-side and
|
||||||
|
|
@ -285,10 +358,11 @@ glue is Phase 2 Step D, still open). The present-row jumps (ST 467,
|
||||||
DOS 1,137) are the dirty-union idle early-out running flag-gated;
|
DOS 1,137) are the dirty-union idle early-out running flag-gated;
|
||||||
gameFrame is the honest present signal.
|
gameFrame is the honest present signal.
|
||||||
|
|
||||||
Correctness statement backing this table: all four ports produce
|
Correctness statement backing this table: all FIVE ports produce
|
||||||
BYTE-IDENTICAL pixels on every measured op and every correctness
|
BYTE-IDENTICAL pixels on every measured op and every correctness
|
||||||
check -- **49/49** surface hashes match the frozen goldens in
|
check -- **51/51** surface hashes match the frozen goldens in
|
||||||
tests/goldens/uber/ (37 timed ops + 12 hashed checks; the PASS/FAIL
|
tests/goldens/uber/ (39 timed ops + 12 hashed checks; the X68000 has no
|
||||||
|
golden of its own and is diffed straight against iigs.txt; the PASS/FAIL
|
||||||
checks -- palette round-trip, PRNG golden sequence, sprite clip
|
checks -- palette round-trip, PRNG golden sequence, sprite clip
|
||||||
round-trip, arena churn, sprite-from-surface -- pass everywhere).
|
round-trip, arena churn, sprite-from-surface -- pass everywhere).
|
||||||
The Phase 0-era divergences (#75 tilePasteMono three-way, #78 ST
|
The Phase 0-era divergences (#75 tilePasteMono three-way, #78 ST
|
||||||
|
|
@ -353,10 +427,137 @@ so the per-window Own/Disown it removes was already negligible -- kept
|
||||||
for the right blitter discipline, not a speedup. Session present3win arc
|
for the right blitter discipline, not a speedup. Session present3win arc
|
||||||
106 -> 128 -> 153 -> 154 = +45%. No known remaining flush lever.
|
106 -> 128 -> 153 -> 154 = +45%. No known remaining flush lever.
|
||||||
|
|
||||||
|
**2026-08-05, FIFTH PORT: Sharp X68000 -- the hashes PASS, the clock
|
||||||
|
does NOT.** src/x68000 + make/x68000.mk (JOEYLIB_PLATFORM_X68000) is on
|
||||||
|
the golden gate: tools/diff-uber-hashes reports OK: 51/51 ops match. The
|
||||||
|
port carries NO frozen golden of its own -- it is diffed straight
|
||||||
|
against tests/goldens/uber/iigs.txt, which it matches exactly. Shape:
|
||||||
|
10 MHz 68000, NATIVE PLANAR storage (4 x 1bpp planes, 40 bytes/row,
|
||||||
|
X68K_PLANE_SIZE 8,000), byte-for-byte the Amiga surface layout -- but
|
||||||
|
the DISPLAY is not the surface. It draws into the TEXT plane (four 1bpp
|
||||||
|
planes at $E00000/$E20000/$E40000/$E60000, 128-byte stride), NOT
|
||||||
|
graphics VRAM, so jlpPresent scatters the compact 40-byte rows into that
|
||||||
|
stride: 32,000 bytes/frame against 128,000 bytes of GVRAM word writes on
|
||||||
|
the chunky path it replaced (src/x68000/x68kPlanar.h:20-33). The 320x200
|
||||||
|
window is CENTRED in a 512x512 mode (_iocs_crtmod 12) at x=96 / y=156,
|
||||||
|
byte-aligned by construction (96/8 = 12). Palette is the 16-entry
|
||||||
|
PCG/text palette at $E82200 -- NOT the $E82000 graphic palette -- in
|
||||||
|
GGGGGRRRRRBBBBBI order, green and red SWAPPED versus RGB555 plus ONE
|
||||||
|
SHARED intensity LSB; and because the text plane has exactly one
|
||||||
|
16-entry palette, per-scanline SCB palettes cannot be honoured (palette
|
||||||
|
0 always wins) -- unlike the ST, which really does emulate per-band SCB
|
||||||
|
palettes from an MFP Timer B ISR, so this port stands alone in ignoring
|
||||||
|
the SCB on screen. [SUPERSEDED 2026-08-06: the port moved to the
|
||||||
|
256-colour graphics plane (_iocs_crtmod 13, palette $E82000) precisely to
|
||||||
|
fix this -- SCB per-band palettes now work, DOS-style. See the
|
||||||
|
2026-08-06 entry at the top. The rest of this paragraph still describes
|
||||||
|
the port accurately.] Sprites COMPILE (shared 68k planar emitter, see the
|
||||||
|
Reading notes). Toolchain is elf2x68k at toolchains/x68000/m68k-xelf
|
||||||
|
(gcc 13.4.0, ELF -- so no leading-underscore asm symbols, and
|
||||||
|
src/m68k/*.s does not link there); disks are built by tools/xdftool.py
|
||||||
|
(Human68k FAT12, 1024-byte sectors, .X executables); the emulator is a
|
||||||
|
patched MAME 0.264 single-driver build (patches/mame-0.264-x68k-*.patch).
|
||||||
|
The only recorded rate-card input so far is from those patches: TVRAM
|
||||||
|
costs ~1.92 extra cycles/access and GVRAM ~1.03 versus main memory,
|
||||||
|
applied only at a 10 MHz CPU clock (docs/x68000_mame_crtc_timing.md:307-309,
|
||||||
|
328) -- which is why the 4x traffic cut to the text plane wins despite
|
||||||
|
TVRAM's higher per-access cost. NO ceilings are derived for this port
|
||||||
|
anywhere in this file, and none should be until the clock is fixed.
|
||||||
|
|
||||||
|
THE COLUMN BELOW IS NOT COMPARABLE TO THE OTHER FOUR. It is recorded raw
|
||||||
|
because the capture is expensive and the log is not kept in-tree, not
|
||||||
|
because the numbers mean what they say -- see measurement artifact 5.
|
||||||
|
Capture route: UBER built with the serial log tee
|
||||||
|
(EXTRA_CFLAGS=-DJOEY_LOG_SERIAL_TEE, which now reaches every port via
|
||||||
|
make/common.mk:26; src/core/debug.c calls jlpLogTee, implemented at
|
||||||
|
src/x68000/hal.c:353) against scripts/x68kSerialPeer.py, which MUST be
|
||||||
|
run as `python3 -u` or a killed peer loses the whole capture to stdio
|
||||||
|
buffering. This is the only route that gets a full log off the machine:
|
||||||
|
the disk route (scripts/verify-x68000-golden.sh) forces -DUBER_FRAMES=1u
|
||||||
|
and cannot produce timings at all -- and in practice cannot produce the
|
||||||
|
log either, because joeylog.txt never commits its size to the Human68k
|
||||||
|
image. Run header as reported: total wall time 15,945 ms, 877 frames
|
||||||
|
@ 55 Hz (both from the suspect clock).
|
||||||
|
|
||||||
|
| Op | X68000 (reported ops/sec) |
|
||||||
|
| --- | --- |
|
||||||
|
| jlSurfaceClear | 8775 |
|
||||||
|
| jlPaletteSet | 14353 |
|
||||||
|
| jlScbSetRange | 8305 |
|
||||||
|
| jlDrawPixel | 8580 |
|
||||||
|
| jlDrawLine H | 8855 |
|
||||||
|
| jlDrawLine V | 8800 |
|
||||||
|
| jlDrawLine diag | 8195 |
|
||||||
|
| jlDrawRect 100x100 | 8470 |
|
||||||
|
| jlDrawCircle r=16 | 8305 |
|
||||||
|
| jlDrawCircle r=80 | 7316 |
|
||||||
|
| jlFillRect 16x16 | 9295 |
|
||||||
|
| jlFillRect 80x80 | 22182 |
|
||||||
|
| jlFillRect 320x200 | 6986 |
|
||||||
|
| jlFillCircle r=40 | 9240 |
|
||||||
|
| jlSamplePixel | 10395 |
|
||||||
|
| jlTileFill | 8195 |
|
||||||
|
| jlTileCopy | 8525 |
|
||||||
|
| jlTileCopyMasked | 8745 |
|
||||||
|
| jlTilePaste | 9405 |
|
||||||
|
| jlTileSnap | 8690 |
|
||||||
|
| jlSpriteSaveUnder | 8580 |
|
||||||
|
| jlSpriteDraw | 8250 |
|
||||||
|
| jlSpriteRestoreUnder | 8886 |
|
||||||
|
| jlSpriteSaveAndDraw | 8305 |
|
||||||
|
| jlStagePresent full | 572 |
|
||||||
|
| jlInputPoll | 8250 |
|
||||||
|
| jlKeyDown | 53006 |
|
||||||
|
| jlKeyPressed | 53611 |
|
||||||
|
| jlMouseX | 125529 |
|
||||||
|
| joeyJoyConnected | 53006 |
|
||||||
|
| jlAudioFrameTick | 23756 |
|
||||||
|
| jlAudioIsPlayingMod | 84472 |
|
||||||
|
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 8965 |
|
||||||
|
| jlSpriteDraw unaligned | 8030 |
|
||||||
|
| jlSpriteDraw sweep16 | 8470 |
|
||||||
|
| jlTilePaste map 10x5 | 8305 |
|
||||||
|
| gameFrame composite | 938 |
|
||||||
|
| jlTileMapPaste 10x5 | 8030 |
|
||||||
|
| gameFrameClean composite | 1245 |
|
||||||
|
|
||||||
|
The tell is in the table itself, no model needed: 26 of the 39 rows land
|
||||||
|
inside a single 7,316-9,405 band -- a 1.29x spread across ops whose real
|
||||||
|
work differs by orders of magnitude. `jlDrawPixel` (8,580), a 100x100
|
||||||
|
rect outline (8,470) and a 16x16 sprite draw (8,250) cannot all cost the
|
||||||
|
same; `jlFillRect 80x80` (22,182) cannot be 2.4x FASTER than `jlFillRect
|
||||||
|
16x16` (9,295); `jlSurfaceClear` (8,775) cannot land within 1.2x of
|
||||||
|
`jlSamplePixel` (10,395). The physics check, in the style of artifact 2:
|
||||||
|
the stage is 4 x 8,000 = 32,000 bytes, so 8,775 clears/sec is 114
|
||||||
|
us/clear = ~1,140 cycles at 10 MHz = 28 bytes PER CYCLE. Even at the ST
|
||||||
|
rate card's write-only fill rate of ~2.2 cyc/B, 32,000 bytes is ~70k
|
||||||
|
cycles = ~7.0 ms = ~140 ops/sec, so that row reads ~60x high. Same for
|
||||||
|
the composite: `gameFrame` at 938/s is 1.07 ms = ~10,700 cycles for a
|
||||||
|
256x96 fill + 16 tile pastes + 3 saveAndDraw + 3 restores + a 96-row
|
||||||
|
band present, when the fill ALONE is 12,288 B x 2.2 = 27k cycles (the
|
||||||
|
very figure this file uses for the ST gameFrame ceiling) -- 2.5x the
|
||||||
|
whole measured frame. Only the cheap rows survive: at ~8 us/call
|
||||||
|
`jlMouseX` (125,529), `jlKeyDown` (53,006) and `jlAudioIsPlayingMod`
|
||||||
|
(84,472) poll the clock far more often than once per frame and read
|
||||||
|
honestly.
|
||||||
|
|
||||||
|
For calibration once the clock is fixed, this port sits between the two
|
||||||
|
68k ports already in the table: a 10 MHz 68000 with NO blitter -- the
|
||||||
|
ST's situation with 25% more clock -- writing a compact 4-plane surface
|
||||||
|
in plain malloc'd main RAM (no Chip RAM constraint, nothing but the CPU
|
||||||
|
ever reads it) and then scattering 32,000 bytes/frame into TVRAM at its
|
||||||
|
~1.92-cycle access penalty. So CPU-bound rows should be expected ABOVE
|
||||||
|
the 8.0 MHz ST roughly on clock ratio, while the present should land
|
||||||
|
between the ST (bus-floor CPU copy, 8 MHz) and the Amiga (7.09 MHz but a
|
||||||
|
blitter and display-DMA contention). None of that is measured yet, and
|
||||||
|
none of it can be until artifact 5 is closed.
|
||||||
|
|
||||||
## Measurement honesty (read before trusting any number)
|
## Measurement honesty (read before trusting any number)
|
||||||
|
|
||||||
Four measurement artifacts have shaped this table's history. Three are
|
Five measurement artifacts have shaped this table's history. Three are
|
||||||
fixed, one is inherent -- know them before comparing rows:
|
fixed, one is inherent, and one -- the X68000 frame clock -- is OPEN and
|
||||||
|
invalidates that port's entire ops/sec column: know them before comparing
|
||||||
|
rows:
|
||||||
|
|
||||||
1. **Per-iteration poll tax -- FIXED (Phase 0).** The pre-audit bench
|
1. **Per-iteration poll tax -- FIXED (Phase 0).** The pre-audit bench
|
||||||
loop called jlFrameCount() between every op; on the IIgs that was a
|
loop called jlFrameCount() between every op; on the IIgs that was a
|
||||||
|
|
@ -383,9 +584,12 @@ fixed, one is inherent -- know them before comparing rows:
|
||||||
Timer-C 200 Hz millis clock (refresh-independent), which ran ~16-22%
|
Timer-C 200 Hz millis clock (refresh-independent), which ran ~16-22%
|
||||||
faster; the Amiga's two clocks agreed exactly (FS-UAE = PAL 50 Hz),
|
faster; the Amiga's two clocks agreed exactly (FS-UAE = PAL 50 Hz),
|
||||||
confirming it was ST-only. Two fixes landed: (a) UBER now computes
|
confirming it was ST-only. Two fixes landed: (a) UBER now computes
|
||||||
ops/sec from `jlMillisElapsed` (refresh-independent on all four
|
ops/sec from `jlMillisElapsed` (its own hardware clock on the ST and
|
||||||
ports; identical to the old formula on IIgs where millis is
|
DOS; derived from the frame counter on the IIgs -- identical to the
|
||||||
GetTick-derived), and (b) ST `jlpFrameHz()` now reads the shifter
|
old formula -- and on the Amiga and X68000, neither of which
|
||||||
|
overrides jlpMillisElapsed; on the X68000 that counter is POLLED, so
|
||||||
|
both UBER clocks carry the same error and the frame-vs-millis
|
||||||
|
cross-check cannot see it -- artifact 5), and (b) ST `jlpFrameHz()` now reads the shifter
|
||||||
resolution + sync-mode registers to return the true 50/60/71 Hz. The
|
resolution + sync-mode registers to return the true 50/60/71 Hz. The
|
||||||
per-op `UBER-CLK:` log line reports frame-vs-millis as a standing
|
per-op `UBER-CLK:` log line reports frame-vs-millis as a standing
|
||||||
cross-check (they now agree). ST cells rose ~17% (e.g. jlSpriteDraw
|
cross-check (they now agree). ST cells rose ~17% (e.g. jlSpriteDraw
|
||||||
|
|
@ -400,6 +604,31 @@ fixed, one is inherent -- know them before comparing rows:
|
||||||
gets the whole CPU. Amiga/DOS/IIgs audited clean for the same bug
|
gets the whole CPU. Amiga/DOS/IIgs audited clean for the same bug
|
||||||
class (~1.8% / ~0.5% / 0% idle cost).
|
class (~1.8% / ~0.5% / 0% idle cost).
|
||||||
|
|
||||||
|
5. **X68000 polled frame clock -- OPEN; that port's whole ops/sec
|
||||||
|
column is invalid.** _iocs_vdispst HANGS the machine when handed a
|
||||||
|
plain C handler, so the V-DISP interrupt is never installed
|
||||||
|
(src/x68000/hal.c:85-92; gVdispInstalled stays false) and
|
||||||
|
jlpFrameCount instead edge-detects the MFP GPIP V-DISP bit each time
|
||||||
|
something calls it (hal.c:310-323). A polled edge detector loses
|
||||||
|
ticks exactly when the work is heavy, and the HAL says so in its own
|
||||||
|
comment: "Measured: 400 audio refills across ~19 frames reported ZERO
|
||||||
|
elapsed frames, because nothing polled in between" (hal.c:275-279).
|
||||||
|
UBER polls once per 16-op batch (uber.c:201-219), so any op whose
|
||||||
|
batch stretches past a V-DISP period makes the counter under-count,
|
||||||
|
and ops/sec = iters * 1000 / millis over-reports by that same factor.
|
||||||
|
The frame-vs-millis cross-check built for artifact 3 cannot catch it:
|
||||||
|
the X68000 has no jlpMillisElapsed override, so millis comes from
|
||||||
|
jlpGenericMillisElapsed, which is computed FROM jlpFrameCount
|
||||||
|
(src/core/port.h:753-758, src/generic/genericPort.c:52-97) -- every
|
||||||
|
UBER-CLK line on that port compares a number with itself, which is
|
||||||
|
why they all agree to within 0.5%. Cheap rows (jlMouseX, jlKeyDown,
|
||||||
|
jlAudioIsPlayingMod) poll often enough to catch every edge and read
|
||||||
|
honestly; everything else is inflated, ~60x on the full-screen rows.
|
||||||
|
THE FIX is recorded in the HAL: an asm thunk for _iocs_vdispst (save
|
||||||
|
d0-d1/a0-a1, call, restore, correct return) so the interrupt owns the
|
||||||
|
counter (hal.c:87-91, 298-300). Until that lands the X68000 gates on
|
||||||
|
HASHES ONLY -- it is a correctness column, not a performance column.
|
||||||
|
|
||||||
## Baseline (2026-07-08): absolute ops/sec per port
|
## Baseline (2026-07-08): absolute ops/sec per port
|
||||||
|
|
||||||
The IIgs is the reference and the perf floor (every other port must
|
The IIgs is the reference and the perf floor (every other port must
|
||||||
|
|
@ -407,7 +636,10 @@ meet or beat it -- project directive). Non-IIgs cells show absolute
|
||||||
ops/sec plus percentage of the IIgs number; sub-100% cells are bolded
|
ops/sec plus percentage of the IIgs number; sub-100% cells are bolded
|
||||||
as below-floor. `tools/uber-perf-table` emits this table directly;
|
as below-floor. `tools/uber-perf-table` emits this table directly;
|
||||||
regenerate after any re-capture (Amiga needs TIMEOUT=600 -- the
|
regenerate after any re-capture (Amiga needs TIMEOUT=600 -- the
|
||||||
default cuts off the final checks). Capture provenance: all four
|
default cuts off the final checks). The tool still knows only the
|
||||||
|
original four ports (its PORTS list, tools/uber-perf-table:40-45) and
|
||||||
|
there is no bench-x68000.sh, so an X68000 column has to be added by
|
||||||
|
hand from a serial-tee capture -- see the 2026-08-05 entry. Capture provenance: all four
|
||||||
ports 2026-07-08 from a full `make clean` rebuild (Phase 6 W1+W2+W3
|
ports 2026-07-08 from a full `make clean` rebuild (Phase 6 W1+W2+W3
|
||||||
landed: fused IIgs sprite/fillRect entries, sprite-mark
|
landed: fused IIgs sprite/fillRect entries, sprite-mark
|
||||||
specialization, per-row/fused presents on Amiga/ST/IIgs, ST movep
|
specialization, per-row/fused presents on Amiga/ST/IIgs, ST movep
|
||||||
|
|
@ -798,13 +1030,19 @@ the real-HW column is the datasheet claim.
|
||||||
|
|
||||||
## Reading notes
|
## Reading notes
|
||||||
|
|
||||||
* **Sprite rows measure COMPILED code on all four ports** since
|
* **Sprite rows measure COMPILED code on all five ports** since
|
||||||
NATIVE-PERF Phase 1 (2026-07-06): the 68k ports JIT all-shift draw
|
NATIVE-PERF Phase 1 (2026-07-06): the 68k ports JIT all-shift draw
|
||||||
variants plus group-window save/restore (previously stubs that fell
|
variants plus group-window save/restore (previously stubs that fell
|
||||||
to interpreters), IIgs codegen re-enabled via finding #77, DOS via
|
to interpreters), IIgs codegen re-enabled via finding #77, DOS via
|
||||||
finding #74. The interpreted walkers remain only as arena-full
|
finding #74. The X68000 joined on the shared 68k PLANAR emitter with
|
||||||
|
no emitter changes -- it takes the AMIGA arm of
|
||||||
|
src/core/spriteDispatch.h (4-plane cdecl routines from
|
||||||
|
src/m68k/spriteEmitPlanar68k.c, one pre-shifted variant per x % 8
|
||||||
|
phase, all 8 compiled), because its surface stride is the Amiga's 40
|
||||||
|
bytes and the display's 128-byte text-plane stride never reaches the
|
||||||
|
emitter. The interpreted walkers remain only as arena-full
|
||||||
degrade paths -- and are proven bit-identical to the compiled code
|
degrade paths -- and are proven bit-identical to the compiled code
|
||||||
by the 49-hash gate.
|
by the 51-hash gate.
|
||||||
* **The below-floor cells are the standing punch list**, and after the
|
* **The below-floor cells are the standing punch list**, and after the
|
||||||
2026-07-07 audio-tax fix the ST has NONE -- every remaining
|
2026-07-07 audio-tax fix the ST has NONE -- every remaining
|
||||||
below-floor graphics cell is the Amiga's: the circle family (diag
|
below-floor graphics cell is the Amiga's: the circle family (diag
|
||||||
|
|
@ -833,7 +1071,7 @@ the real-HW column is the datasheet claim.
|
||||||
call on the 65816; 32-bit shifts there are per-bit helper loops).
|
call on the 65816; 32-bit shifts there are per-bit helper loops).
|
||||||
Output is bit-identical -- host-proven over the golden sequence and
|
Output is bit-identical -- host-proven over the golden sequence and
|
||||||
a 50M-state sweep, and gated by UBER's random-golden check on all
|
a 50M-state sweep, and gated by UBER's random-golden check on all
|
||||||
four ports.
|
five ports.
|
||||||
* **Input predicates stay functions** (Phase 8 #40 decision): the
|
* **Input predicates stay functions** (Phase 8 #40 decision): the
|
||||||
post-clock-fix numbers (12K-77K ops/sec) put call overhead nowhere
|
post-clock-fix numbers (12K-77K ops/sec) put call overhead nowhere
|
||||||
near a real frame budget; macro forms in the public header were
|
near a real frame budget; macro forms in the public header were
|
||||||
|
|
|
||||||
181
README.md
181
README.md
|
|
@ -1,12 +1,13 @@
|
||||||
# JoeyLib
|
# JoeyLib
|
||||||
|
|
||||||
A unified C game-development library targeting four early 16-bit
|
A unified C game-development library targeting five early 16-bit
|
||||||
platforms from a single codebase:
|
platforms from a single codebase:
|
||||||
|
|
||||||
- Apple IIgs (reference platform)
|
- Apple IIgs (reference platform)
|
||||||
- Commodore Amiga (A500 / 68000 baseline)
|
- Commodore Amiga (A500 / 68000 baseline)
|
||||||
- Atari ST (STF / 68000 baseline)
|
- Atari ST (STF / 68000 baseline)
|
||||||
- MS-DOS (386 / VGA, DJGPP)
|
- MS-DOS (386 / VGA, DJGPP)
|
||||||
|
- Sharp X68000 (68000 @ 10 MHz, Human68k, elf2x68k)
|
||||||
|
|
||||||
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.
|
||||||
|
|
@ -26,8 +27,12 @@ make
|
||||||
```
|
```
|
||||||
|
|
||||||
This builds `libjoey.a` for every target whose toolchain is installed,
|
This builds `libjoey.a` for every target whose toolchain is installed,
|
||||||
plus the example programs (`hello`, `pattern`, `keys`, `serial`, `save`,
|
plus the example programs (`hello`, `pattern`, `draw`, `keys`, `serial`,
|
||||||
`joy`, `sprite`, `audio`) for each.
|
`sertest`, `save`, `joy`, `sprite`, `audio`, `uber`, `adventure`,
|
||||||
|
`adventure2`, `agi`, `spacetaxi`) for each. The IIgs is the exception:
|
||||||
|
`make iigs` compile-checks the library and runs the clang smoke test, and
|
||||||
|
`make -f make/iigs.mk iigs-examples` builds its example binaries. The
|
||||||
|
Sharp X68000 is not covered at all -- see below.
|
||||||
|
|
||||||
|
|
||||||
## Building for a single target
|
## Building for a single target
|
||||||
|
|
@ -40,6 +45,43 @@ make atarist
|
||||||
make dos
|
make dos
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The Sharp X68000 has no top-level target: `toolchains/install.sh` does not
|
||||||
|
fetch elf2x68k and the root `Makefile` has no `x68000` rule. Stage the
|
||||||
|
elf2x68k tarball under `toolchains/x68000/m68k-xelf` and drive the fragment
|
||||||
|
directly -- it resolves its own compiler relative to the repo, so this one
|
||||||
|
does not need `env.sh`:
|
||||||
|
|
||||||
|
```
|
||||||
|
make -f make/x68000.mk # libjoey.a plus SERIAL.X, UBER.X, AUDIO.X
|
||||||
|
```
|
||||||
|
|
||||||
|
Every fragment also answers to a bare `clean`, and every fragment that builds
|
||||||
|
through `make/common.mk` -- all of them except `make/iigs.mk`, which assembles
|
||||||
|
its own clang flags -- honors `EXTRA_CFLAGS`:
|
||||||
|
|
||||||
|
```
|
||||||
|
make -f make/atarist.mk clean
|
||||||
|
make -f make/x68000.mk EXTRA_CFLAGS=-DJOEY_LOG_SERIAL_TEE
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Conformance gate
|
||||||
|
|
||||||
|
`examples/uber` is the conformance vehicle: it exercises every public op and
|
||||||
|
logs a surface hash per op. `tools/diff-uber-hashes <reference-log> <test-log>`
|
||||||
|
compares two `joeylog.txt` captures and exits non-zero on any mismatch. The
|
||||||
|
frozen references live in `tests/goldens/uber/`. All five ports currently agree
|
||||||
|
on all 51 hashes; the X68000 has no golden of its own and is diffed against
|
||||||
|
`tests/goldens/uber/iigs.txt`, which it matches exactly:
|
||||||
|
|
||||||
|
```
|
||||||
|
make -f make/x68000.mk x68000-verify-golden # ~70 min under the patched MAME
|
||||||
|
make -f make/x68000.mk x68000-verify-serial # both-directions RS-232C, ~1 min
|
||||||
|
```
|
||||||
|
|
||||||
|
Both X68000 gates want `X68K_SCRATCH` pointing at a work dir containing
|
||||||
|
`x68mame/`, and a MAME built with the patches in `patches/`.
|
||||||
|
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
|
|
@ -47,12 +89,24 @@ make dos
|
||||||
docs/ design and reference documentation
|
docs/ design and reference documentation
|
||||||
include/joey/ public headers
|
include/joey/ public headers
|
||||||
src/core/ portable library code
|
src/core/ portable library code
|
||||||
src/codegen/ runtime sprite codegen (per-CPU emitters)
|
src/generic/ portable-C default for every overridable op
|
||||||
src/port/<plat>/ per-platform HAL implementations
|
src/codegen/ shared sprite-codegen staging / compile machinery
|
||||||
src/shared68k/ assembly shared by Amiga and Atari ST
|
src/<machine>/ per-machine HAL + per-CPU sprite emitter
|
||||||
|
(iigs, amiga, atarist, dos, x68000)
|
||||||
|
src/m68k/ 68k code shared across machines (planar and word-
|
||||||
|
interleaved sprite emitters; surface68k.s, which is
|
||||||
|
linked by Amiga and Atari ST only)
|
||||||
|
src/blank/ copy-to-start template for a new port
|
||||||
tools/assetbake/ PNG -> native .tbk / .spr baker (Python)
|
tools/assetbake/ PNG -> native .tbk / .spr baker (Python)
|
||||||
tools/joeymod/ Protracker .MOD converter (passthrough or .NTP)
|
tools/joeymod/ Protracker .MOD converter (passthrough or .NTP)
|
||||||
|
tools/spritebake/ offline .spr -> .spc pre-compiled sprite baker
|
||||||
|
tools/songbake/ chip-tracker text notation -> JYM1 stream
|
||||||
|
tools/xdftool.py Human68k FAT12 .XDF disk image reader / writer
|
||||||
|
tools/diff-uber-hashes UBER golden-hash comparator
|
||||||
examples/ example programs
|
examples/ example programs
|
||||||
|
scripts/ run / bench / verification scripts
|
||||||
|
patches/ MAME patches the X68000 gates need
|
||||||
|
tests/goldens/uber/ frozen UBER conformance hashes
|
||||||
toolchains/ self-contained cross-build tools
|
toolchains/ self-contained cross-build tools
|
||||||
make/ per-target Makefile fragments
|
make/ per-target Makefile fragments
|
||||||
build/<plat>/ per-target build outputs
|
build/<plat>/ per-target build outputs
|
||||||
|
|
@ -77,8 +131,7 @@ is a quick reference. Every entry point is plain C, no C++ extensions.
|
||||||
```c
|
```c
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint32_t codegenBytes; // runtime compiled-sprite cache size
|
uint32_t codegenBytes; // runtime compiled-sprite cache size
|
||||||
uint16_t maxSurfaces; // maximum concurrent surfaces
|
uint32_t audioBytes; // reserved; not yet consulted by any engine
|
||||||
uint32_t audioBytes; // audio sample / module RAM pool
|
|
||||||
} jlConfigT;
|
} jlConfigT;
|
||||||
|
|
||||||
bool jlInit (const jlConfigT *config);
|
bool jlInit (const jlConfigT *config);
|
||||||
|
|
@ -87,9 +140,17 @@ const char *jlLastError (void);
|
||||||
const char *jlPlatformName (void);
|
const char *jlPlatformName (void);
|
||||||
const char *jlVersionString(void);
|
const char *jlVersionString(void);
|
||||||
|
|
||||||
|
void *jlAlloc (uint32_t bytes); // native allocator, not malloc
|
||||||
|
void jlFree (void *p);
|
||||||
|
|
||||||
void jlWaitVBL (void); // block until next VBL
|
void jlWaitVBL (void); // block until next VBL
|
||||||
uint16_t jlFrameCount (void); // monotonic 16-bit frame counter
|
uint16_t jlFrameCount (void); // monotonic 16-bit frame counter
|
||||||
uint16_t jlFrameHz (void); // 50 / 60 / 70 depending on port
|
uint16_t jlFrameHz (void); // 50 / 55 / 60 / 70 depending on port
|
||||||
|
uint32_t jlMillisElapsed(void); // monotonic ms since jlInit
|
||||||
|
|
||||||
|
uint32_t jlRandom (void); // portable, bit-identical per seed
|
||||||
|
uint16_t jlRandomRange (uint16_t bound);
|
||||||
|
void jlRandomSeed (uint32_t seed);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -98,9 +159,9 @@ uint16_t jlFrameHz (void); // 50 / 60 / 70 depending on port
|
||||||
All surfaces are 320x200 16-color images with a 200-entry SCB table
|
All surfaces are 320x200 16-color images with a 200-entry SCB table
|
||||||
and 16 palettes of 16 `$0RGB` colors. In-memory storage is
|
and 16 palettes of 16 `$0RGB` colors. In-memory storage is
|
||||||
target-native: chunky 4bpp packed on IIgs and DOS, native planar
|
target-native: chunky 4bpp packed on IIgs and DOS, native planar
|
||||||
(separate bitplanes on Amiga, word-interleaved planes on Atari ST)
|
(separate bitplanes on Amiga and Sharp X68000, word-interleaved planes
|
||||||
on the 68k ports. The public API speaks in color indices (0..15) and
|
on Atari ST) on the 68k ports. The public API speaks in color indices
|
||||||
hides the storage format.
|
(0..15) and hides the storage format.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
#define SURFACE_WIDTH 320
|
#define SURFACE_WIDTH 320
|
||||||
|
|
@ -159,7 +220,11 @@ void jlFloodFillBounded (jlSurfaceT *s, int16_t x, int16_t y,
|
||||||
|
|
||||||
Colors are 12-bit `$0RGB`. Color 0 of every palette is forced to
|
Colors are 12-bit `$0RGB`. Color 0 of every palette is forced to
|
||||||
black on `jlPaletteSet`. Each scanline picks one of the 16 palettes
|
black on `jlPaletteSet`. Each scanline picks one of the 16 palettes
|
||||||
via the SCB.
|
via the SCB. Every port honours it. The X68000 does it the same way
|
||||||
|
the DOS port does: it renders through the 256-colour graphics plane,
|
||||||
|
so all 16 palettes are resident at once and each pixel byte is
|
||||||
|
`(scb[y] << 4) | nibble` -- the pixel value selects the band, with no
|
||||||
|
raster interrupt involved.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
void jlPaletteSet (jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
|
void jlPaletteSet (jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16);
|
||||||
|
|
@ -244,8 +309,18 @@ void jlSpriteDestroy (jlSpriteT *sp);
|
||||||
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels,
|
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels,
|
||||||
uint16_t maxCels, uint16_t *outPalette);
|
uint16_t maxCels, uint16_t *outPalette);
|
||||||
|
|
||||||
|
// Same contract, but reading a `.spc` bank pre-compiled for THIS target by
|
||||||
|
// the offline baker `tools/spritebake`: the native blit routines are copied
|
||||||
|
// straight into the codegen arena, so there is no startup JIT. The runtime
|
||||||
|
// twin writes one back out after a jlSpriteCompile pass.
|
||||||
|
uint16_t jlSpriteBankLoadPrecompiled(const char *path, jlSpriteT **outCels,
|
||||||
|
uint16_t maxCels, uint16_t *outPalette);
|
||||||
|
bool jlSpriteBankSavePrecompiled(const char *path, jlSpriteT **cels,
|
||||||
|
uint16_t count, const uint16_t *palette);
|
||||||
|
|
||||||
bool jlSpriteCompile (jlSpriteT *sp); // build per-shift fast path
|
bool jlSpriteCompile (jlSpriteT *sp); // build per-shift fast path
|
||||||
void jlSpritePrewarm (jlSpriteT *sp); // hint: compile if not already
|
void jlSpritePrewarm (jlSpriteT *sp); // hint: compile if not already
|
||||||
|
uint32_t jlSpriteCompiledSize (const jlSpriteT *sp);
|
||||||
|
|
||||||
void jlSpriteDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
|
void jlSpriteDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
|
||||||
void jlSpriteSaveUnder (const jlSurfaceT *s, jlSpriteT *sp,
|
void jlSpriteSaveUnder (const jlSurfaceT *s, jlSpriteT *sp,
|
||||||
|
|
@ -254,6 +329,17 @@ void jlSpriteRestoreUnder (jlSurfaceT *s, const jlSpriteBackupT *backup
|
||||||
void jlSpriteSaveAndDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
|
void jlSpriteSaveAndDraw (jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
|
||||||
jlSpriteBackupT *backup);
|
jlSpriteBackupT *backup);
|
||||||
|
|
||||||
|
// TRUSTED variants: skip the per-call geometry validation for a caller that
|
||||||
|
// pre-compiles its sprites and guarantees the sprite is FULLY on-surface.
|
||||||
|
// UB if that is violated -- no clipping happens. They fall back to the
|
||||||
|
// validated entry for uncompiled sprites and on ports with no compiled path.
|
||||||
|
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);
|
||||||
|
|
||||||
void jlSpriteCompact (void); // defrag the codegen arena
|
void jlSpriteCompact (void); // defrag the codegen arena
|
||||||
uint32_t jlSpriteCodegenBytesUsed (void);
|
uint32_t jlSpriteCodegenBytesUsed (void);
|
||||||
uint32_t jlSpriteCodegenBytesTotal (void);
|
uint32_t jlSpriteCodegenBytesTotal (void);
|
||||||
|
|
@ -273,7 +359,11 @@ Two blob formats:
|
||||||
* **`.tbk` (tile bank)** -- one or more 8x8 tiles in per-target
|
* **`.tbk` (tile bank)** -- one or more 8x8 tiles in per-target
|
||||||
planar layout (Amiga plane-major; Atari ST row-major-with-planes-
|
planar layout (Amiga plane-major; Atari ST row-major-with-planes-
|
||||||
per-row; DOS / IIgs chunky 4bpp). The loader `jlTileBankLoad`
|
per-row; DOS / IIgs chunky 4bpp). The loader `jlTileBankLoad`
|
||||||
rejects files baked for the wrong target.
|
rejects files baked for the wrong target. There is no X68000 target:
|
||||||
|
`assetbake.py` bakes only the four above, and the loader still expects
|
||||||
|
the DOS chunky target byte there (`src/core/assetLoad.c`) even though
|
||||||
|
that port's surfaces and tile ops are now planar -- so `.tbk` baking
|
||||||
|
is not wired up for the X68000 yet.
|
||||||
* **`.spr` (sprite cel set)** -- one or more uniform-sized sprite
|
* **`.spr` (sprite cel set)** -- one or more uniform-sized sprite
|
||||||
cels in cross-target chunky 4bpp. The Phase 11 walker reads chunky
|
cels in cross-target chunky 4bpp. The Phase 11 walker reads chunky
|
||||||
and converts to planar at draw time, so the same blob serves every
|
and converts to planar at draw time, so the same blob serves every
|
||||||
|
|
@ -286,10 +376,13 @@ tools/assetbake/assetbake.py --type tile --target {amiga|atarist|dos|iigs} in.
|
||||||
tools/assetbake/assetbake.py --type sprite --cell WxH in.png out.spr
|
tools/assetbake/assetbake.py --type sprite --cell WxH in.png out.spr
|
||||||
```
|
```
|
||||||
|
|
||||||
The Makefiles (`make/{amiga,atarist,dos,iigs}.mk`) wire bake rules
|
The Makefiles (`make/{amiga,atarist,dos}.mk`) wire bake rules
|
||||||
per target so `make <target>` produces baked blobs under
|
per target so `make <target>` produces baked blobs under
|
||||||
`examples/<game>/generated/<target>/` and stages them into the
|
`examples/<game>/generated/<target>/` and stages them into the
|
||||||
runtime tree at `build/<target>/.../DATA/`.
|
runtime tree at `build/<target>/.../DATA/`. `make/iigs.mk` runs no
|
||||||
|
`assetbake.py`: its `generated/iigs` tree is produced out-of-band and the
|
||||||
|
fragment only bakes `.spc` from the committed `.spr`. `make/x68000.mk`
|
||||||
|
has no asset rules at all.
|
||||||
|
|
||||||
For runtime-extracted content (sprites peeled out of a procedural
|
For runtime-extracted content (sprites peeled out of a procedural
|
||||||
or captured surface), use `jlSpriteCreateFromSurface` -- the
|
or captured surface), use `jlSpriteCreateFromSurface` -- the
|
||||||
|
|
@ -351,14 +444,18 @@ void jlJoystickReset (jlJoystickE js, uint8_t deadZone);
|
||||||
|
|
||||||
### Audio (`joey/audio.h`)
|
### Audio (`joey/audio.h`)
|
||||||
|
|
||||||
4-channel Protracker-style music plus four one-shot SFX slots. Module
|
4-channel Protracker-style music plus five one-shot SFX slots, three
|
||||||
data must be the platform-native form produced by `tools/joeymod`
|
PSG-style tone voices and a noise channel. Module data must be the
|
||||||
(`.mod` for Amiga/DOS/ST; `.ntp` for IIgs; `.amod` if you want
|
platform-native form produced by `tools/joeymod` (`.mod` for
|
||||||
loop=false on Amiga). A failed `jlAudioInit` is non-fatal; the rest
|
Amiga/DOS/ST/X68000; `.ntp` for IIgs; `.amod` if you want loop=false on
|
||||||
of the API stays callable as no-ops.
|
Amiga). Amiga plays `.mod` through PTPlayer and the IIgs through the
|
||||||
|
NinjaTrackerPlus replayer; DOS, Atari ST and X68000 decode with
|
||||||
|
libxmp-lite. A failed `jlAudioInit` is non-fatal; the rest of the API
|
||||||
|
stays callable as no-ops.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
#define JOEY_AUDIO_SFX_SLOTS 4
|
#define JOEY_AUDIO_SFX_SLOTS 5
|
||||||
|
#define JOEY_AUDIO_VOICES 3
|
||||||
|
|
||||||
bool jlAudioInit (void);
|
bool jlAudioInit (void);
|
||||||
void jlAudioShutdown (void);
|
void jlAudioShutdown (void);
|
||||||
|
|
@ -369,8 +466,24 @@ bool jlAudioIsPlayingMod (void);
|
||||||
|
|
||||||
void jlAudioPlaySfx (uint8_t slot, const uint8_t *sample,
|
void jlAudioPlaySfx (uint8_t slot, const uint8_t *sample,
|
||||||
uint32_t length, uint16_t rateHz);
|
uint32_t length, uint16_t rateHz);
|
||||||
|
void jlAudioPlaySfxStream (uint8_t slot, jlAudioStreamFillT fill, void *ctx,
|
||||||
|
uint16_t rateHz);
|
||||||
void jlAudioStopSfx (uint8_t slot);
|
void jlAudioStopSfx (uint8_t slot);
|
||||||
|
|
||||||
|
// Portable chip-tracker music: a JYM1 event stream baked by tools/songbake,
|
||||||
|
// played through the tone/noise layer at register-write cost on every port.
|
||||||
|
// No CPU mixing, so it is viable where jlAudioPlayMod is not.
|
||||||
|
bool jlMusicPlay (const uint8_t *data, uint32_t length, bool loop);
|
||||||
|
void jlMusicStop (void);
|
||||||
|
bool jlMusicIsPlaying (void);
|
||||||
|
void jlMusicPause (void);
|
||||||
|
void jlMusicResume (void);
|
||||||
|
void jlMusicSetAtten (uint8_t atten);
|
||||||
|
|
||||||
|
void jlAudioTone (uint16_t freqHz);
|
||||||
|
void jlAudioVoice (uint8_t voice, uint16_t freqHz, uint8_t atten);
|
||||||
|
void jlAudioNoise (uint8_t pitch, uint8_t atten);
|
||||||
|
|
||||||
void jlAudioFrameTick (void);
|
void jlAudioFrameTick (void);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -418,12 +531,12 @@ void jlSerialFlush (void); // discard buffered input
|
||||||
|
|
||||||
Per-port device mapping:
|
Per-port device mapping:
|
||||||
|
|
||||||
| Device | IIgs | DOS | Amiga | Atari ST |
|
| Device | IIgs | DOS | Amiga | Atari ST | X68000 |
|
||||||
|---------------------|-------------------------|-----------|-------------------|--------------|
|
|---------------------|-------------------------|-----------|-------------------|--------------|-----------------|
|
||||||
| `JL_SERIAL_DEFAULT` | modem port (SCC A) | COM1 | serial.device 0 | Modem/RS-232 |
|
| `JL_SERIAL_DEFAULT` | modem port (SCC A) | COM1 | serial.device 0 | Modem/RS-232 | RS-232C (SCC A) |
|
||||||
| `JL_SERIAL_MODEM` | modem port (SCC A) | COM1 | serial.device 0 | Modem/RS-232 |
|
| `JL_SERIAL_MODEM` | modem port (SCC A) | COM1 | serial.device 0 | Modem/RS-232 | RS-232C (SCC A) |
|
||||||
| `JL_SERIAL_PRINTER` | printer port (SCC B) | COM1 | serial.device 0 | Modem/RS-232 |
|
| `JL_SERIAL_PRINTER` | printer port (SCC B) | COM1 | serial.device 0 | Modem/RS-232 | RS-232C (SCC A) |
|
||||||
| `JL_SERIAL_SLOT` | 6551 SSC in slot `unit` | COM`unit` | serial.device `unit` | Modem/RS-232 |
|
| `JL_SERIAL_SLOT` | 6551 SSC in slot `unit` | COM`unit` | serial.device `unit` | Modem/RS-232 | RS-232C (SCC A) |
|
||||||
|
|
||||||
Only the IIgs has multiple built-in ports; elsewhere `MODEM`/`PRINTER` fall
|
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
|
||||||
|
|
@ -456,7 +569,8 @@ uint32_t jlDiskFree (void); // free bytes on the save volume (0 if unknown)
|
||||||
Directory create / delete / free-space use a per-port HAL (POSIX `mkdir` +
|
Directory create / delete / free-space use a per-port HAL (POSIX `mkdir` +
|
||||||
`remove` + INT 21h on DOS; `mkdir` + GEMDOS `Dfree` on ST; dos.library
|
`remove` + INT 21h on DOS; `mkdir` + GEMDOS `Dfree` on ST; dos.library
|
||||||
`CreateDir`/`DeleteFile`/`Info` on Amiga; GS/OS `Create` + a
|
`CreateDir`/`DeleteFile`/`Info` on Amiga; GS/OS `Create` + a
|
||||||
`GetDevNumber`/`DInfo`/`Volume` chain on the IIgs). `scripts/check-save.sh`
|
`GetDevNumber`/`DInfo`/`Volume` chain on the IIgs; Human68k `_dos_mkdir` +
|
||||||
|
`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: [`docs/save.md`](docs/save.md).
|
||||||
|
|
@ -477,6 +591,15 @@ void jlLogReset(void);
|
||||||
|
|
||||||
Output goes to `joeylog.txt` in the program's working directory.
|
Output goes to `joeylog.txt` in the program's working directory.
|
||||||
|
|
||||||
|
Building with `EXTRA_CFLAGS=-DJOEY_LOG_SERIAL_TEE` additionally mirrors every
|
||||||
|
`jlLog` / `jlLogF` line out the port's serial line as it is written --
|
||||||
|
unbounded, needs no disk, and arrives in real time, which is what makes a
|
||||||
|
wedged run diagnosable. It is OFF by default and is currently implemented
|
||||||
|
only for the X68000 (`jlpLogTee` in `src/x68000/hal.c`); the host end is
|
||||||
|
`scripts/x68kSerialPeer.py`, which must be run under `python3 -u` -- it never
|
||||||
|
passes `flush=True`, so a killed peer loses its whole capture to stdio
|
||||||
|
buffering.
|
||||||
|
|
||||||
|
|
||||||
### Platform macros (`joey/platform.h`)
|
### Platform macros (`joey/platform.h`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -436,6 +436,10 @@
|
||||||
#define JL_HAS_WAIT_VBL // _iocs_vsync
|
#define JL_HAS_WAIT_VBL // _iocs_vsync
|
||||||
#define JL_HAS_FRAME_COUNT
|
#define JL_HAS_FRAME_COUNT
|
||||||
#define JL_HAS_FRAME_HZ // ~55 Hz in the 31 kHz modes
|
#define JL_HAS_FRAME_HZ // ~55 Hz in the 31 kHz modes
|
||||||
|
// Chains Human68k's MFP Timer C (~100 Hz). Without this the port fell back to
|
||||||
|
// the generic clock, which derives ms from the POLLED frame counter and
|
||||||
|
// undercounts exactly in proportion to how slow the op is.
|
||||||
|
#define JL_HAS_MILLIS_ELAPSED
|
||||||
// Save add-on: Human68k _dos_dskfre / _dos_mkdir, structurally the ST HAL.
|
// Save add-on: Human68k _dos_dskfre / _dos_mkdir, structurally the ST HAL.
|
||||||
#define JL_HAS_DISK_FREE
|
#define JL_HAS_DISK_FREE
|
||||||
#define JL_HAS_SAVE_DELETE
|
#define JL_HAS_SAVE_DELETE
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,10 @@ CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer \
|
||||||
LDFLAGS := -lm
|
LDFLAGS := -lm
|
||||||
|
|
||||||
PORT_C_SRCS := $(wildcard $(SRC_DIR)/x68000/*.c)
|
PORT_C_SRCS := $(wildcard $(SRC_DIR)/x68000/*.c)
|
||||||
# Shared 68k planar asm (src/m68k/surface68k.s), already used by Amiga and ST:
|
# Port-local 68000 asm, if any lands. Empty today: SCB per-band palettes are
|
||||||
# circle outline and 4-plane span fill. Free reuse now the surface layout
|
# done in the pixel expansion (DOS-style), not with a raster ISR, so the port is
|
||||||
# matches.
|
# pure C again.
|
||||||
SHARED_S := $(wildcard $(SRC_68K)/*.s)
|
PORT_S_SRCS := $(wildcard $(SRC_DIR)/x68000/*.s)
|
||||||
|
|
||||||
# No src/m68k sources yet: those are the planar sprite emitters and the planar
|
# No src/m68k sources yet: those are the planar sprite emitters and the planar
|
||||||
# surface helpers, and this port is still chunky. They come in with the storage
|
# surface helpers, and this port is still chunky. They come in with the storage
|
||||||
|
|
@ -63,6 +63,7 @@ LIB_OBJS := \
|
||||||
$(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS)) \
|
$(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS)) \
|
||||||
$(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS)) \
|
$(patsubst $(SRC_DIR)/generic/%.c,$(BUILD)/obj/generic/%.o,$(GENERIC_C_SRCS)) \
|
||||||
$(patsubst $(SRC_DIR)/x68000/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \
|
$(patsubst $(SRC_DIR)/x68000/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \
|
||||||
|
$(patsubst $(SRC_DIR)/x68000/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \
|
||||||
$(BUILD)/obj/68k/spriteEmitPlanar68k.o \
|
$(BUILD)/obj/68k/spriteEmitPlanar68k.o \
|
||||||
$(BUILD)/obj/codegen/spriteCompile.o \
|
$(BUILD)/obj/codegen/spriteCompile.o \
|
||||||
$(BUILD)/obj/codegen/spriteStage.o
|
$(BUILD)/obj/codegen/spriteStage.o
|
||||||
|
|
@ -89,6 +90,7 @@ $(LIBXMP_AR): $(LIBXMP_OBJS)
|
||||||
|
|
||||||
LIB := $(LIBDIR)/libjoey.a
|
LIB := $(LIBDIR)/libjoey.a
|
||||||
|
|
||||||
|
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
|
||||||
SERIAL_SRC := $(EXAMPLES)/serial/serial.c
|
SERIAL_SRC := $(EXAMPLES)/serial/serial.c
|
||||||
UBER_SRC := $(EXAMPLES)/uber/uber.c
|
UBER_SRC := $(EXAMPLES)/uber/uber.c
|
||||||
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
|
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
|
||||||
|
|
@ -99,7 +101,7 @@ all x68000: x68000-lib x68000-examples
|
||||||
|
|
||||||
x68000-lib: $(LIB) $(LIBXMP_AR)
|
x68000-lib: $(LIB) $(LIBXMP_AR)
|
||||||
|
|
||||||
x68000-examples: $(BINDIR)/SERIAL.X $(BINDIR)/UBER.X $(BINDIR)/AUDIO.X
|
x68000-examples: $(BINDIR)/SERIAL.X $(BINDIR)/UBER.X $(BINDIR)/AUDIO.X $(BINDIR)/PATTERN.X
|
||||||
|
|
||||||
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
|
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
|
|
@ -109,6 +111,12 @@ $(BUILD)/obj/generic/%.o: $(SRC_DIR)/generic/%.c
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
$(X68K_CC) $(CFLAGS) -c $< -o $@
|
$(X68K_CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
# Port-local asm. Same object dir as the port C objects; make picks between the
|
||||||
|
# two pattern rules by which prerequisite exists.
|
||||||
|
$(BUILD)/obj/port/%.o: $(SRC_DIR)/x68000/%.s
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
$(X68K_CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s
|
$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
$(X68K_CC) $(CFLAGS) -c $< -o $@
|
$(X68K_CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
@ -129,6 +137,12 @@ $(LIB): $(LIB_OBJS)
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
$(X68K_AR) rcs $@ $(LIB_OBJS)
|
$(X68K_AR) rcs $@ $(LIB_OBJS)
|
||||||
|
|
||||||
|
# PATTERN is the SCB acceptance vehicle: 8 bands of 25 lines with 8 gradient
|
||||||
|
# palettes, so a mis-placed band boundary is immediately visible.
|
||||||
|
$(BINDIR)/PATTERN.X: $(PATTERN_SRC) $(LIB) $(LIBXMP_AR)
|
||||||
|
@mkdir -p $(dir $@)
|
||||||
|
$(X68K_CC) $(CFLAGS) $(PATTERN_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
|
||||||
|
|
||||||
$(BINDIR)/SERIAL.X: $(SERIAL_SRC) $(LIB) $(LIBXMP_AR)
|
$(BINDIR)/SERIAL.X: $(SERIAL_SRC) $(LIB) $(LIBXMP_AR)
|
||||||
@mkdir -p $(dir $@)
|
@mkdir -p $(dir $@)
|
||||||
$(X68K_CC) $(CFLAGS) $(SERIAL_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
|
$(X68K_CC) $(CFLAGS) $(SERIAL_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
|
||||||
|
|
|
||||||
330
src/x68000/hal.c
330
src/x68000/hal.c
|
|
@ -20,6 +20,9 @@
|
||||||
#include <x68k/iocs.h>
|
#include <x68k/iocs.h>
|
||||||
|
|
||||||
#include "port.h"
|
#include "port.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include "joey/debug.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "surfaceInternal.h"
|
#include "surfaceInternal.h"
|
||||||
|
|
@ -34,10 +37,11 @@
|
||||||
// width, so the row stride is a constant, not a function of SURFACE_WIDTH.
|
// width, so the row stride is a constant, not a function of SURFACE_WIDTH.
|
||||||
#define X68K_GVRAM_STRIDE 512u
|
#define X68K_GVRAM_STRIDE 512u
|
||||||
|
|
||||||
// _iocs_crtmod screen mode. 12 = 512x512, 16 colours, 31 kHz. The library's
|
// _iocs_crtmod screen mode. 13 = 512x512, 256 colours, 31 kHz -- same timing as
|
||||||
// 320x200 stage is drawn into the top-left corner; there is no 320x200 mode on
|
// mode 12 (512x512/16) one slot below it, but with the 256-entry graphics
|
||||||
// this machine, so some form of letterboxing is unavoidable.
|
// palette this port needs for SCB per-band palettes. There is no 320x200 mode on
|
||||||
#define X68K_CRTMOD_512_16 12
|
// this machine, so the stage is centred and letterboxed.
|
||||||
|
#define X68K_CRTMOD_512_256 13
|
||||||
|
|
||||||
// _iocs_bitsns key-group numbers that carry the keys jlKeyE cares about.
|
// _iocs_bitsns key-group numbers that carry the keys jlKeyE cares about.
|
||||||
#define X68K_KEYGROUP_COUNT 15
|
#define X68K_KEYGROUP_COUNT 15
|
||||||
|
|
@ -49,6 +53,57 @@
|
||||||
#define X68K_TEE_SPIN_LIMIT 200000ul
|
#define X68K_TEE_SPIN_LIMIT 200000ul
|
||||||
|
|
||||||
|
|
||||||
|
// ----- SCB per-band palette (256-colour graphics plane) ----------------------
|
||||||
|
//
|
||||||
|
// JoeyLib surfaces carry an SCB: one palette index per scanline. This port does
|
||||||
|
// it the way the DOS port does (src/dos/hal.c:7), not the way the Atari ST does.
|
||||||
|
// DOS writes `pixel byte = (scb[y] << 4) | nibble` into a 256-entry DAC, so all
|
||||||
|
// 16 palettes are resident at once and the PIXEL VALUE selects the band. No
|
||||||
|
// interrupt, no timing window, correct by construction.
|
||||||
|
//
|
||||||
|
// The ST needs a raster interrupt only because the STF shifter really has 16
|
||||||
|
// hardware colours. This machine does not: the graphics plane in 256-colour mode
|
||||||
|
// has a 256-entry palette at $E82000 -- exactly 16 palettes x 16 colours. The
|
||||||
|
// TEXT plane cannot do it (four 1bpp planes cap it at 16, and MAME's
|
||||||
|
// get_text_pixel resolves 0-15 only), which is why this port moved off it.
|
||||||
|
//
|
||||||
|
// GVRAM layout in 256-colour mode, from MAME x68k_v.cpp:339-340:
|
||||||
|
// colour = gvram[lineoffset0 + x] & 0x000F; // low nibble
|
||||||
|
// colour |= gvram[lineoffset1 + x] & 0x00F0; // high nibble
|
||||||
|
// With both graphics scroll registers 0 the two line offsets are equal, so both
|
||||||
|
// nibbles come from the SAME word: the palette index is that word's LOW BYTE.
|
||||||
|
// One byte write per pixel, 512 words per line.
|
||||||
|
#define X68K_GVRAM_BASE 0x00C00000UL
|
||||||
|
#define X68K_GVRAM_WORDS_ROW 512u
|
||||||
|
// 256-entry graphics palette (x68k.cpp:965). NOT $E82200, the 16-entry text/PCG
|
||||||
|
// block the old text-plane path used.
|
||||||
|
#define X68K_GFX_PALETTE ((volatile uint16_t *)0xE82000L)
|
||||||
|
#define X68K_PALETTE_ENTRIES (SURFACE_PALETTE_COUNT * SURFACE_COLORS_PER_PALETTE)
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Millisecond clock -----------------------------------------------------
|
||||||
|
//
|
||||||
|
// Without this the port fell back to jlpGenericMillisElapsed(), which derives
|
||||||
|
// milliseconds from the FRAME COUNTER -- and this port's counter is polled
|
||||||
|
// (vdispPoll), not interrupt-driven. An op spanning more than one frame lets
|
||||||
|
// V-DISP edges pass unobserved, so the measured time shrank in proportion to how
|
||||||
|
// slow the op was and the two errors cancelled: every UBER row landed near
|
||||||
|
// 10,000 ops/sec regardless of workload. Games pacing on jlMillisElapsed were
|
||||||
|
// equally wrong.
|
||||||
|
//
|
||||||
|
// Fix: chain Human68k's existing MFP Timer C tick (~100 Hz) and count it. MFP
|
||||||
|
// channel 5, so vector ($40 | 5) = $45 at address $45 * 4 = $114.
|
||||||
|
#define X68K_TICK_VEC_ADDR 0x00000114ul
|
||||||
|
// Timer C's rate is NOT assumed -- it is derived at init from the MFP's own
|
||||||
|
// registers, because Human68k's programming is not what you would guess. A
|
||||||
|
// measured run showed ~178 Hz, not the 100 Hz a first guess suggested:
|
||||||
|
// 4 MHz / (prescale 200 * TCDR 112) = 178.6 Hz.
|
||||||
|
#define X68K_MFP_TCDCR ((volatile uint8_t *)0xE8801DL) // reg 14
|
||||||
|
#define X68K_MFP_TCDR ((volatile uint8_t *)0xE88023L) // reg 17
|
||||||
|
#define X68K_MFP_CLOCK_HZ 4000000ul
|
||||||
|
#define X68K_TCDCR_C_SHIFT 4u
|
||||||
|
#define X68K_TCDCR_C_MASK 0x07u
|
||||||
|
|
||||||
// ----- Module state ---------------------------------------------------------
|
// ----- Module state ---------------------------------------------------------
|
||||||
|
|
||||||
static int gPrevCrtMode = -1;
|
static int gPrevCrtMode = -1;
|
||||||
|
|
@ -62,10 +117,30 @@ static uint8_t gLastVdisp = 0;
|
||||||
// True once the V-DISP interrupt handler is live; the poll then stands down.
|
// True once the V-DISP interrupt handler is live; the poll then stands down.
|
||||||
static bool gVdispInstalled = false;
|
static bool gVdispInstalled = false;
|
||||||
|
|
||||||
|
// Bumped by x68kTickIsr.s on every Human68k Timer C interrupt. Written from
|
||||||
|
// interrupt context, so volatile; read with a single long load, which is atomic
|
||||||
|
// against the handler's addq.l on a 68000.
|
||||||
|
volatile uint32_t gX68kTicks = 0ul;
|
||||||
|
extern void x68kTickIsr(void);
|
||||||
|
extern uint32_t x68kTickChainAddr; // the JMP operand inside the ISR
|
||||||
|
static uint32_t gTickBase = 0ul;
|
||||||
|
static uint32_t gTickHz = 0ul;
|
||||||
|
static bool gTickBaseSet = false;
|
||||||
|
static bool gTickHooked = false;
|
||||||
|
|
||||||
|
// ----- Prototypes -----
|
||||||
|
|
||||||
|
static uint32_t tickHzFromMfp(void);
|
||||||
|
uint32_t jlpMillisElapsed(void);
|
||||||
|
uint16_t jlpFrameHz(void);
|
||||||
|
static void buildSpreadTables(void);
|
||||||
|
static void installTickIsr(void);
|
||||||
|
|
||||||
|
static void uploadGfxPalette(const jlSurfaceT *src);
|
||||||
|
|
||||||
// Defined below with the timing code; declared here because jlpInit installs it.
|
|
||||||
static void vdispHandler(void);
|
static void vdispHandler(void);
|
||||||
static void vdispPoll(void);
|
static void vdispPoll(void);
|
||||||
|
|
||||||
extern void *const gX68kVdispHandlerRef;
|
extern void *const gX68kVdispHandlerRef;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -80,16 +155,19 @@ bool jlpInit(const jlConfigT *config) {
|
||||||
// if we were already supervisor -- in which case do not switch back.
|
// if we were already supervisor -- in which case do not switch back.
|
||||||
gPrevSsp = _dos_super(0);
|
gPrevSsp = _dos_super(0);
|
||||||
gPrevCrtMode = _iocs_crtmod(-1); // -1 queries without changing
|
gPrevCrtMode = _iocs_crtmod(-1); // -1 queries without changing
|
||||||
_iocs_crtmod(X68K_CRTMOD_512_16);
|
_iocs_crtmod(X68K_CRTMOD_512_256);
|
||||||
_iocs_g_clr_on(); // clear graphics + enable the plane
|
_iocs_g_clr_on(); // clear graphics + enable the plane
|
||||||
// NOTE: installing vdispHandler via _iocs_vdispst HANGS the machine --
|
// NOTE: installing vdispHandler via _iocs_vdispst HANGS the machine --
|
||||||
// tested, no serial output at all, so it wedges before main() gets going.
|
// tested, no serial output at all, so it wedges before main() gets going.
|
||||||
// The handler ABI is not a plain C function: a _VDISPST handler is entered
|
// IOCS does NOT wrap the handler: _VDISPST writes the pointer straight into
|
||||||
// from the interrupt and this one returns with RTS from a context IOCS does
|
// exception vector $134 (iplrom.dat $FF9DC8), exactly as _CRTCRAS does for
|
||||||
// not expect. Getting it right needs an asm thunk (save d0-d1/a0-a1, call,
|
// $138. So the handler must be a raw interrupt routine -- preserve every
|
||||||
// restore, correct return), which is worth doing but is not a one-liner.
|
// register, return with RTE -- and a C function (RTS, free to clobber
|
||||||
|
// d0/d1/a0/a1) can never be valid. A raw asm handler would be required;
|
||||||
|
// the polled fallback below owns the frame counter until then.
|
||||||
// Until then the polled fallback below owns the counter.
|
// Until then the polled fallback below owns the counter.
|
||||||
gVdispInstalled = false;
|
gVdispInstalled = false;
|
||||||
|
installTickIsr();
|
||||||
gModeSet = true;
|
gModeSet = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -110,6 +188,120 @@ void jlpShutdown(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Timer C's interrupt rate, read from the MFP rather than assumed:
|
||||||
|
// MFP clock / (prescaler * data register). The prescaler is a 3-bit code in the
|
||||||
|
// high nibble of TCDCR; code 0 means the timer is stopped.
|
||||||
|
static uint32_t tickHzFromMfp(void) {
|
||||||
|
static const uint16_t kPrescale[8] = { 0u, 4u, 10u, 16u, 50u, 64u, 100u, 200u };
|
||||||
|
uint16_t code;
|
||||||
|
uint16_t divisor;
|
||||||
|
uint16_t data;
|
||||||
|
|
||||||
|
code = (uint16_t)((*X68K_MFP_TCDCR >> X68K_TCDCR_C_SHIFT) & X68K_TCDCR_C_MASK);
|
||||||
|
divisor = kPrescale[code];
|
||||||
|
if (divisor == 0u) {
|
||||||
|
return 0ul;
|
||||||
|
}
|
||||||
|
data = *X68K_MFP_TCDR;
|
||||||
|
if (data == 0u) {
|
||||||
|
data = 256u; // 0 in the data register means 256
|
||||||
|
}
|
||||||
|
return X68K_MFP_CLOCK_HZ / ((uint32_t)divisor * (uint32_t)data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Chain our counter onto Human68k's Timer C interrupt. The previous handler is
|
||||||
|
// patched into the ISR's JMP operand, so it still runs and still owns the RTE --
|
||||||
|
// the OS clock keeps working. Supervisor already (jlpInit's _dos_super), so the
|
||||||
|
// vector write is legal. Refuses if the vector looks unset, because chaining to
|
||||||
|
// nothing would fault on the first tick.
|
||||||
|
static void installTickIsr(void) {
|
||||||
|
// Laundered through a volatile: gcc treats a constant pointer this close to
|
||||||
|
// zero as a null dereference and -Werror=array-bounds rejects it, though
|
||||||
|
// $114 is a perfectly real supervisor-writable exception vector.
|
||||||
|
static volatile uintptr_t addr = X68K_TICK_VEC_ADDR;
|
||||||
|
volatile uint32_t *vec;
|
||||||
|
|
||||||
|
vec = (volatile uint32_t *)addr;
|
||||||
|
if (*vec == 0ul) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gTickHz = tickHzFromMfp();
|
||||||
|
if (gTickHz == 0ul) {
|
||||||
|
return; // timer stopped: no usable clock
|
||||||
|
}
|
||||||
|
x68kTickChainAddr = *vec;
|
||||||
|
*vec = (uint32_t)x68kTickIsr;
|
||||||
|
gTickHooked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- SCB per-band palette --------------------------------------------------
|
||||||
|
|
||||||
|
// Load all 16 palettes into the 256-entry graphics palette: entry p*16+c is
|
||||||
|
// palette p colour c, so a pixel byte of (p << 4) | c selects it directly.
|
||||||
|
// Rebuilt only when a palette actually changes; the SCB flag is deliberately
|
||||||
|
// NOT consumed here, because the per-line choice is applied in the pixel
|
||||||
|
// expansion, which reads src->scb directly. Same contract as
|
||||||
|
// src/dos/hal.c:232-234.
|
||||||
|
static void uploadGfxPalette(const jlSurfaceT *src) {
|
||||||
|
uint16_t pal;
|
||||||
|
uint16_t col;
|
||||||
|
|
||||||
|
for (pal = 0u; pal < SURFACE_PALETTE_COUNT; pal++) {
|
||||||
|
for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) {
|
||||||
|
X68K_GFX_PALETTE[(pal << 4) | col] = x68kColorFromRgb12(src->palette[pal][col]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- Present expansion tables ----------------------------------------------
|
||||||
|
//
|
||||||
|
// The naive loop extracted one nibble per pixel with four shifts and a mask,
|
||||||
|
// then stored one byte: about 8 operations per pixel over 64,000 pixels, which
|
||||||
|
// put the composite game frame at 0.10x of the IIgs.
|
||||||
|
//
|
||||||
|
// DOS's gExpandLut cannot be copied directly: mode 13h pixels are ADJACENT
|
||||||
|
// bytes so DOS fuses two into one 16-bit store, whereas GVRAM here is one
|
||||||
|
// 16-bit WORD per pixel (index in the low byte), so the output bytes are two
|
||||||
|
// apart. The fusion that works on this machine is the other way up -- write
|
||||||
|
// WHOLE WORDS, because one 32-bit store then covers exactly two pixels. The
|
||||||
|
// high byte belongs to graphics pages 2/3, which the 256-colour index does not
|
||||||
|
// read (x68k_v.cpp:339-340 masks $000F | $00F0), so writing 0 there is safe.
|
||||||
|
//
|
||||||
|
// So the tables are indexed by a PLANE and a NIBBLE (4 source pixels) and are
|
||||||
|
// pre-interleaved into word positions: gSpreadHi holds the first two of those
|
||||||
|
// pixels as (px0 << 16) | px1, gSpreadLo the second two. Four planes OR
|
||||||
|
// together, palBase ORs in replicated, and the result is two long stores per
|
||||||
|
// four pixels -- no shifting in the inner loop at all.
|
||||||
|
#define X68K_SPREAD_NIBBLES 16u
|
||||||
|
|
||||||
|
static uint32_t gSpreadHi[X68K_BITPLANES][X68K_SPREAD_NIBBLES];
|
||||||
|
static uint32_t gSpreadLo[X68K_BITPLANES][X68K_SPREAD_NIBBLES];
|
||||||
|
static bool gSpreadBuilt = false;
|
||||||
|
|
||||||
|
|
||||||
|
// Bit 3 of the nibble is the LEFTMOST of its four pixels, matching the plane
|
||||||
|
// byte convention (bit 7 leftmost of eight).
|
||||||
|
static void buildSpreadTables(void) {
|
||||||
|
uint16_t plane;
|
||||||
|
uint16_t nib;
|
||||||
|
uint32_t bit;
|
||||||
|
|
||||||
|
for (plane = 0u; plane < X68K_BITPLANES; plane++) {
|
||||||
|
bit = (uint32_t)1ul << plane;
|
||||||
|
for (nib = 0u; nib < X68K_SPREAD_NIBBLES; nib++) {
|
||||||
|
gSpreadHi[plane][nib] = (((nib >> 3) & 1u) ? (bit << 16) : 0ul)
|
||||||
|
| (((nib >> 2) & 1u) ? bit : 0ul);
|
||||||
|
gSpreadLo[plane][nib] = (((nib >> 1) & 1u) ? (bit << 16) : 0ul)
|
||||||
|
| (((nib >> 0) & 1u) ? bit : 0ul);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gSpreadBuilt = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ----- Present --------------------------------------------------------------
|
// ----- Present --------------------------------------------------------------
|
||||||
|
|
||||||
// Copy the surface's four planes into the TEXT PLANE at $E00000.
|
// Copy the surface's four planes into the TEXT PLANE at $E00000.
|
||||||
|
|
@ -123,27 +315,34 @@ void jlpShutdown(void) {
|
||||||
//
|
//
|
||||||
// Dirty rows only, and only the marked band within a row.
|
// Dirty rows only, and only the marked band within a row.
|
||||||
void jlpPresent(const jlSurfaceT *src) {
|
void jlpPresent(const jlSurfaceT *src) {
|
||||||
X68kPlanarT *pd;
|
X68kPlanarT *pd;
|
||||||
uint16_t y;
|
volatile uint32_t *wordDst;
|
||||||
uint16_t plane;
|
const uint8_t *p0;
|
||||||
uint16_t firstByte;
|
const uint8_t *p1;
|
||||||
uint16_t rowBytes;
|
const uint8_t *p2;
|
||||||
const uint8_t *s;
|
const uint8_t *p3;
|
||||||
uint8_t *d;
|
uint32_t rowBase;
|
||||||
|
uint16_t y;
|
||||||
|
uint16_t firstByte;
|
||||||
|
uint16_t rowBytes;
|
||||||
|
uint32_t pal2;
|
||||||
|
uint16_t b;
|
||||||
|
uint16_t hi0, hi1, hi2, hi3;
|
||||||
|
uint16_t lo0, lo1, lo2, lo3;
|
||||||
|
uint8_t palBase;
|
||||||
|
|
||||||
if (src == NULL) {
|
if (src == NULL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
vdispPoll(); // keep the frame counter honest: see the note on vdispPoll
|
vdispPoll(); // keep the frame counter honest: see the note on vdispPoll
|
||||||
// Push the palette when it has changed. Without this NOTHING ever uploads
|
// Reload the 256-entry graphics palette when a palette changed. The SCB
|
||||||
// colours and the display keeps whatever Human68k left in the text palette.
|
// flag is NOT consumed: the per-line palette choice is applied below in the
|
||||||
// The text plane has ONE 16-entry palette, so SCB per-scanline palette
|
// pixel expansion, which reads src->scb directly (as DOS does).
|
||||||
// selection cannot be honoured -- palette 0 is used, matching the Atari ST.
|
if (!gSpreadBuilt) {
|
||||||
|
buildSpreadTables();
|
||||||
|
}
|
||||||
if (gStagePaletteDirty) {
|
if (gStagePaletteDirty) {
|
||||||
uint16_t c;
|
uploadGfxPalette(src);
|
||||||
for (c = 0u; c < 16u; c++) {
|
|
||||||
X68K_TEXT_PALETTE[c] = x68kColorFromRgb12(src->palette[0][c]);
|
|
||||||
}
|
|
||||||
gStagePaletteDirty = false;
|
gStagePaletteDirty = false;
|
||||||
}
|
}
|
||||||
pd = x68kSurfacePlanar(src);
|
pd = x68kSurfacePlanar(src);
|
||||||
|
|
@ -160,14 +359,39 @@ void jlpPresent(const jlSurfaceT *src) {
|
||||||
if ((uint16_t)(firstByte + rowBytes) > X68K_BYTES_PER_ROW) {
|
if ((uint16_t)(firstByte + rowBytes) > X68K_BYTES_PER_ROW) {
|
||||||
rowBytes = (uint16_t)(X68K_BYTES_PER_ROW - firstByte);
|
rowBytes = (uint16_t)(X68K_BYTES_PER_ROW - firstByte);
|
||||||
}
|
}
|
||||||
for (plane = 0u; plane < X68K_BITPLANES; plane++) {
|
// This row's band selects the high nibble of every pixel byte -- the
|
||||||
s = pd->planes[plane] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte;
|
// whole SCB mechanism, exactly src/dos/hal.c:83.
|
||||||
// Centred: shift down X68K_ORIGIN_Y rows and right X68K_ORIGIN_BYTE
|
palBase = (uint8_t)(src->scb[y] << 4);
|
||||||
// bytes. X is byte-aligned (96/8 = 12) so no bit shifting is needed.
|
p0 = pd->planes[0] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte;
|
||||||
d = (uint8_t *)(X68K_TVRAM_PLANE0 + ((uint32_t)plane * X68K_TVRAM_PLANE_STEP)
|
p1 = pd->planes[1] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte;
|
||||||
+ x68kTvramOffset(0u, (uint16_t)(y + X68K_ORIGIN_Y))
|
p2 = pd->planes[2] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte;
|
||||||
+ X68K_ORIGIN_BYTE + firstByte);
|
p3 = pd->planes[3] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte;
|
||||||
memcpy(d, s, rowBytes);
|
// Centred in the 512x512 screen. One 16-bit GVRAM word per pixel, and
|
||||||
|
// the palette index is that word's LOW byte -- odd address on this
|
||||||
|
// big-endian bus.
|
||||||
|
rowBase = X68K_GVRAM_BASE
|
||||||
|
+ (((uint32_t)(y + X68K_ORIGIN_Y) * X68K_GVRAM_WORDS_ROW)
|
||||||
|
+ X68K_ORIGIN_X + ((uint32_t)firstByte * 8u)) * 2u;
|
||||||
|
wordDst = (volatile uint32_t *)rowBase;
|
||||||
|
// Two long stores per four pixels; each long is two 16-bit GVRAM words.
|
||||||
|
pal2 = (uint32_t)(((uint32_t)palBase << 16) | (uint32_t)palBase);
|
||||||
|
for (b = 0u; b < rowBytes; b++) {
|
||||||
|
hi0 = (uint16_t)(p0[b] >> 4);
|
||||||
|
hi1 = (uint16_t)(p1[b] >> 4);
|
||||||
|
hi2 = (uint16_t)(p2[b] >> 4);
|
||||||
|
hi3 = (uint16_t)(p3[b] >> 4);
|
||||||
|
lo0 = (uint16_t)(p0[b] & 0x0Fu);
|
||||||
|
lo1 = (uint16_t)(p1[b] & 0x0Fu);
|
||||||
|
lo2 = (uint16_t)(p2[b] & 0x0Fu);
|
||||||
|
lo3 = (uint16_t)(p3[b] & 0x0Fu);
|
||||||
|
*wordDst++ = gSpreadHi[0][hi0] | gSpreadHi[1][hi1]
|
||||||
|
| gSpreadHi[2][hi2] | gSpreadHi[3][hi3] | pal2;
|
||||||
|
*wordDst++ = gSpreadLo[0][hi0] | gSpreadLo[1][hi1]
|
||||||
|
| gSpreadLo[2][hi2] | gSpreadLo[3][hi3] | pal2;
|
||||||
|
*wordDst++ = gSpreadHi[0][lo0] | gSpreadHi[1][lo1]
|
||||||
|
| gSpreadHi[2][lo2] | gSpreadHi[3][lo3] | pal2;
|
||||||
|
*wordDst++ = gSpreadLo[0][lo0] | gSpreadLo[1][lo1]
|
||||||
|
| gSpreadLo[2][lo2] | gSpreadLo[3][lo3] | pal2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -339,9 +563,24 @@ void jlpWaitVBL(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Frames since init, derived from the honest millisecond tick rather than from
|
||||||
|
// counting V-DISP edges.
|
||||||
|
//
|
||||||
|
// The edge counter (vdispPoll) can only see a transition while the CPU happens
|
||||||
|
// to be inside a JoeyLib call, so any op that spans more than one frame loses
|
||||||
|
// the edges in between. That undercount is what made UBER's "16 frame" windows
|
||||||
|
// run for 88 seconds and what made every op report the same throughput. The
|
||||||
|
// tick is interrupt-driven and cannot miss, so time -> frames is monotonic and
|
||||||
|
// correct regardless of what the caller is doing.
|
||||||
|
//
|
||||||
|
// vdispPoll still runs: jlpWaitVBL needs the real V-DISP edge to synchronise
|
||||||
|
// to, and gFrameCount remains the fallback when the tick could not be hooked.
|
||||||
uint16_t jlpFrameCount(void) {
|
uint16_t jlpFrameCount(void) {
|
||||||
vdispPoll();
|
vdispPoll();
|
||||||
return gFrameCount;
|
if (!gTickHooked) {
|
||||||
|
return gFrameCount;
|
||||||
|
}
|
||||||
|
return (uint16_t)((jlpMillisElapsed() * (uint32_t)jlpFrameHz()) / 1000ul);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -371,6 +610,29 @@ void jlpLogTee(const char *text) {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// Milliseconds since the first call. Latches its base explicitly rather than
|
||||||
|
// treating 0 as a sentinel, because 0 is a legal tick value at power-on and at
|
||||||
|
// every wrap. Falls back to the frame-derived generic clock if the tick could
|
||||||
|
// not be hooked, which is wrong in the ways described above but better than
|
||||||
|
// returning a constant.
|
||||||
|
uint32_t jlpMillisElapsed(void) {
|
||||||
|
uint32_t ticks;
|
||||||
|
|
||||||
|
if (!gTickHooked) {
|
||||||
|
return jlpGenericMillisElapsed();
|
||||||
|
}
|
||||||
|
ticks = gX68kTicks;
|
||||||
|
if (!gTickBaseSet) {
|
||||||
|
gTickBase = ticks;
|
||||||
|
gTickBaseSet = true;
|
||||||
|
}
|
||||||
|
// Scale by the measured rate. Multiply first (ticks fit comfortably in 32
|
||||||
|
// bits for any realistic session) so the divide does not quantize away the
|
||||||
|
// sub-tick remainder.
|
||||||
|
return (uint32_t)(((ticks - gTickBase) * 1000ul) / gTickHz);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
uint16_t jlpFrameHz(void) {
|
uint16_t jlpFrameHz(void) {
|
||||||
// 31 kHz modes on this machine run at ~55.5 Hz rather than 60. Reported
|
// 31 kHz modes on this machine run at ~55.5 Hz rather than 60. Reported
|
||||||
// only -- the generic millisElapsed divides by it.
|
// only -- the generic millisElapsed divides by it.
|
||||||
|
|
|
||||||
30
src/x68000/x68kTickIsr.s
Normal file
30
src/x68000/x68kTickIsr.s
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
| Sharp X68000 millisecond tick.
|
||||||
|
|
|
||||||
|
| Human68k already runs MFP Timer C as its ~100 Hz system tick, so rather than
|
||||||
|
| program a timer of our own we CHAIN its interrupt: bump a counter, then jump
|
||||||
|
| to whatever handler was there before. That keeps the OS clock working and
|
||||||
|
| costs one add per tick.
|
||||||
|
|
|
||||||
|
| Entered DIRECTLY from 68000 exception vector $45 (MFP channel 5 = Timer C,
|
||||||
|
| vector base $40 | 5), so this is a raw interrupt handler -- it must not
|
||||||
|
| disturb any register and must not return with RTS. It does neither: addq.l on
|
||||||
|
| an absolute address touches no register, and control leaves through a JMP to
|
||||||
|
| the previous handler, which owns the RTE.
|
||||||
|
|
|
||||||
|
| The JMP's operand is patched at install time by installTickIsr() in hal.c via
|
||||||
|
| the x68kTickChainAddr symbol below. Patching the operand rather than doing an
|
||||||
|
| indirect jump through a pointer is what keeps this register-free -- a 68000
|
||||||
|
| has no jmp ([abs]) addressing mode.
|
||||||
|
|
|
||||||
|
| GAS m68k syntax, ELF target -- symbols carry NO leading underscore.
|
||||||
|
|
||||||
|
.text
|
||||||
|
.even
|
||||||
|
.globl x68kTickIsr
|
||||||
|
.globl x68kTickChainAddr
|
||||||
|
|
||||||
|
x68kTickIsr:
|
||||||
|
addq.l #1,gX68kTicks
|
||||||
|
.word 0x4EF9 | jmp <abs.l>
|
||||||
|
x68kTickChainAddr:
|
||||||
|
.long 0 | patched with the previous handler
|
||||||
Loading…
Add table
Reference in a new issue