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,
|
||||
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
|
||||
#77 (the loader/segment-BSS + malloc-heap reservation that forced the
|
||||
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;
|
||||
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
|
||||
check -- **49/49** surface hashes match the frozen goldens in
|
||||
tests/goldens/uber/ (37 timed ops + 12 hashed checks; the PASS/FAIL
|
||||
check -- **51/51** surface hashes match the frozen goldens in
|
||||
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
|
||||
round-trip, arena churn, sprite-from-surface -- pass everywhere).
|
||||
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
|
||||
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)
|
||||
|
||||
Four measurement artifacts have shaped this table's history. Three are
|
||||
fixed, one is inherent -- know them before comparing rows:
|
||||
Five measurement artifacts have shaped this table's history. Three are
|
||||
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
|
||||
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%
|
||||
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
|
||||
ops/sec from `jlMillisElapsed` (refresh-independent on all four
|
||||
ports; identical to the old formula on IIgs where millis is
|
||||
GetTick-derived), and (b) ST `jlpFrameHz()` now reads the shifter
|
||||
ops/sec from `jlMillisElapsed` (its own hardware clock on the ST and
|
||||
DOS; derived from the frame counter on the IIgs -- identical to the
|
||||
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
|
||||
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
|
||||
|
|
@ -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
|
||||
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
|
||||
|
||||
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
|
||||
as below-floor. `tools/uber-perf-table` emits this table directly;
|
||||
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
|
||||
landed: fused IIgs sprite/fillRect entries, sprite-mark
|
||||
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
|
||||
|
||||
* **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
|
||||
variants plus group-window save/restore (previously stubs that fell
|
||||
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
|
||||
by the 49-hash gate.
|
||||
by the 51-hash gate.
|
||||
* **The below-floor cells are the standing punch list**, and after the
|
||||
2026-07-07 audio-tax fix the ST has NONE -- every remaining
|
||||
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).
|
||||
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
|
||||
four ports.
|
||||
five ports.
|
||||
* **Input predicates stay functions** (Phase 8 #40 decision): the
|
||||
post-clock-fix numbers (12K-77K ops/sec) put call overhead nowhere
|
||||
near a real frame budget; macro forms in the public header were
|
||||
|
|
|
|||
181
README.md
181
README.md
|
|
@ -1,12 +1,13 @@
|
|||
# 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:
|
||||
|
||||
- Apple IIgs (reference platform)
|
||||
- Commodore Amiga (A500 / 68000 baseline)
|
||||
- Atari ST (STF / 68000 baseline)
|
||||
- MS-DOS (386 / VGA, DJGPP)
|
||||
- Sharp X68000 (68000 @ 10 MHz, Human68k, elf2x68k)
|
||||
|
||||
The Apple IIgs defines the capability ceiling. Stronger platforms coast.
|
||||
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,
|
||||
plus the example programs (`hello`, `pattern`, `keys`, `serial`, `save`,
|
||||
`joy`, `sprite`, `audio`) for each.
|
||||
plus the example programs (`hello`, `pattern`, `draw`, `keys`, `serial`,
|
||||
`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
|
||||
|
|
@ -40,6 +45,43 @@ make atarist
|
|||
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
|
||||
|
||||
|
|
@ -47,12 +89,24 @@ make dos
|
|||
docs/ design and reference documentation
|
||||
include/joey/ public headers
|
||||
src/core/ portable library code
|
||||
src/codegen/ runtime sprite codegen (per-CPU emitters)
|
||||
src/port/<plat>/ per-platform HAL implementations
|
||||
src/shared68k/ assembly shared by Amiga and Atari ST
|
||||
src/generic/ portable-C default for every overridable op
|
||||
src/codegen/ shared sprite-codegen staging / compile machinery
|
||||
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/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
|
||||
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
|
||||
make/ per-target Makefile fragments
|
||||
build/<plat>/ per-target build outputs
|
||||
|
|
@ -77,8 +131,7 @@ is a quick reference. Every entry point is plain C, no C++ extensions.
|
|||
```c
|
||||
typedef struct {
|
||||
uint32_t codegenBytes; // runtime compiled-sprite cache size
|
||||
uint16_t maxSurfaces; // maximum concurrent surfaces
|
||||
uint32_t audioBytes; // audio sample / module RAM pool
|
||||
uint32_t audioBytes; // reserved; not yet consulted by any engine
|
||||
} jlConfigT;
|
||||
|
||||
bool jlInit (const jlConfigT *config);
|
||||
|
|
@ -87,9 +140,17 @@ const char *jlLastError (void);
|
|||
const char *jlPlatformName (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
|
||||
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
|
||||
and 16 palettes of 16 `$0RGB` colors. In-memory storage is
|
||||
target-native: chunky 4bpp packed on IIgs and DOS, native planar
|
||||
(separate bitplanes on Amiga, word-interleaved planes on Atari ST)
|
||||
on the 68k ports. The public API speaks in color indices (0..15) and
|
||||
hides the storage format.
|
||||
(separate bitplanes on Amiga and Sharp X68000, word-interleaved planes
|
||||
on Atari ST) on the 68k ports. The public API speaks in color indices
|
||||
(0..15) and hides the storage format.
|
||||
|
||||
```c
|
||||
#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
|
||||
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
|
||||
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 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
|
||||
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 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,
|
||||
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
|
||||
uint32_t jlSpriteCodegenBytesUsed (void);
|
||||
uint32_t jlSpriteCodegenBytesTotal (void);
|
||||
|
|
@ -273,7 +359,11 @@ Two blob formats:
|
|||
* **`.tbk` (tile bank)** -- one or more 8x8 tiles in per-target
|
||||
planar layout (Amiga plane-major; Atari ST row-major-with-planes-
|
||||
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
|
||||
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
|
||||
|
|
@ -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
|
||||
```
|
||||
|
||||
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
|
||||
`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
|
||||
or captured surface), use `jlSpriteCreateFromSurface` -- the
|
||||
|
|
@ -351,14 +444,18 @@ void jlJoystickReset (jlJoystickE js, uint8_t deadZone);
|
|||
|
||||
### Audio (`joey/audio.h`)
|
||||
|
||||
4-channel Protracker-style music plus four one-shot SFX slots. Module
|
||||
data must be the platform-native form produced by `tools/joeymod`
|
||||
(`.mod` for Amiga/DOS/ST; `.ntp` for IIgs; `.amod` if you want
|
||||
loop=false on Amiga). A failed `jlAudioInit` is non-fatal; the rest
|
||||
of the API stays callable as no-ops.
|
||||
4-channel Protracker-style music plus five one-shot SFX slots, three
|
||||
PSG-style tone voices and a noise channel. Module data must be the
|
||||
platform-native form produced by `tools/joeymod` (`.mod` for
|
||||
Amiga/DOS/ST/X68000; `.ntp` for IIgs; `.amod` if you want loop=false on
|
||||
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
|
||||
#define JOEY_AUDIO_SFX_SLOTS 4
|
||||
#define JOEY_AUDIO_SFX_SLOTS 5
|
||||
#define JOEY_AUDIO_VOICES 3
|
||||
|
||||
bool jlAudioInit (void);
|
||||
void jlAudioShutdown (void);
|
||||
|
|
@ -369,8 +466,24 @@ bool jlAudioIsPlayingMod (void);
|
|||
|
||||
void jlAudioPlaySfx (uint8_t slot, const uint8_t *sample,
|
||||
uint32_t length, uint16_t rateHz);
|
||||
void jlAudioPlaySfxStream (uint8_t slot, jlAudioStreamFillT fill, void *ctx,
|
||||
uint16_t rateHz);
|
||||
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);
|
||||
```
|
||||
|
||||
|
|
@ -418,12 +531,12 @@ void jlSerialFlush (void); // discard buffered input
|
|||
|
||||
Per-port device mapping:
|
||||
|
||||
| Device | IIgs | DOS | Amiga | Atari ST |
|
||||
|---------------------|-------------------------|-----------|-------------------|--------------|
|
||||
| `JL_SERIAL_DEFAULT` | 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 |
|
||||
| `JL_SERIAL_PRINTER` | printer port (SCC B) | COM1 | serial.device 0 | Modem/RS-232 |
|
||||
| `JL_SERIAL_SLOT` | 6551 SSC in slot `unit` | COM`unit` | serial.device `unit` | Modem/RS-232 |
|
||||
| Device | IIgs | DOS | Amiga | Atari ST | X68000 |
|
||||
|---------------------|-------------------------|-----------|-------------------|--------------|-----------------|
|
||||
| `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 | RS-232C (SCC A) |
|
||||
| `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 | RS-232C (SCC A) |
|
||||
|
||||
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
|
||||
|
|
@ -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` +
|
||||
`remove` + INT 21h on DOS; `mkdir` + GEMDOS `Dfree` on ST; dos.library
|
||||
`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
|
||||
IIgs save path under MAME. Full reference, per-port behavior, and porting
|
||||
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.
|
||||
|
||||
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`)
|
||||
|
||||
|
|
|
|||
|
|
@ -436,6 +436,10 @@
|
|||
#define JL_HAS_WAIT_VBL // _iocs_vsync
|
||||
#define JL_HAS_FRAME_COUNT
|
||||
#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.
|
||||
#define JL_HAS_DISK_FREE
|
||||
#define JL_HAS_SAVE_DELETE
|
||||
|
|
|
|||
|
|
@ -37,10 +37,10 @@ CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer \
|
|||
LDFLAGS := -lm
|
||||
|
||||
PORT_C_SRCS := $(wildcard $(SRC_DIR)/x68000/*.c)
|
||||
# Shared 68k planar asm (src/m68k/surface68k.s), already used by Amiga and ST:
|
||||
# circle outline and 4-plane span fill. Free reuse now the surface layout
|
||||
# matches.
|
||||
SHARED_S := $(wildcard $(SRC_68K)/*.s)
|
||||
# Port-local 68000 asm, if any lands. Empty today: SCB per-band palettes are
|
||||
# done in the pixel expansion (DOS-style), not with a raster ISR, so the port is
|
||||
# pure C again.
|
||||
PORT_S_SRCS := $(wildcard $(SRC_DIR)/x68000/*.s)
|
||||
|
||||
# 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
|
||||
|
|
@ -63,6 +63,7 @@ LIB_OBJS := \
|
|||
$(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)/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/codegen/spriteCompile.o \
|
||||
$(BUILD)/obj/codegen/spriteStage.o
|
||||
|
|
@ -89,6 +90,7 @@ $(LIBXMP_AR): $(LIBXMP_OBJS)
|
|||
|
||||
LIB := $(LIBDIR)/libjoey.a
|
||||
|
||||
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
|
||||
SERIAL_SRC := $(EXAMPLES)/serial/serial.c
|
||||
UBER_SRC := $(EXAMPLES)/uber/uber.c
|
||||
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
|
||||
|
|
@ -99,7 +101,7 @@ all x68000: x68000-lib x68000-examples
|
|||
|
||||
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
|
||||
@mkdir -p $(dir $@)
|
||||
|
|
@ -109,6 +111,12 @@ $(BUILD)/obj/generic/%.o: $(SRC_DIR)/generic/%.c
|
|||
@mkdir -p $(dir $@)
|
||||
$(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
|
||||
@mkdir -p $(dir $@)
|
||||
$(X68K_CC) $(CFLAGS) -c $< -o $@
|
||||
|
|
@ -129,6 +137,12 @@ $(LIB): $(LIB_OBJS)
|
|||
@mkdir -p $(dir $@)
|
||||
$(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)
|
||||
@mkdir -p $(dir $@)
|
||||
$(X68K_CC) $(CFLAGS) $(SERIAL_SRC) $(LIB) $(LIBXMP_AR) $(LDFLAGS) -o $@
|
||||
|
|
|
|||
320
src/x68000/hal.c
320
src/x68000/hal.c
|
|
@ -20,6 +20,9 @@
|
|||
#include <x68k/iocs.h>
|
||||
|
||||
#include "port.h"
|
||||
#include <stddef.h>
|
||||
|
||||
#include "joey/debug.h"
|
||||
#include <string.h>
|
||||
|
||||
#include "surfaceInternal.h"
|
||||
|
|
@ -34,10 +37,11 @@
|
|||
// width, so the row stride is a constant, not a function of SURFACE_WIDTH.
|
||||
#define X68K_GVRAM_STRIDE 512u
|
||||
|
||||
// _iocs_crtmod screen mode. 12 = 512x512, 16 colours, 31 kHz. The library's
|
||||
// 320x200 stage is drawn into the top-left corner; there is no 320x200 mode on
|
||||
// this machine, so some form of letterboxing is unavoidable.
|
||||
#define X68K_CRTMOD_512_16 12
|
||||
// _iocs_crtmod screen mode. 13 = 512x512, 256 colours, 31 kHz -- same timing as
|
||||
// mode 12 (512x512/16) one slot below it, but with the 256-entry graphics
|
||||
// palette this port needs for SCB per-band palettes. There is no 320x200 mode on
|
||||
// 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.
|
||||
#define X68K_KEYGROUP_COUNT 15
|
||||
|
|
@ -49,6 +53,57 @@
|
|||
#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 ---------------------------------------------------------
|
||||
|
||||
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.
|
||||
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 vdispPoll(void);
|
||||
|
||||
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.
|
||||
gPrevSsp = _dos_super(0);
|
||||
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
|
||||
// NOTE: installing vdispHandler via _iocs_vdispst HANGS the machine --
|
||||
// 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
|
||||
// from the interrupt and this one returns with RTS from a context IOCS does
|
||||
// not expect. Getting it right needs an asm thunk (save d0-d1/a0-a1, call,
|
||||
// restore, correct return), which is worth doing but is not a one-liner.
|
||||
// IOCS does NOT wrap the handler: _VDISPST writes the pointer straight into
|
||||
// exception vector $134 (iplrom.dat $FF9DC8), exactly as _CRTCRAS does for
|
||||
// $138. So the handler must be a raw interrupt routine -- preserve every
|
||||
// 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.
|
||||
gVdispInstalled = false;
|
||||
installTickIsr();
|
||||
gModeSet = 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 --------------------------------------------------------------
|
||||
|
||||
// Copy the surface's four planes into the TEXT PLANE at $E00000.
|
||||
|
|
@ -124,26 +316,33 @@ void jlpShutdown(void) {
|
|||
// Dirty rows only, and only the marked band within a row.
|
||||
void jlpPresent(const jlSurfaceT *src) {
|
||||
X68kPlanarT *pd;
|
||||
volatile uint32_t *wordDst;
|
||||
const uint8_t *p0;
|
||||
const uint8_t *p1;
|
||||
const uint8_t *p2;
|
||||
const uint8_t *p3;
|
||||
uint32_t rowBase;
|
||||
uint16_t y;
|
||||
uint16_t plane;
|
||||
uint16_t firstByte;
|
||||
uint16_t rowBytes;
|
||||
const uint8_t *s;
|
||||
uint8_t *d;
|
||||
uint32_t pal2;
|
||||
uint16_t b;
|
||||
uint16_t hi0, hi1, hi2, hi3;
|
||||
uint16_t lo0, lo1, lo2, lo3;
|
||||
uint8_t palBase;
|
||||
|
||||
if (src == NULL) {
|
||||
return;
|
||||
}
|
||||
vdispPoll(); // keep the frame counter honest: see the note on vdispPoll
|
||||
// Push the palette when it has changed. Without this NOTHING ever uploads
|
||||
// colours and the display keeps whatever Human68k left in the text palette.
|
||||
// The text plane has ONE 16-entry palette, so SCB per-scanline palette
|
||||
// selection cannot be honoured -- palette 0 is used, matching the Atari ST.
|
||||
if (gStagePaletteDirty) {
|
||||
uint16_t c;
|
||||
for (c = 0u; c < 16u; c++) {
|
||||
X68K_TEXT_PALETTE[c] = x68kColorFromRgb12(src->palette[0][c]);
|
||||
// Reload the 256-entry graphics palette when a palette changed. The SCB
|
||||
// flag is NOT consumed: the per-line palette choice is applied below in the
|
||||
// pixel expansion, which reads src->scb directly (as DOS does).
|
||||
if (!gSpreadBuilt) {
|
||||
buildSpreadTables();
|
||||
}
|
||||
if (gStagePaletteDirty) {
|
||||
uploadGfxPalette(src);
|
||||
gStagePaletteDirty = false;
|
||||
}
|
||||
pd = x68kSurfacePlanar(src);
|
||||
|
|
@ -160,14 +359,39 @@ void jlpPresent(const jlSurfaceT *src) {
|
|||
if ((uint16_t)(firstByte + rowBytes) > X68K_BYTES_PER_ROW) {
|
||||
rowBytes = (uint16_t)(X68K_BYTES_PER_ROW - firstByte);
|
||||
}
|
||||
for (plane = 0u; plane < X68K_BITPLANES; plane++) {
|
||||
s = pd->planes[plane] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte;
|
||||
// Centred: shift down X68K_ORIGIN_Y rows and right X68K_ORIGIN_BYTE
|
||||
// bytes. X is byte-aligned (96/8 = 12) so no bit shifting is needed.
|
||||
d = (uint8_t *)(X68K_TVRAM_PLANE0 + ((uint32_t)plane * X68K_TVRAM_PLANE_STEP)
|
||||
+ x68kTvramOffset(0u, (uint16_t)(y + X68K_ORIGIN_Y))
|
||||
+ X68K_ORIGIN_BYTE + firstByte);
|
||||
memcpy(d, s, rowBytes);
|
||||
// This row's band selects the high nibble of every pixel byte -- the
|
||||
// whole SCB mechanism, exactly src/dos/hal.c:83.
|
||||
palBase = (uint8_t)(src->scb[y] << 4);
|
||||
p0 = pd->planes[0] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte;
|
||||
p1 = pd->planes[1] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte;
|
||||
p2 = pd->planes[2] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte;
|
||||
p3 = pd->planes[3] + ((uint32_t)y * X68K_BYTES_PER_ROW) + firstByte;
|
||||
// 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,10 +563,25 @@ 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) {
|
||||
vdispPoll();
|
||||
if (!gTickHooked) {
|
||||
return gFrameCount;
|
||||
}
|
||||
return (uint16_t)((jlpMillisElapsed() * (uint32_t)jlpFrameHz()) / 1000ul);
|
||||
}
|
||||
|
||||
|
||||
#ifdef JOEY_LOG_SERIAL_TEE
|
||||
|
|
@ -371,6 +610,29 @@ void jlpLogTee(const char *text) {
|
|||
#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) {
|
||||
// 31 kHz modes on this machine run at ~55.5 Hz rather than 60. Reported
|
||||
// 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