X68k, ST, and Amiga optimizations.
This commit is contained in:
parent
477d39f7e9
commit
1a7c7ec05a
37 changed files with 3393 additions and 1413 deletions
|
|
@ -14,15 +14,18 @@ output level running from one utterance into the next, so a phrase whose
|
|||
first word ends on the low level plays its second word inverted. Both
|
||||
versions are stored and the player picks by the running parity.
|
||||
speech.bin (little-endian):
|
||||
"STSP" u8 version(2) u8 pitchBase u8 pitchCount u8 count
|
||||
"STSP" u8 version(3) u8 pitchBase u8 pitchCount u8 count
|
||||
u8 format (0 = signed +/-100, 1 = 0x80-biased for the IIgs raw stream)
|
||||
u8 pad[3] u32 dataBytes
|
||||
entries[pitchCount * count * 2]: u8 code, u8 parity, u16 samples,
|
||||
u32 offset -- polarity is the innermost index (0 = as decoded,
|
||||
1 = inverted). parity = 1 when the utterance ends on the
|
||||
opposite level from the one it started on.
|
||||
u8 polarities (1 or 2) u8 pad[2] u32 dataBytes
|
||||
entries[pitchCount * count * polarities]: u8 code, u8 parity,
|
||||
u16 samples, u32 offset -- polarity is the innermost index
|
||||
(0 = as decoded, 1 = inverted). parity = 1 when the utterance
|
||||
ends on the opposite level from the one it started on.
|
||||
data: sample bytes
|
||||
usage: genSpeechPcm.py stSpeechData.c speech.bin [--biased]
|
||||
With --single only polarity 0 is written, halving the file; the player
|
||||
negates the samples itself when the running parity calls for it. The
|
||||
X68000 needs that: its Human68k floppy cannot spare the other 60 KB.
|
||||
usage: genSpeechPcm.py stSpeechData.c speech.bin [--biased] [--single]
|
||||
"""
|
||||
import re
|
||||
import struct
|
||||
|
|
@ -77,6 +80,8 @@ def decode(rle, perOn):
|
|||
def main():
|
||||
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
biased = "--biased" in sys.argv[1:]
|
||||
single = "--single" in sys.argv[1:]
|
||||
polarities = 1 if single else 2
|
||||
src, dst = args[0], args[1]
|
||||
entries, data = parseSource(src)
|
||||
records = []
|
||||
|
|
@ -86,7 +91,7 @@ def main():
|
|||
for code, off, length in entries:
|
||||
samples, level = decode(data[off:off + length], perOn)
|
||||
parity = 1 if level < 0 else 0
|
||||
for pol in range(2):
|
||||
for pol in range(polarities):
|
||||
pcm = bytearray()
|
||||
for s in samples:
|
||||
v = -s if pol else s
|
||||
|
|
@ -94,12 +99,14 @@ def main():
|
|||
records.append((code, parity, len(samples), len(blob)))
|
||||
blob += pcm
|
||||
with open(dst, "wb") as f:
|
||||
f.write(b"STSP" + struct.pack("<BBBBBBBBI", 2, PITCH_BASE, PITCH_COUNT, len(entries), 1 if biased else 0, 0, 0, 0, len(blob)))
|
||||
f.write(b"STSP" + struct.pack("<BBBBBBBBI", 3, PITCH_BASE, PITCH_COUNT, len(entries), 1 if biased else 0, polarities, 0, 0, len(blob)))
|
||||
for code, parity, n, off in records:
|
||||
f.write(struct.pack("<BBHI", code, parity, n, off))
|
||||
f.write(blob)
|
||||
total = sum(n for _, _, n, _ in records)
|
||||
print(f"{dst}: {len(entries)} utterances x {PITCH_COUNT} pitches x 2 polarities, {total} samples, {len(blob)} data bytes, {'biased' if biased else 'signed'}")
|
||||
print(f"{dst}: {len(entries)} utterances x {PITCH_COUNT} pitches x {polarities} "
|
||||
f"polarit{'y' if polarities == 1 else 'ies'}, {total} samples, {len(blob)} data bytes, "
|
||||
f"{'biased' if biased else 'signed'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@
|
|||
#define ST_GAME_OVER_TICKS 60u
|
||||
// Menu joystick repeat delay ($4101 with X = $82).
|
||||
#define ST_MENU_REPEAT_TICKS 8u
|
||||
// How long a fatal load message stays up, in display frames, and how many
|
||||
// must pass before a joystick press can cut it short (a button held from
|
||||
// before must not skip the message).
|
||||
#define ST_FATAL_HOLD_FRAMES 600u
|
||||
#define ST_FATAL_ARM_FRAMES 30u
|
||||
|
||||
// The immortal cabbies ($4A89 image, $4E35 screen).
|
||||
#define ST_HS_ENTRIES 10u
|
||||
|
|
@ -87,6 +92,7 @@ typedef struct {
|
|||
uint8_t hsCursor; // $4F4D the blinking cursor's current glyph
|
||||
uint8_t hsBlinkTicks; // ticks toward the next cursor flip
|
||||
bool paused; // $4FCB: SHIFT is holding the game
|
||||
bool fatal; // an asset on the disk would not load
|
||||
} StGameT;
|
||||
|
||||
// The immortal cabbies ($4A89): ten entries of a right-aligned "dddd.dd"
|
||||
|
|
@ -150,6 +156,14 @@ static const uint8_t *kAboutLines[7] = {
|
|||
(const uint8_t *)"ALSO TRY RESCUE SQUAD BY JOHN KUTCHER",
|
||||
};
|
||||
static const uint8_t kTextReturn[] = "PRESS FIRE TO RETURN";
|
||||
// Fatal load messages. Two short centred lines in the game's own charset,
|
||||
// upper case because the C64 font has no lower case in this bank.
|
||||
static const char kTextFatalMemory[] = "OUT OF MEMORY";
|
||||
static const char kTextFatalSpeech[] = "SPEECH WILL NOT LOAD";
|
||||
static const char kTextFatalSprites[] = "SPRITES WILL NOT COMPILE";
|
||||
static const char kTextFatalLoad[] = "DISK ERROR";
|
||||
static const char kTextFatalTitle[] = "CANNOT LOAD TITLE SCREEN";
|
||||
static const char kTextFatalLevel[] = "CANNOT LOAD LEVEL";
|
||||
|
||||
static StGameT gGame;
|
||||
static StSimT gSim;
|
||||
|
|
@ -163,6 +177,8 @@ static StLevelT gLevel;
|
|||
|
||||
static void blankScreen(StSimT *sim, uint8_t border, uint8_t bg);
|
||||
static void enterTitle(void);
|
||||
static void fatalHold(void);
|
||||
static void fatalStop(const char *line1, const char *line2);
|
||||
static void gameOverEnter(void);
|
||||
static void gameOverFinish(void);
|
||||
static void highScoreDraw(void);
|
||||
|
|
@ -207,9 +223,10 @@ static void blankScreen(StSimT *sim, uint8_t border, uint8_t bg) {
|
|||
static void enterTitle(void) {
|
||||
gSim.demoMode = 0u;
|
||||
if (!stLevelLoad(&gLevel, "levels/title.dat")) {
|
||||
ST_LOG("spacetaxi: ! title load FAILED");
|
||||
fatalStop(kTextFatalLoad, kTextFatalTitle);
|
||||
return;
|
||||
}
|
||||
stRenderLevelCels(gStage, 0); // the last level's cels go
|
||||
(void)stRenderLevelCels(gStage, 0); // the last level's cels go
|
||||
stTitleEnter(&gSim, &gLevel);
|
||||
stRenderSceneChanged(&gSim);
|
||||
stRenderTitleLogo(&gSim);
|
||||
|
|
@ -442,6 +459,45 @@ static void runNameEntry(uint8_t ticks) {
|
|||
}
|
||||
|
||||
|
||||
// Hold a fatal message on screen long enough to be read, then let the
|
||||
// caller shut down. FIRE or a key cuts the wait short.
|
||||
static void fatalHold(void) {
|
||||
uint8_t last = (uint8_t)jlFrameCount();
|
||||
uint16_t held = 0u;
|
||||
|
||||
// Count display frames, not loop turns: presenting an unchanged
|
||||
// screen costs almost nothing, so a turn count would run out in well
|
||||
// under a second on the faster ports.
|
||||
while (held < ST_FATAL_HOLD_FRAMES) {
|
||||
uint8_t now;
|
||||
|
||||
jlInputPoll();
|
||||
if (held > ST_FATAL_ARM_FRAMES && (joyMask() & 0x1Fu) != 0u) {
|
||||
return;
|
||||
}
|
||||
now = (uint8_t)jlFrameCount();
|
||||
held = (uint16_t)(held + (uint8_t)(now - last));
|
||||
last = now;
|
||||
jlStagePresent();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// An asset the game ships with will not load. Say so on screen and stop:
|
||||
// a mute game, or one running every sprite through the interpreter, reads
|
||||
// as a slow machine rather than a broken install, and that is worse than
|
||||
// not starting.
|
||||
static void fatalStop(const char *line1, const char *line2) {
|
||||
ST_LOG("spacetaxi: ! FATAL -- %s / %s", line1, line2);
|
||||
ST_LOG_FLUSH();
|
||||
stAudioSilence();
|
||||
stAudioNoise(false);
|
||||
jlMusicStop();
|
||||
stRenderFatal(gStage, line1, line2);
|
||||
gGame.fatal = true;
|
||||
}
|
||||
|
||||
|
||||
// $5FBB -- "GAME OVER!" over the play field, sprites hidden.
|
||||
static void gameOverEnter(void) {
|
||||
stSimDrawText(&gSim, 14u, 10u, kTextBlank12, 1u);
|
||||
|
|
@ -505,8 +561,7 @@ static bool loadLevel(uint8_t index) {
|
|||
ST_LOG("spacetaxi: ! cannot load %s", path);
|
||||
return false;
|
||||
}
|
||||
stRenderLevelCels(gStage, &gLevel);
|
||||
return true;
|
||||
return stRenderLevelCels(gStage, &gLevel);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -842,12 +897,25 @@ static void startDemoScreen(void) {
|
|||
(void)stSimAdvancePlayer(&gSim);
|
||||
gSim.levelState++;
|
||||
if (!loadLevel(e->levelIndex)) {
|
||||
enterTitle();
|
||||
fatalStop(kTextFatalLoad, kTextFatalLevel);
|
||||
return;
|
||||
}
|
||||
// $40CA: blank the character screen first. Without this the two
|
||||
// green lines land on top of the title, which is still up -- logo,
|
||||
// byline, copyright and all -- where the original shows them on a
|
||||
// cleared screen (observed on real hardware, 2026-09-23). Same
|
||||
// clear stIntroEnter does, and it leaves the sprite mask alone so
|
||||
// the line below still keeps sprites 0..2.
|
||||
memset(gSim.screen, ST_CHAR_SPACE, ST_SCREEN_CELLS);
|
||||
memset(gSim.color, 0, ST_SCREEN_CELLS);
|
||||
stSimDirtyAll(&gSim);
|
||||
stSimGlyphReset(&gSim);
|
||||
gSim.charDirtyAll = true;
|
||||
gSim.charDirtyCount = 0u;
|
||||
stSimDrawText(&gSim, 10u, 8u, kTextGetReady, 5u);
|
||||
stSimDrawText(&gSim, 10u, 10u, kTextOnTheTaxi, 5u);
|
||||
gSim.frame.enableMask &= 0x07u;
|
||||
stRenderSceneChanged(&gSim);
|
||||
gGame.waitTicks = ST_GET_READY_TICKS;
|
||||
gGame.state = ST_STATE_GET_READY;
|
||||
}
|
||||
|
|
@ -881,7 +949,7 @@ static void startNewScreen(void) {
|
|||
}
|
||||
gGame.pendingLevel = next;
|
||||
if (!loadLevel(next)) {
|
||||
enterTitle();
|
||||
fatalStop(kTextFatalLoad, kTextFatalLevel);
|
||||
return;
|
||||
}
|
||||
gSim.levelState++;
|
||||
|
|
@ -931,21 +999,31 @@ int main(void) {
|
|||
memset(&gSim, 0, sizeof(gSim));
|
||||
stRenderInit(stage);
|
||||
stRenderLoading(stage);
|
||||
stAudioInit();
|
||||
if (!stAudioInit()) {
|
||||
fatalStop(kTextFatalMemory, kTextFatalSpeech);
|
||||
}
|
||||
// Boot: the playback buffer as the C64 leaves it, and the state a
|
||||
// fresh machine has when the first title intro starts ($4092 + the
|
||||
// title's own sparkle/walk history from the dump).
|
||||
stSimNewGame(&gSim, 1u, false);
|
||||
stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES);
|
||||
gSim.waveIdx = 3u;
|
||||
stRenderPrewarm(stage, &gSim);
|
||||
if (!gGame.fatal && !stRenderPrewarm(stage, &gSim)) {
|
||||
fatalStop(kTextFatalMemory, kTextFatalSprites);
|
||||
}
|
||||
highScoreLoad(); // $23E5
|
||||
if (!gGame.fatal) {
|
||||
enterTitle();
|
||||
}
|
||||
gGame.lastFrame = (uint8_t)jlFrameCount();
|
||||
|
||||
for (;;) {
|
||||
uint8_t ticks;
|
||||
|
||||
if (gGame.fatal) {
|
||||
fatalHold();
|
||||
break;
|
||||
}
|
||||
jlInputPoll();
|
||||
// $4FCB runStopWatcher, on the game tick, the intro and the menus:
|
||||
// SHIFT held freezes everything with the sound gated off and any
|
||||
|
|
@ -1056,5 +1134,5 @@ int main(void) {
|
|||
stAudioShutdown();
|
||||
stRenderShutdown();
|
||||
jlShutdown();
|
||||
return 0;
|
||||
return gGame.fatal ? 1 : 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,11 @@
|
|||
#if defined(__W65816__) || defined(JOEYLIB_PLATFORM_X68000)
|
||||
#define ST_LOG_RESET() ((void)0)
|
||||
#define ST_LOG(...) ((void)0)
|
||||
#define ST_LOG_FLUSH() ((void)0)
|
||||
#else
|
||||
#define ST_LOG_RESET() jlLogReset()
|
||||
#define ST_LOG(...) jlLogF(__VA_ARGS__)
|
||||
#define ST_LOG_FLUSH() jlLogFlush()
|
||||
#endif
|
||||
|
||||
// Level loading through the JoeyLib data path (DATA/levels/...).
|
||||
|
|
@ -54,12 +56,22 @@ void stRenderTitleLogo(StSimT *sim);
|
|||
// animates without a single pixel moving. Call after
|
||||
// stRenderSceneChanged, which turns it back off.
|
||||
void stRenderIntroStars(void);
|
||||
void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim);
|
||||
// Fill the stage with a two-line message and present it. Used when an
|
||||
// asset the game ships with will not load, so a player sees why the
|
||||
// game stopped instead of a silently degraded ride.
|
||||
void stRenderFatal(jlSurfaceT *stage, const char *line1, const char *line2);
|
||||
// Compile every always-on cel. False when a shipped asset is present but
|
||||
// unusable, so main can stop with a message instead of running the game
|
||||
// on interpreted cels.
|
||||
bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim);
|
||||
// Ready a level's hook cels before its ride (evicts the previous level's).
|
||||
void stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level);
|
||||
// False only when this level's baked bank exists and will not load; a
|
||||
// port that ships no per-level banks compiles them and returns true.
|
||||
bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level);
|
||||
|
||||
// Audio sink setup and the per-tick release timers ($4320).
|
||||
void stAudioInit(void);
|
||||
// False when the shipped speech blob will not load.
|
||||
bool stAudioInit(void);
|
||||
void stAudioShutdown(void);
|
||||
void stAudioTick(void);
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,13 @@
|
|||
|
||||
#define ST_ATTEN_LOUD 4u
|
||||
#define ST_ATTEN_SOFT 8u
|
||||
// The jet's noise pitch: jlAudioNoise takes 0..31 in YM noise-period
|
||||
// style, where 31 is the lowest rumble (0 is the brightest hiss).
|
||||
#define ST_NOISE_JET_PITCH 31u
|
||||
// Noise sound effects (the crash and the wreck's impact) use the same
|
||||
// deep end of the range. The C64 takes the rate from the program's
|
||||
// frequency word; this is the one simplification in that mapping.
|
||||
#define ST_NOISE_SFX_PITCH 31u
|
||||
#define ST_ATTEN_OFF 15u
|
||||
|
||||
// NTSC SID: Hz = word * 1022727 / 16777216 = word * 0.06096.
|
||||
|
|
@ -56,10 +63,16 @@
|
|||
// the ride half a second on every "HEY TAXI"; the 1-bit intermediate
|
||||
// format that followed still needed a per-byte expansion loop.
|
||||
#define ST_SPEECH_FILE "speech.bin"
|
||||
#define ST_SPEECH_VERSION 2u
|
||||
#define ST_SPEECH_VERSION 3u
|
||||
#define ST_SPEECH_HEADER_BYTES 16u
|
||||
#define ST_SPEECH_ENTRY_BYTES 8u
|
||||
#define ST_SPEECH_POLARITIES 2u
|
||||
// speech.bin carries each utterance in one polarity or two. Two is the
|
||||
// cheap case -- the player just picks the baked variant -- but it doubles
|
||||
// the file, and the X68000's Human68k floppy cannot spare the other
|
||||
// 60 KB, so that port bakes one and the player negates the samples
|
||||
// itself. Negation is the same byte arithmetic in both sample formats:
|
||||
// signed 0 - v, and 0x80-biased 0x100 - v, are one operation.
|
||||
#define ST_SPEECH_POLARITIES_MAX 2u
|
||||
#define ST_SPEECH_FORMAT_SIGNED 0u
|
||||
#define ST_SPEECH_FORMAT_BIASED 1u
|
||||
#define ST_SPEECH_LOAD_CHUNK 16384u
|
||||
|
|
@ -88,6 +101,9 @@ typedef struct {
|
|||
static StSfxSlotT gSfx; // SID voice 1 (programs + sweep)
|
||||
static StSfxSlotT gTone; // SID voice 2
|
||||
static bool gNoiseOn;
|
||||
// Release countdown for a NOISE sound effect, in game ticks, mirroring
|
||||
// the two tone slots' ticksLeft. 0 = no burst running.
|
||||
static uint16_t gNoiseTicks;
|
||||
|
||||
// Speech playback state. The fill callback walks a queue of utterances so
|
||||
// a phrase like "HT" ("Hey" + "Taxi") runs as one continuous stream
|
||||
|
|
@ -109,7 +125,9 @@ static const uint8_t *gSpeechPcm; // current utterance's samples
|
|||
static uint16_t gSpeechSample; // next sample index in it
|
||||
static uint16_t gSpeechSamples; // its length
|
||||
static uint8_t gSpeechParity; // it ends on the opposite level
|
||||
static uint8_t gSpeechPol; // 0 / 1: which baked polarity the next utterance plays
|
||||
static uint8_t gSpeechPol; // 0 / 1: the polarity the next utterance plays in
|
||||
static uint8_t gSpeechPolarities; // how many the file carries, 1 or 2
|
||||
static bool gSpeechInvert; // negate on the way out (1-polarity file, odd polarity)
|
||||
// Copied-ahead PCM ring for the stream (see speechFill). Allocated by
|
||||
// stAudioInit rather than static: 8 KB of BSS is a fifth of the IIgs
|
||||
// entry bank, which text, rodata, BSS and the C heap all share.
|
||||
|
|
@ -121,7 +139,7 @@ static bool gSpeechDecoding; // the decoder still has stream left
|
|||
|
||||
|
||||
static uint16_t sidHz(uint16_t word);
|
||||
static void speechLoad(void);
|
||||
static bool speechLoad(void);
|
||||
static bool speechNext(void);
|
||||
static void speechArm(void);
|
||||
static uint16_t speechDecode(uint8_t *dst, uint16_t count);
|
||||
|
|
@ -145,7 +163,7 @@ static uint16_t sidHz(uint16_t word) {
|
|||
// is empty, which ends the stream.
|
||||
// DATA/speech.bin into one jlAlloc block; without it the game is mute
|
||||
// but otherwise fine.
|
||||
static void speechLoad(void) {
|
||||
static bool speechLoad(void) {
|
||||
FILE *fp = jlDataOpen(ST_SPEECH_FILE, "rb");
|
||||
uint8_t head[ST_SPEECH_HEADER_BYTES];
|
||||
uint8_t format;
|
||||
|
|
@ -155,22 +173,30 @@ static void speechLoad(void) {
|
|||
|
||||
gSpeechBlob = 0;
|
||||
if (fp == 0) {
|
||||
return;
|
||||
ST_LOG("staudio: ! cannot open DATA/%s", ST_SPEECH_FILE);
|
||||
return false;
|
||||
}
|
||||
format = (jlAudioSfxRawFormat() == JL_SFX_RAW_UNSIGNED8_NOZERO) ? ST_SPEECH_FORMAT_BIASED : ST_SPEECH_FORMAT_SIGNED;
|
||||
if (fread(head, 1u, sizeof(head), fp) != sizeof(head) || head[0] != 'S' || head[1] != 'T' || head[2] != 'S' || head[3] != 'P' || head[4] != ST_SPEECH_VERSION || head[8] != format) {
|
||||
if (fread(head, 1u, sizeof(head), fp) != sizeof(head) || head[0] != 'S' || head[1] != 'T' || head[2] != 'S' || head[3] != 'P' || head[4] != ST_SPEECH_VERSION || head[8] != format
|
||||
|| head[9] == 0u || head[9] > ST_SPEECH_POLARITIES_MAX) {
|
||||
ST_LOG("staudio: ! speech.bin rejected (want v%u fmt%u)", ST_SPEECH_VERSION, format);
|
||||
fclose(fp);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
gSpeechPolarities = head[9];
|
||||
gSpeechPitchBase = head[5];
|
||||
gSpeechPitchCount = head[6];
|
||||
gSpeechCount = head[7];
|
||||
dataBytes = (uint32_t)head[12] | ((uint32_t)head[13] << 8) | ((uint32_t)head[14] << 16) | ((uint32_t)head[15] << 24);
|
||||
total = ST_SPEECH_HEADER_BYTES + (uint32_t)gSpeechPitchCount * gSpeechCount * ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES + dataBytes;
|
||||
total = ST_SPEECH_HEADER_BYTES + (uint32_t)gSpeechPitchCount * gSpeechCount * gSpeechPolarities * ST_SPEECH_ENTRY_BYTES + dataBytes;
|
||||
gSpeechBlob = (uint8_t *)jlAlloc(total);
|
||||
if (gSpeechBlob == 0) {
|
||||
// The blob is ~120 KB of sample bytes, so on the tighter
|
||||
// machines this is the failure most likely to bite. It stops
|
||||
// the game: a silent Space Taxi is a broken Space Taxi.
|
||||
ST_LOG("staudio: ! speech alloc FAILED (%lu bytes)", (unsigned long)total);
|
||||
fclose(fp);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
memcpy(gSpeechBlob, head, sizeof(head));
|
||||
// The block spans banks on the IIgs; read it in bank-safe pieces.
|
||||
|
|
@ -182,6 +208,7 @@ static void speechLoad(void) {
|
|||
want = ST_SPEECH_LOAD_CHUNK;
|
||||
}
|
||||
if (fread(gSpeechBlob + done, 1u, (size_t)want, fp) != (size_t)want) {
|
||||
ST_LOG("staudio: ! speech read FAILED at %lu of %lu", (unsigned long)done, (unsigned long)total);
|
||||
jlFree(gSpeechBlob);
|
||||
gSpeechBlob = 0;
|
||||
break;
|
||||
|
|
@ -189,6 +216,11 @@ static void speechLoad(void) {
|
|||
done += want;
|
||||
}
|
||||
fclose(fp);
|
||||
if (gSpeechBlob == 0) {
|
||||
return false;
|
||||
}
|
||||
ST_LOG("staudio: speech %lu bytes, %u utterances x %u pitches x %u polarities", (unsigned long)total, gSpeechCount, gSpeechPitchCount, gSpeechPolarities);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -206,19 +238,24 @@ static bool speechNext(void) {
|
|||
}
|
||||
while (gSpeechHead != gSpeechTail) {
|
||||
uint8_t code = gSpeechQueue[gSpeechHead];
|
||||
const uint8_t *e = gSpeechBlob + ST_SPEECH_HEADER_BYTES + (uint16_t)row * gSpeechCount * ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES;
|
||||
const uint8_t *e = gSpeechBlob + ST_SPEECH_HEADER_BYTES + (uint16_t)row * gSpeechCount * gSpeechPolarities * ST_SPEECH_ENTRY_BYTES;
|
||||
uint8_t k;
|
||||
|
||||
gSpeechHead = (uint8_t)((gSpeechHead + 1u) % ST_SPEECH_QUEUE);
|
||||
for (k = 0u; k < gSpeechCount; k++, e += ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES) {
|
||||
for (k = 0u; k < gSpeechCount; k++, e += gSpeechPolarities * ST_SPEECH_ENTRY_BYTES) {
|
||||
if (e[0] == code) {
|
||||
const uint8_t *v = e + (uint16_t)gSpeechPol * ST_SPEECH_ENTRY_BYTES;
|
||||
// Two polarities baked: take the variant. One: take the
|
||||
// only one and negate it on the way out when the running
|
||||
// polarity is the odd one.
|
||||
uint8_t pol = (gSpeechPolarities == 2u) ? gSpeechPol : 0u;
|
||||
const uint8_t *v = e + (uint16_t)pol * ST_SPEECH_ENTRY_BYTES;
|
||||
uint32_t offset = (uint32_t)v[4] | ((uint32_t)v[5] << 8) | ((uint32_t)v[6] << 16) | ((uint32_t)v[7] << 24);
|
||||
|
||||
gSpeechInvert = (gSpeechPolarities == 1u && gSpeechPol != 0u);
|
||||
gSpeechParity = v[1];
|
||||
gSpeechSamples = (uint16_t)(v[2] | ((uint16_t)v[3] << 8));
|
||||
gSpeechSample = 0u;
|
||||
gSpeechPcm = gSpeechBlob + ST_SPEECH_HEADER_BYTES + (uint32_t)gSpeechPitchCount * gSpeechCount * ST_SPEECH_POLARITIES * ST_SPEECH_ENTRY_BYTES + offset;
|
||||
gSpeechPcm = gSpeechBlob + ST_SPEECH_HEADER_BYTES + (uint32_t)gSpeechPitchCount * gSpeechCount * gSpeechPolarities * ST_SPEECH_ENTRY_BYTES + offset;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -252,7 +289,17 @@ static uint16_t speechDecode(uint8_t *dst, uint16_t count) {
|
|||
if (take > left) {
|
||||
take = left;
|
||||
}
|
||||
if (gSpeechInvert) {
|
||||
const uint8_t *from = gSpeechPcm + gSpeechSample;
|
||||
uint8_t *to = dst + n;
|
||||
uint16_t i;
|
||||
|
||||
for (i = 0u; i < take; i++) {
|
||||
to[i] = (uint8_t)(0u - (uint16_t)from[i]);
|
||||
}
|
||||
} else {
|
||||
jlMemCopy(dst + n, gSpeechPcm + gSpeechSample, take);
|
||||
}
|
||||
gSpeechSample = (uint16_t)(gSpeechSample + take);
|
||||
n = (uint16_t)(n + take);
|
||||
}
|
||||
|
|
@ -368,14 +415,27 @@ void stAudioTick(void) {
|
|||
jlAudioVoice(gTone.voice, 0u, ST_ATTEN_OFF);
|
||||
}
|
||||
}
|
||||
if (gNoiseTicks != 0u) {
|
||||
gNoiseTicks--;
|
||||
if (gNoiseTicks == 0u) {
|
||||
gNoiseOn = false;
|
||||
jlAudioNoise(0u, ST_ATTEN_OFF);
|
||||
} else {
|
||||
// Re-assert: one call buys about 80 ms on the IIgs.
|
||||
jlAudioNoise(ST_NOISE_SFX_PITCH, ST_ATTEN_LOUD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void stAudioInit(void) {
|
||||
bool stAudioInit(void) {
|
||||
bool ok;
|
||||
|
||||
(void)jlAudioInit();
|
||||
gSpeechStage = (uint8_t *)jlAlloc(ST_SPEECH_STAGE_BYTES);
|
||||
speechLoad();
|
||||
ok = (gSpeechStage != 0) && speechLoad();
|
||||
gSfx.voice = ST_VOICE_SFX;
|
||||
gNoiseTicks = 0u;
|
||||
gSfx.ticksLeft = 0u;
|
||||
gTone.voice = ST_VOICE_TONE;
|
||||
gTone.ticksLeft = 0u;
|
||||
|
|
@ -385,6 +445,8 @@ void stAudioInit(void) {
|
|||
gSpeechActive = false;
|
||||
gSpeechPitch = ST_SPEECH_PITCH_BASE;
|
||||
gSpeechPol = 0u;
|
||||
gSpeechInvert = false;
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -401,16 +463,38 @@ void stAudioShutdown(void) {
|
|||
|
||||
|
||||
// $6D6A / $6A3B -- voice 3 jet noise gate.
|
||||
// $6D6A's voice-3 gate. The C64 holds SID voice 3 open for as long as a
|
||||
// direction is held, so the jet has to keep sounding for the whole burn.
|
||||
//
|
||||
// This re-asserts on EVERY call rather than only on the rising edge,
|
||||
// because jlAudioNoise is a per-frame assertion and not a latch: the
|
||||
// IIgs plays one ~80 ms one-shot per call and expects the caller to keep
|
||||
// asking, exactly as jlMusicPlay's sequencer does for a held noise note
|
||||
// (include/joey/audio.h). Latching it on the edge is why the thruster
|
||||
// used to sound one short burst and then die with the taxi still
|
||||
// climbing. The ports that DO latch take the repeat harmlessly: the ST
|
||||
// rewrites the same YM registers, and Paula's redundant DMA set is
|
||||
// click-free by design.
|
||||
void stAudioNoise(bool on) {
|
||||
if (on == gNoiseOn) {
|
||||
if (on && gNoiseTicks != 0u) {
|
||||
// A sound effect owns the voice until its release runs out, so
|
||||
// the jet does not talk over the crash. The C64 shares SID
|
||||
// voice 3 between them the same way and silences the jet before
|
||||
// firing one. An explicit silence below still wins, which
|
||||
// matters because stAudioMusic clears this voice before a tune.
|
||||
return;
|
||||
}
|
||||
gNoiseOn = on;
|
||||
if (on) {
|
||||
jlAudioNoise(31u, ST_ATTEN_SOFT);
|
||||
} else {
|
||||
jlAudioNoise(0u, ST_ATTEN_OFF);
|
||||
gNoiseOn = true;
|
||||
jlAudioNoise(ST_NOISE_JET_PITCH, ST_ATTEN_SOFT);
|
||||
return;
|
||||
}
|
||||
if (!gNoiseOn && gNoiseTicks == 0u) {
|
||||
return;
|
||||
}
|
||||
gNoiseOn = false;
|
||||
gNoiseTicks = 0u;
|
||||
jlAudioNoise(0u, ST_ATTEN_OFF);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -423,9 +507,17 @@ void stAudioSfx(const uint8_t *program9) {
|
|||
StSfxSlotT *slot = (program9[8] == 1u) ? &gTone : &gSfx;
|
||||
|
||||
if ((ctrl & 0x80u) != 0u) {
|
||||
// Noise waveform: a burst on the noise generator.
|
||||
jlAudioNoise(31u, ST_ATTEN_LOUD);
|
||||
// Noise waveform: a burst on the noise generator, held for the
|
||||
// program's release time exactly as the tone slots below hold
|
||||
// theirs. It used to fire one jlAudioNoise and return, which is
|
||||
// not a burst at all: that call is a per-frame ASSERTION, not a
|
||||
// latch (include/joey/audio.h), so on the IIgs the wreck's
|
||||
// explosion lasted the ~80 ms of one one-shot and was easy to
|
||||
// miss entirely, while on Paula it would have hung until some
|
||||
// later stAudioNoise(false). stAudioTick re-asserts it.
|
||||
gNoiseTicks = (uint16_t)(ticks + 1u);
|
||||
gNoiseOn = true;
|
||||
jlAudioNoise(ST_NOISE_SFX_PITCH, ST_ATTEN_LOUD);
|
||||
return;
|
||||
}
|
||||
jlAudioVoice(slot->voice, sidHz(word), ST_ATTEN_LOUD);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,31 @@ const StCelDefT kStCels[] = {
|
|||
const uint16_t kStCelCount = (uint16_t)(sizeof(kStCels) / sizeof(kStCels[0]));
|
||||
|
||||
|
||||
// Cels the title screen draws that are deliberately NOT pre-compiled.
|
||||
//
|
||||
// Its five decoration sprites are recoloured by logoColorCycle ($4861),
|
||||
// which rewrites sprites 3..7 with the next logo colour every cycle
|
||||
// step, so each is seen in all eight of kLogoColor's values -- forty
|
||||
// combinations. The parked cab's jet is a forty-first, colour 6 rather
|
||||
// than the flame list's 7. None of them are worth a bank slot: on the
|
||||
// 65816 a compile costs about half a second, so baking the forty would
|
||||
// add some twenty seconds to startup, and raising kStCelCount at all is
|
||||
// risky -- 37 made the runtime reject the whole precompiled bank (see
|
||||
// the note in the project memory). They draw interpreted, which
|
||||
// a static title screen can well afford. stRender consults this list so
|
||||
// the mid-ride warning stays about real misses.
|
||||
// A zero colour means "in any colour": the decorations cycle through all
|
||||
// eight. The jet is listed for the cab colour alone, because the flame
|
||||
// list above already carries it in colour 7 for the ride itself.
|
||||
const StCelDefT kStInterpretedCels[] = {
|
||||
{ 0xDB, 0u, 1u, MC0, MC1 }, { 0xDE, 0u, 1u, MC0, MC1 },
|
||||
{ 0xDF, 0u, 1u, MC0, MC1 }, { 0xE0, 0u, 1u, MC0, MC1 },
|
||||
{ 0xE1, 0u, 1u, MC0, MC1 },
|
||||
{ 0xD8, CAB, 1u, MC0, MC1 },
|
||||
};
|
||||
const uint16_t kStInterpretedCount = (uint16_t)(sizeof(kStInterpretedCels) / sizeof(kStInterpretedCels[0]));
|
||||
|
||||
|
||||
void stCelBlob(const uint8_t *bm, uint8_t multi, uint8_t color, uint8_t mc0, uint8_t mc1, uint8_t *out) {
|
||||
uint8_t pal[4];
|
||||
uint8_t tx;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ typedef struct {
|
|||
|
||||
extern const StCelDefT kStCels[];
|
||||
extern const uint16_t kStCelCount;
|
||||
// Cels the title screen draws interpreted by design; a zero colour
|
||||
// entry matches any colour. See the note beside the table in stCels.c.
|
||||
extern const StCelDefT kStInterpretedCels[];
|
||||
extern const uint16_t kStInterpretedCount;
|
||||
|
||||
// The cels a level's hook program can show (stLevelCels.c): at most `max`
|
||||
// into `out`, no duplicates. 0 for a level without hook sprites.
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ static StSpriteCacheT *cacheFreeSlot(void);
|
|||
static void levelCelsEvict(void);
|
||||
static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr);
|
||||
static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total);
|
||||
static bool celDrawsInterpreted(uint8_t ptr, uint8_t color);
|
||||
static uint8_t cellCol(uint16_t cell, uint8_t row);
|
||||
static uint8_t cellRow(uint16_t cell);
|
||||
static void collectGlyphs(StSimT *sim);
|
||||
|
|
@ -166,10 +167,11 @@ static bool loadPrecompiledCels(void);
|
|||
static bool logoBuildTile(const StSimT *sim);
|
||||
static uint8_t logoFrameIndex(const StSimT *sim);
|
||||
static void logoPaletteApply(const StSimT *sim, uint8_t frame);
|
||||
static void starPaletteApply(void);
|
||||
static void paintCells(jlSurfaceT *stage, StSimT *sim);
|
||||
static void pasteCell(jlSurfaceT *stage, const StSimT *sim, uint16_t cell, uint8_t bx, uint8_t by);
|
||||
static bool prewarmReport(uint16_t expect);
|
||||
static bool spriteOnScreen(int16_t px, int16_t py);
|
||||
static void starPaletteApply(void);
|
||||
static void tileFromChunky(jlTileT *out, const uint8_t *chunky);
|
||||
|
||||
|
||||
|
|
@ -278,7 +280,7 @@ static jlSpriteT *cachedSprite(StSimT *sim, uint8_t idx) {
|
|||
if (idx <= 2u && gPrewarming) {
|
||||
cacheCompile(slot);
|
||||
}
|
||||
if (!gPrewarming) {
|
||||
if (!gPrewarming && !celDrawsInterpreted(ptr, color)) {
|
||||
ST_LOG("spacetaxi: ! cel $%02X colour %u (level %u) built mid-ride -- add it to stLevelCelList", ptr, color, level);
|
||||
}
|
||||
slot->lastUse = gRender.cacheStamp;
|
||||
|
|
@ -313,6 +315,26 @@ static const uint8_t kCellRow[125] = {
|
|||
// Column of a cell whose row is known: cell - row * 40 through a table,
|
||||
// because the two-shift form of the multiply came back as a __mulhi3
|
||||
// library call on the 65816.
|
||||
// True for a cel that is MEANT to be built here rather than pre-listed,
|
||||
// so the warning below stays about real misses. Only the title screen's
|
||||
// decorations qualify: logoColorCycle recolours them every cycle step,
|
||||
// which is forty ptr/colour pairs that would each cost a compile (see
|
||||
// the note beside kStTitleDecorCels in stCels.c).
|
||||
static bool celDrawsInterpreted(uint8_t ptr, uint8_t color) {
|
||||
uint16_t k;
|
||||
|
||||
for (k = 0u; k < kStInterpretedCount; k++) {
|
||||
if (kStInterpretedCels[k].ptr != ptr) {
|
||||
continue;
|
||||
}
|
||||
if (kStInterpretedCels[k].color == 0u || kStInterpretedCels[k].color == color) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static uint8_t cellCol(uint16_t cell, uint8_t row) {
|
||||
static const uint16_t kRowBase[ST_SCREEN_ROWS] = {
|
||||
0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480,
|
||||
|
|
@ -874,21 +896,22 @@ static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total) {
|
|||
// here behind the LOADING bar. Either way nothing compiles once the
|
||||
// level is running. Keyed exactly as cachedSprite keys them, so every
|
||||
// lookup during the ride is a hit.
|
||||
void stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
|
||||
bool stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
|
||||
static StCelDefT list[ST_LEVEL_CELS_MAX];
|
||||
jlSpriteT *cels[ST_LEVEL_CELS_MAX];
|
||||
char path[24];
|
||||
uint8_t n;
|
||||
uint8_t got;
|
||||
uint8_t ready = 0u;
|
||||
uint8_t k;
|
||||
|
||||
levelCelsEvict();
|
||||
if (level == 0) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
n = stLevelCelList(level, list, ST_LEVEL_CELS_MAX);
|
||||
if (n == 0u) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
memcpy(path, "sprites/level", 13u);
|
||||
path[13] = (char)('0' + (level->levelIndex + 1u) / 10u);
|
||||
|
|
@ -938,11 +961,17 @@ void stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
|
|||
slot->used = true;
|
||||
slot->pinned = true;
|
||||
slot->lastUse = gRender.cacheStamp;
|
||||
ready++;
|
||||
}
|
||||
gPrewarming = false;
|
||||
if (got != n) {
|
||||
jlFillRect(stage, 60, 110, 200u, 6u, 0u);
|
||||
}
|
||||
if (ready < n) {
|
||||
ST_LOG("spacetaxi: ! level %u has %u of %u cels", level->levelIndex, ready, n);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -976,11 +1005,54 @@ static bool loadPrecompiledCels(void) {
|
|||
e->used = true;
|
||||
e->pinned = true;
|
||||
e->lastUse = 0u;
|
||||
// The bank's cels arrive compiled ONLY if the arena had room for
|
||||
// them: jlSpriteBankLoadPrecompiled loads a cel either way and
|
||||
// leaves the ones that did not fit to the interpreter. So ask
|
||||
// the sprite instead of assuming, or the flag lies and any
|
||||
// future cacheCompile skips a cel that really does need work.
|
||||
e->compiled = (jlSpriteCompiledSize(e->sprite) != 0u);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// A fatal message, centred, in the game's own charset. Used when an
|
||||
// asset that IS on the disk will not load: the game then stops instead
|
||||
// of playing on in a degraded state, because a mute game or one running
|
||||
// every sprite through the interpreter looks like a slow machine rather
|
||||
// than a broken install.
|
||||
void stRenderFatal(jlSurfaceT *stage, const char *line1, const char *line2) {
|
||||
const uint8_t *charset = stC64Charset();
|
||||
const char *lines[2];
|
||||
uint8_t i;
|
||||
uint8_t k;
|
||||
|
||||
if (stage == 0) {
|
||||
return;
|
||||
}
|
||||
lines[0] = line1;
|
||||
lines[1] = line2;
|
||||
jlSurfaceClear(stage, 0u);
|
||||
for (i = 0u; i < 2u; i++) {
|
||||
uint8_t len;
|
||||
uint8_t col;
|
||||
|
||||
if (lines[i] == 0) {
|
||||
continue;
|
||||
}
|
||||
len = 0u;
|
||||
while (lines[i][len] != '\0' && len < ST_SCREEN_COLS) {
|
||||
len++;
|
||||
}
|
||||
col = (uint8_t)((ST_SCREEN_COLS - len) / 2u);
|
||||
for (k = 0u; k < len; k++) {
|
||||
jlTilePasteGlyph(stage, (uint8_t)(col + k), (uint8_t)(ST_LOADING_ROW + i * 2u),
|
||||
&charset[(uint8_t)lines[i][k] * 8u], ST_LOADING_COLOR, 0u);
|
||||
}
|
||||
}
|
||||
jlStagePresent();
|
||||
}
|
||||
|
||||
|
||||
// "LOADING..." centred on an otherwise black stage. The charset is the
|
||||
// game's own, indexed by ASCII (the C64 font's letters sit at their
|
||||
|
|
@ -999,12 +1071,13 @@ void stRenderLoading(jlSurfaceT *stage) {
|
|||
jlStagePresent();
|
||||
}
|
||||
|
||||
void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
|
||||
bool stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
|
||||
static const uint8_t kCabPtrs[] = { 0xC0, 0xC1, 0xDC, 0xDD, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8 };
|
||||
static const uint8_t kPassPtrs[] = { 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xD9 };
|
||||
static const uint8_t kFlamePtrs[] = { 0xD8, 0xD4, 0xD5, 0xD2, 0xD1, 0xD7, 0xD3, 0xD6 };
|
||||
uint8_t total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs) + 1u);
|
||||
uint8_t done = 0u;
|
||||
bool ok;
|
||||
uint8_t k;
|
||||
StFrameT saved;
|
||||
|
||||
|
|
@ -1012,7 +1085,7 @@ void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
|
|||
// runtime build below on any port without a matching .spc.
|
||||
if (loadPrecompiledCels()) {
|
||||
ST_LOG("spacetaxi: cels from sprites/staxicels.spc");
|
||||
return;
|
||||
return prewarmReport(kStCelCount);
|
||||
}
|
||||
ST_LOG("spacetaxi: ! no usable sprites/staxicels.spc -- JIT prewarm");
|
||||
saved = sim->frame;
|
||||
|
|
@ -1050,7 +1123,45 @@ void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
|
|||
(void)cachedSprite(sim, 2u);
|
||||
sim->frame = saved;
|
||||
gPrewarming = false;
|
||||
ok = prewarmReport(total);
|
||||
jlFillRect(stage, 60, 110, 200u, 6u, 0u);
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
||||
// Say how the prewarmed cels came out, and judge them. A cel past the end
|
||||
// of the codegen arena still DRAWS, interpreted, so a port whose arena
|
||||
// holds only some of the cels is slower but sound -- that is the ST at
|
||||
// 2 MB, 12 of 37 compiled, and it is not an error. Zero compiled is:
|
||||
// it means the arena itself never came, which on a 1 MB ST is the speech
|
||||
// blob having taken the heap, and every sprite then runs about forty
|
||||
// times slower. The caller stops on a false rather than let that pass
|
||||
// for a slow machine.
|
||||
static bool prewarmReport(uint16_t expect) {
|
||||
uint32_t bytes = 0u;
|
||||
uint8_t used = 0u;
|
||||
uint8_t hot = 0u;
|
||||
uint8_t k;
|
||||
|
||||
for (k = 0u; k < ST_SPRITE_CACHE; k++) {
|
||||
uint32_t n;
|
||||
|
||||
if (!gRender.cache[k].used) {
|
||||
continue;
|
||||
}
|
||||
n = jlSpriteCompiledSize(gRender.cache[k].sprite);
|
||||
used++;
|
||||
if (n != 0u) {
|
||||
hot++;
|
||||
bytes += n;
|
||||
}
|
||||
}
|
||||
ST_LOG("spacetaxi: prewarm %u cels: %u compiled, %u interpreted, %lu arena bytes", used, hot, (uint8_t)(used - hot), (unsigned long)bytes);
|
||||
if (used < expect) {
|
||||
ST_LOG("spacetaxi: ! only %u of %u cels built", used, expect);
|
||||
return false;
|
||||
}
|
||||
return hot != 0u;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ STAXI_SPR_GEN := $(patsubst $(STAXI_SRC_DIR)/%.png,$(STAXI_GEN_DIR)/%.spr,$(
|
|||
STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN))
|
||||
STAXI_TBK_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_TBK_GEN))
|
||||
STAXI_SPR_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPR_GEN))
|
||||
STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) $(STAXICELS_SPC_RUN)
|
||||
STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN)
|
||||
# Pre-compiled sprite banks (.spc): baked from the .spr by the host spritebake
|
||||
# tool for THIS target and staged next to the .spr, so the runtime loads
|
||||
# compiled routines with no startup JIT (jlSpriteBankLoadPrecompiled).
|
||||
|
|
@ -151,6 +151,14 @@ STAXI_ASSET_DSTS += $(STAXI_SPEECH_RUN)
|
|||
STAXICELS_SPR_GEN := $(STAXI_GEN_DIR)/sprites/staxicels.spr
|
||||
STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc)
|
||||
STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN))
|
||||
# Added HERE, not up with the other asset destinations: this variable is
|
||||
# defined below that point, and the := there expanded it to nothing, so
|
||||
# the compiled cel bank was never a prerequisite of the port and never
|
||||
# got copied into the build tree. Every port shipped whatever .spc was
|
||||
# staged the day the file was first created, and edits to stCels.c did
|
||||
# not reach the game (found 2026-09-23 when a new cel made the runtime
|
||||
# reject the bank as too short).
|
||||
STAXI_ASSET_DSTS += $(STAXICELS_SPC_RUN)
|
||||
STAXI_SPC_GEN := $(STAXI_SPR_GEN:.spr=.spc)
|
||||
STAXI_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPC_GEN))
|
||||
ASSETBAKE_BIN := $(REPO_DIR)/tools/assetbake/assetbake.py
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ STAXI_SPR_GEN := $(patsubst $(STAXI_SRC_DIR)/%.png,$(STAXI_GEN_DIR)/%.spr,$(
|
|||
STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN))
|
||||
STAXI_TBK_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_TBK_GEN))
|
||||
STAXI_SPR_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPR_GEN))
|
||||
STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) $(STAXICELS_SPC_RUN)
|
||||
STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN)
|
||||
# Pre-compiled sprite banks (.spc): baked from the .spr by the host spritebake
|
||||
# tool for THIS target and staged next to the .spr, so the runtime loads
|
||||
# compiled routines with no startup JIT (jlSpriteBankLoadPrecompiled). ST is
|
||||
|
|
@ -131,6 +131,14 @@ STAXI_ASSET_DSTS += $(STAXI_SPEECH_RUN)
|
|||
STAXICELS_SPR_GEN := $(STAXI_GEN_DIR)/sprites/staxicels.spr
|
||||
STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc)
|
||||
STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN))
|
||||
# Added HERE, not up with the other asset destinations: this variable is
|
||||
# defined below that point, and the := there expanded it to nothing, so
|
||||
# the compiled cel bank was never a prerequisite of the port and never
|
||||
# got copied into the build tree. Every port shipped whatever .spc was
|
||||
# staged the day the file was first created, and edits to stCels.c did
|
||||
# not reach the game (found 2026-09-23 when a new cel made the runtime
|
||||
# reject the bank as too short).
|
||||
STAXI_ASSET_DSTS += $(STAXICELS_SPC_RUN)
|
||||
STAXI_SPC_GEN := $(STAXI_SPR_GEN:.spr=.spc)
|
||||
STAXI_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPC_GEN))
|
||||
ASSETBAKE_BIN := $(REPO_DIR)/tools/assetbake/assetbake.py
|
||||
|
|
@ -331,7 +339,13 @@ $(STAXI_GEN_DIR)/sprites/level%.spr: $(STAXI_GEN_DIR)/levels/level%.dat $(STAXIB
|
|||
# Speech bake rules live down here so they never become the default goal.
|
||||
$(STAXI_SPEECH_GEN): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py
|
||||
@mkdir -p $(dir $@)
|
||||
python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@
|
||||
# --single: one polarity, 60 KB instead of 120 KB. Measured on a
|
||||
# stock 1 MB machine, the two-polarity file cannot be allocated at
|
||||
# all -- "speech alloc FAILED (120828 bytes)", so that ST plays the
|
||||
# whole game mute -- while the half-size one loads there. 2 MB and up
|
||||
# take either. The samples are identical: the player negates them
|
||||
# when the running polarity is odd, one subtraction per byte.
|
||||
python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ --single
|
||||
$(STAXI_SPEECH_RUN): $(STAXI_SPEECH_GEN)
|
||||
@mkdir -p $(dir $@)
|
||||
cp $< $@
|
||||
|
|
|
|||
10
make/dos.mk
10
make/dos.mk
|
|
@ -122,7 +122,7 @@ ASSETBAKE_TARGET := dos
|
|||
STAXI_LEVEL_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_LEVEL_GEN))
|
||||
STAXI_TBK_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_TBK_GEN))
|
||||
STAXI_SPR_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPR_GEN))
|
||||
STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) $(STAXICELS_SPC_RUN)
|
||||
STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN)
|
||||
# Pre-compiled sprite banks (.spc): baked from the .spr by the host spritebake
|
||||
# tool for THIS target and staged next to the .spr, so the runtime loads
|
||||
# compiled routines with no startup JIT (jlSpriteBankLoadPrecompiled).
|
||||
|
|
@ -152,6 +152,14 @@ STAXI_ASSET_DSTS += $(STAXI_SPEECH_RUN)
|
|||
STAXICELS_SPR_GEN := $(STAXI_GEN_DIR)/sprites/staxicels.spr
|
||||
STAXICELS_SPC_GEN := $(STAXICELS_SPR_GEN:.spr=.spc)
|
||||
STAXICELS_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXICELS_SPC_GEN))
|
||||
# Added HERE, not up with the other asset destinations: this variable is
|
||||
# defined below that point, and the := there expanded it to nothing, so
|
||||
# the compiled cel bank was never a prerequisite of the port and never
|
||||
# got copied into the build tree. Every port shipped whatever .spc was
|
||||
# staged the day the file was first created, and edits to stCels.c did
|
||||
# not reach the game (found 2026-09-23 when a new cel made the runtime
|
||||
# reject the bank as too short).
|
||||
STAXI_ASSET_DSTS += $(STAXICELS_SPC_RUN)
|
||||
STAXI_SPC_GEN := $(STAXI_SPR_GEN:.spr=.spc)
|
||||
STAXI_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXI_SPC_GEN))
|
||||
|
||||
|
|
|
|||
45
make/iigs.mk
45
make/iigs.mk
|
|
@ -79,7 +79,7 @@ NTP_BIN := $(BUILD)/audio/ntpplayer.bin
|
|||
NTP_ASM := $(BUILD)/audio/ntpdata.s
|
||||
IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32
|
||||
|
||||
.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-disk-all iigs-verify iigs-verify-all iigs-verify-save iigs-verify-serial iigs-verify-shrtail clean-iigs clean
|
||||
.PHONY: all iigs iigs-lib iigs-clang-smoke iigs-examples iigs-disk iigs-disk-all iigs-game-disk iigs-game-disks iigs-game-disk-agi iigs-verify-game-disk iigs-verify iigs-verify-all iigs-verify-save iigs-verify-serial iigs-verify-shrtail clean-iigs clean
|
||||
|
||||
# Default: compile-check the library + run the end-to-end smoke test.
|
||||
all iigs: iigs-lib iigs-clang-smoke
|
||||
|
|
@ -147,6 +147,49 @@ $(EXAMPLES)/spacetaxi/generated/iigs/sprites/%.spc: $(EXAMPLES)/spacetaxi/genera
|
|||
iigs-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(STAXILEVEL_IIGS_SPC) $(STAXI_IIGS_SPEECH)
|
||||
BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey.2mg
|
||||
|
||||
# ONE self-booting volume per game: a GS/OS system stripped to what JoeyLib
|
||||
# actually uses, a launcher in place of the Finder, and the program in its own
|
||||
# folder so that folder can be copied to a hard drive and run from there.
|
||||
# Boot to the program is 21.8 s against 43.4 s for the two-floppy boot disk.
|
||||
# The size is chosen per example: a program that fits beside a stripped
|
||||
# system gets a bootable 800KB 3.5" floppy, and one that does not (STAXI,
|
||||
# AGI) gets a 2048KB volume, which MAME can only boot off a CFFA2 card
|
||||
# because its 3.5 drive is 800KB GCR:
|
||||
# mame apple2gs -sl7 cffa2 -hard1 build/iigs/bin/STAXI.2mg
|
||||
# Pass GAME=name for a different example, GAME_DISK_SIZE to force a size.
|
||||
# It builds its OWN one-example data image rather than depending on
|
||||
# iigs-disk: the default joey.2mg carries the small examples and NOT STAXI
|
||||
# (see make-iigs-disk.sh's JOEY_DISK_EXAMPLES), and the gates depend on
|
||||
# joey.2mg's exact contents, so this must not rewrite it.
|
||||
GAME ?= STAXI
|
||||
iigs-game-disk: iigs-examples $(NTP_BIN) $(STAXI_IIGS_SPC) $(STAXILEVEL_IIGS_SPC) $(STAXI_IIGS_SPEECH)
|
||||
BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" JOEY_DISK_EXAMPLES="$(GAME)" \
|
||||
$(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/$(GAME)-data.2mg
|
||||
JOEY_GAME_DISK_SIZE="$(or $(GAME_DISK_SIZE),auto)" \
|
||||
$(REPO_DIR)/scripts/make-iigs-game-disk.sh $(BINDIR)/$(GAME).2mg $(GAME) $(GAME) $(BINDIR)/$(GAME)-data.2mg
|
||||
|
||||
# Every example that ships as its own bootable volume. AGI is excluded
|
||||
# because its resources come from a different builder (see below).
|
||||
IIGS_GAME_DISKS := DRAW PATTERN KEYS JOY SPRITE SAVE AUDIO ADV ADV2 UBER SERIAL SERTEST STAXI
|
||||
|
||||
iigs-game-disks:
|
||||
@for g in $(IIGS_GAME_DISKS); do \
|
||||
$(MAKE) --no-print-directory -f make/iigs.mk iigs-game-disk GAME=$$g || exit 1; \
|
||||
done
|
||||
|
||||
# AGI needs King's Quest III's resources, which make-agi-iigs-disk.sh lays
|
||||
# out, so it cannot use the generic one-example data image. It also wants a
|
||||
# bigger Memory Manager pool: run it with -ramsize 8M.
|
||||
AGI_GAME ?= kq3
|
||||
iigs-game-disk-agi: $(BINDIR)/AGI
|
||||
$(REPO_DIR)/scripts/make-agi-iigs-disk.sh $(BINDIR)/AGI-data.2mg $(AGI_GAME)
|
||||
$(REPO_DIR)/scripts/make-iigs-game-disk.sh $(BINDIR)/AGI.2mg AGI AGI $(BINDIR)/AGI-data.2mg
|
||||
|
||||
# Boot one game disk headlessly and assert it reaches the program and draws.
|
||||
# An 800KB image goes in the 3.5 drive, a larger one on a CFFA2 card.
|
||||
iigs-verify-game-disk: iigs-game-disk
|
||||
$(REPO_DIR)/scripts/verify-iigs-game-disk.sh $(BINDIR)/$(GAME).2mg
|
||||
|
||||
# Every launchable example on one image. The full set (~1.7 MB of binaries
|
||||
# plus STAXI's data) will not fit an 800KB 3.5" floppy, so this is a 16 MB
|
||||
# ProDOS hard-disk image (joey-all.2mg): boot it in MAME with
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ LIB_OBJS := \
|
|||
$(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/planarFill.o \
|
||||
$(BUILD)/obj/68k/planarPlot.o \
|
||||
$(BUILD)/obj/68k/spriteEmitPlanar68k.o \
|
||||
$(BUILD)/obj/codegen/spriteCompile.o \
|
||||
$(BUILD)/obj/codegen/spriteStage.o
|
||||
|
|
@ -189,7 +191,17 @@ STAXI_ASSET_DSTS := $(STAXI_LEVEL_RUN) $(STAXICELS_SPC_RUN)
|
|||
# for this target, so no cel is JIT-compiled mid-ride. Levels without
|
||||
# hook sprites have no bank; the game JIT-compiles at level entry when a
|
||||
# bank is missing.
|
||||
STAXI_CEL_LEVELS := 07 08 09 10 12 16 17 21
|
||||
# ...except on the X68000, where they do not fit. The Human68k floppy is
|
||||
# 1232 KB and these eight banks are 787 KB of it (level09 alone is 236 KB,
|
||||
# level10 175, level17 165), which with the 296 KB binary, the levels and
|
||||
# the speech put the disk about 300 KB over however small speech gets.
|
||||
# stRenderLevelCels already handles their absence the right way: it builds
|
||||
# and compiles the level's cels at ENTRY, behind the LOADING bar, so the
|
||||
# ride itself still never compiles. That is also what spacetaxi.c's
|
||||
# codegen-arena comment says the 68k ports are meant to do -- their cels
|
||||
# are big and their JIT is fast. Set STAXI_CEL_LEVELS to the list above to
|
||||
# ship them again if the payload ever moves to a hard disk image.
|
||||
STAXI_CEL_LEVELS :=
|
||||
STAXILEVEL_SPR_GEN := $(foreach n,$(STAXI_CEL_LEVELS),$(STAXI_GEN_DIR)/sprites/level$(n).spr)
|
||||
STAXILEVEL_SPC_GEN := $(STAXILEVEL_SPR_GEN:.spr=.spc)
|
||||
STAXILEVEL_SPC_RUN := $(patsubst $(STAXI_GEN_DIR)/%,$(STAXI_RUN_DIR)/%,$(STAXILEVEL_SPC_GEN))
|
||||
|
|
@ -203,7 +215,11 @@ STAXI_SPEECH_RUN := $(STAXI_RUN_DIR)/speech.bin
|
|||
STAXI_ASSET_DSTS += $(STAXI_SPEECH_RUN)
|
||||
$(STAXI_SPEECH_GEN): $(EXAMPLES)/spacetaxi/stSpeechData.c $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py
|
||||
@mkdir -p $(dir $@)
|
||||
python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@
|
||||
# --single: one polarity, 60 KB instead of 120 KB. The 1232 KB
|
||||
# Human68k floppy cannot hold the two-polarity file alongside the
|
||||
# binary and the levels -- it came up 18 KB short -- and the player
|
||||
# negates the samples itself when the running polarity is odd.
|
||||
python3 $(EXAMPLES)/spacetaxi/assets/genSpeechPcm.py $< $@ --single
|
||||
$(STAXI_SPEECH_RUN): $(STAXI_SPEECH_GEN)
|
||||
@mkdir -p $(dir $@)
|
||||
cp $< $@
|
||||
|
|
|
|||
172
scripts/bench-x68000.sh
Executable file
172
scripts/bench-x68000.sh
Executable file
|
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env bash
|
||||
# bench-x68000.sh - X68000 performance run, the twin of bench-atarist.sh.
|
||||
#
|
||||
# The sibling of verify-x68000-golden.sh: same staging and same
|
||||
# completion detection, but UBER keeps its REAL measurement window, so the
|
||||
# joeylog it leaves behind carries honest per-operation rates instead of
|
||||
# hashes. Those rates are only trustworthy because this port now has an
|
||||
# interrupt-driven millisecond clock (src/x68000/hal.c chains Human68k's MFP
|
||||
# Timer C); before that every figure converged on ~10,000 ops/sec because the
|
||||
# frame-derived clock under-counted exactly as much as the work cost.
|
||||
#
|
||||
# Prints the UBER rows and leaves the full log in build/x68000/joeylog.txt.
|
||||
#
|
||||
# Takes a few minutes. The run EXITS ON COMPLETION, not on a frame budget: UBER
|
||||
# ends by clearing the whole stage to colour index 2 and presenting it before it
|
||||
# blocks in jlWaitForAnyKey, so the Lua below watches GVRAM for that screen.
|
||||
# (It used to burn a flat 300,000 frames -- ~90 minutes of emulated time -- a
|
||||
# budget sized back when this port still drew through the slow generic renderer.
|
||||
# The actual work is a Human68k boot plus well under 10,000 frames.)
|
||||
# X68K_BENCH_FRAMES is only a backstop: reaching it means the done-screen was
|
||||
# never detected, which is a failed run, not a normal exit. The budget is
|
||||
# larger than the gate's because a real measurement window runs each operation
|
||||
# for hundreds of frames rather than one.
|
||||
#
|
||||
# X68K_SCRATCH=<dir with x68mame/> bash scripts/bench-x68000.sh
|
||||
set -uo pipefail
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
SP="${X68K_SCRATCH:?set X68K_SCRATCH to a work dir containing x68mame/}"
|
||||
MAME="$repo/toolchains/cache/mame-mame0264/x68k"
|
||||
TEMPLATE="$SP/x68mame/HUMAN302.XDF"
|
||||
FRAMES="${X68K_BENCH_FRAMES:-120000}"
|
||||
WALL="${X68K_BENCH_WALL:-7200}"
|
||||
|
||||
export PATH="$repo/toolchains/x68000/m68k-xelf/bin:$PATH"
|
||||
work=$(mktemp -d -t joey-x68bench.XXXXXX)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
# -s (strip) keeps the staged binary small. (It used to be load-bearing: the
|
||||
# data disk was built by deleting the template's root FILES, which leaves the
|
||||
# stock ~900 KB of Human68k utilities in SYS/ BIN/ BASIC2/ ... still allocated
|
||||
# and only ~330 KB usable. `xdftool empty` frees the whole 1221-cluster disk,
|
||||
# so the size pressure is gone -- the strip is now just tidiness.)
|
||||
m68k-xelf-gcc -s -O2 -m68000 -fomit-frame-pointer \
|
||||
-DJOEYLIB_PLATFORM_X68000 \
|
||||
-I"$repo/include" -I"$repo/src/core" -I"$repo/src/x68000" \
|
||||
-I"$repo/toolchains/audio/libxmp-lite/include" \
|
||||
"$repo/examples/uber/uber.c" \
|
||||
"$repo/build/x68000/lib/libjoey.a" "$repo/build/x68000/lib/libxmplite.a" -lm \
|
||||
-o "$work/UBER.X" || exit 1
|
||||
|
||||
# TWO DISKS: the boot disk carries only Human68k and AUTOEXEC, while the binary
|
||||
# AND joeylog.txt live on the second disk on B:. AUTOEXEC switches to B: before
|
||||
# launching so the log lands there too. Keeping the writable log off the boot
|
||||
# disk also means a run can never corrupt the thing that boots it.
|
||||
printf 'B:\r\nB:\\UBER.X\r\n\x1a' > "$work/AUTOEXEC.BAT"
|
||||
cp "$TEMPLATE" "$work/boot.xdf"
|
||||
# Data disk = the template emptied (keeps the BPB and boot sector). `empty`
|
||||
# rather than a list of filenames: the stock disk's utilities live in
|
||||
# SUBDIRECTORIES, which `delete` cannot see or free.
|
||||
cp "$TEMPLATE" "$work/data.xdf"
|
||||
python3 "$repo/tools/xdftool.py" empty "$work/data.xdf" >/dev/null
|
||||
# Trim the boot disk of anything not needed to boot and run one .X.
|
||||
for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do
|
||||
python3 "$repo/tools/xdftool.py" delete "$work/boot.xdf" "$f" >/dev/null 2>&1
|
||||
done
|
||||
for d in SYS HIS BIN BASIC2 ASK ETC; do
|
||||
python3 "$repo/tools/xdftool.py" deltree "$work/boot.xdf" "$d" >/dev/null 2>&1
|
||||
done
|
||||
python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$work/UBER.X" UBER.X >/dev/null || exit 1
|
||||
python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null
|
||||
|
||||
# Completion-driven exit. UBER's last act before jlWaitForAnyKey is
|
||||
# jlSurfaceClear(gStage, 2) + jlStagePresent, so an all-colour-2 stage in GVRAM
|
||||
# is the "hashes are computed and flushed" signal. The benchmark's own
|
||||
# jlSurfaceClear/jlStagePresent ops can paint that colour for an instant, so the
|
||||
# reading only counts once it has held for GREEN_STABLE frames -- the finished
|
||||
# screen never changes again, a mid-benchmark frame never lasts.
|
||||
#
|
||||
# jlLogFlush only fflush()es; the directory entry is finalised by the atexit
|
||||
# fclose. So the keypress that releases jlWaitForAnyKey still has to be posted,
|
||||
# and MAME must not be killed until UBER has returned to Human68k -- hence the
|
||||
# repeated posts and the EXIT_SETTLE wait before exiting.
|
||||
cat > "$work/bench.lua" <<LUA
|
||||
local mem = manager.machine.devices[":maincpu"].spaces["program"]
|
||||
local frame = 0
|
||||
local greenRun = 0
|
||||
local exitAt = 0
|
||||
local done = false
|
||||
|
||||
local FRAMES = $FRAMES
|
||||
local GREEN_STABLE = 600 -- ~11 s of emulated time held steady
|
||||
local EXIT_SETTLE = 900 -- keypress -> fclose -> back at the Human68k prompt
|
||||
|
||||
-- crtmod 13, 256-colour graphics plane: one 16-bit word per pixel at \$C00000,
|
||||
-- 512 words per row, the 320x200 stage centred at origin (96, 156).
|
||||
local function stagePixel(px, py)
|
||||
return mem:read_u8(0xC00000 + ((156 + py) * 512 + 96 + px) * 2 + 1)
|
||||
end
|
||||
|
||||
local kProbes = { {4,4}, {160,4}, {315,4}, {4,100}, {160,100}, {315,100},
|
||||
{4,196}, {160,196}, {315,196}, {80,50}, {240,150}, {200,80} }
|
||||
|
||||
local function screenIsDone()
|
||||
for _, p in ipairs(kProbes) do
|
||||
if stagePixel(p[1], p[2]) ~= 2 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
emu.register_frame_done(function()
|
||||
frame = frame + 1
|
||||
if done then return end
|
||||
if frame % 2000 == 0 then io.write("BENCH f"..frame.."\n"); io.flush() end
|
||||
|
||||
if exitAt == 0 then
|
||||
if screenIsDone() then
|
||||
greenRun = greenRun + 1
|
||||
if greenRun >= GREEN_STABLE then
|
||||
io.write("BENCH done-screen f"..frame.."\n"); io.flush()
|
||||
exitAt = frame + EXIT_SETTLE
|
||||
end
|
||||
else
|
||||
greenRun = 0
|
||||
end
|
||||
else
|
||||
-- Release jlWaitForAnyKey; extra presses land harmlessly at the prompt.
|
||||
if frame % 60 == 0 then manager.machine.natkeyboard:post(" ") end
|
||||
if frame >= exitAt then
|
||||
done = true
|
||||
io.write("BENCH exit f"..frame.."\n"); io.flush()
|
||||
manager.machine:exit()
|
||||
end
|
||||
end
|
||||
|
||||
-- Backstop only: reaching this means the done-screen never appeared.
|
||||
if frame > FRAMES then
|
||||
done = true
|
||||
io.write("BENCH budget-exhausted f"..frame.."\n"); io.flush()
|
||||
manager.machine:exit()
|
||||
end
|
||||
end)
|
||||
LUA
|
||||
|
||||
runLog=$(timeout -s KILL "$WALL" "$MAME" x68000 -bios ipl10 \
|
||||
-rompath "$SP/x68mame/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" \
|
||||
-video none -sound none -nothrottle \
|
||||
-autoboot_script "$work/bench.lua" </dev/null 2>&1 | grep '^BENCH')
|
||||
echo "$runLog"
|
||||
|
||||
# The backstop is not a normal exit: it means UBER never reached its done-screen,
|
||||
# so whatever log is on the disk (if any) is from an unfinished run.
|
||||
if echo "$runLog" | grep -q 'budget-exhausted'; then
|
||||
echo "bench-x68000: FAIL - UBER's done-screen never appeared within $FRAMES frames" >&2
|
||||
echo " (raise X68K_BENCH_FRAMES if UBER legitimately got slower; otherwise the run died early)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$runLog" | grep -q 'done-screen'; then
|
||||
echo "bench-x68000: FAIL - MAME stopped before UBER finished (wall timeout ${WALL}s?)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 "$repo/tools/xdftool.py" extract "$work/data.xdf" joeylog.txt "$work/x68.txt" || {
|
||||
echo "bench-x68000: FAIL - no joeylog.txt (run did not reach jlLogFlush)" >&2
|
||||
exit 1
|
||||
}
|
||||
tr -d '\r' < "$work/x68.txt" > "$work/x68.clean"
|
||||
cp "$work/x68.clean" "$repo/build/x68000/joeylog.txt"
|
||||
grep -E '^UBER:' "$work/x68.clean"
|
||||
echo "bench-x68000: full log -> $repo/build/x68000/joeylog.txt"
|
||||
|
|
@ -41,14 +41,19 @@ xvfb_pid=$!
|
|||
sleep 0.3
|
||||
trap 'kill $xvfb_pid 2>/dev/null || true; kill $dosbox_pid 2>/dev/null || true' EXIT
|
||||
|
||||
# Mount with an explicit -c, not by handing DOSBox the directory as a
|
||||
# positional argument: 0.74-3 ignored it here and dropped the program at
|
||||
# "Drive C does not exist", which looks exactly like a program that
|
||||
# failed to start. The cycles override has to be one argv entry too --
|
||||
# unquoted it split into three words and ate the path.
|
||||
dosboxArgs=(-conf "$conf" -set "mouse_capture=seamless")
|
||||
if [[ -n ${CYCLES:-} ]]; then
|
||||
dosboxArgs+=(-set "cpu cycles=${CYCLES}")
|
||||
fi
|
||||
dosboxArgs+=(-c "mount c \"$mount_dir\"" -c "C:" -c "$file")
|
||||
|
||||
DISPLAY=$display SDL_VIDEO_X11_VISUALID="" \
|
||||
dosbox \
|
||||
-conf "$conf" \
|
||||
-set "mouse_capture=seamless" \
|
||||
${CYCLES:+-set "cpu cycles=${CYCLES}"} \
|
||||
-c "C:" \
|
||||
-c "$file" \
|
||||
"$mount_dir" >/tmp/captureDos-dosbox.log 2>&1 &
|
||||
dosbox "${dosboxArgs[@]}" >/tmp/captureDos-dosbox.log 2>&1 &
|
||||
dosbox_pid=$!
|
||||
|
||||
# Wait for DOSBox to boot the program and settle.
|
||||
|
|
|
|||
208
scripts/make-iigs-game-disk.sh
Executable file
208
scripts/make-iigs-game-disk.sh
Executable file
|
|
@ -0,0 +1,208 @@
|
|||
#!/usr/bin/env bash
|
||||
# make-iigs-game-disk.sh - Build ONE self-booting GS/OS volume that boots
|
||||
# straight into a JoeyLib program, with the program in its own folder so
|
||||
# that folder can be copied to a hard drive and run from there.
|
||||
#
|
||||
# Layout of the result:
|
||||
# /<VOL>/System/... a GS/OS boot system stripped to what JoeyLib
|
||||
# actually uses (see the drop list below)
|
||||
# /<VOL>/System/Start tools/iigsStart, which sets prefix 0 and
|
||||
# QuitGS's to the program below
|
||||
# /<VOL>/<PROG>/ the program, its ntpplayer and its DATA tree
|
||||
#
|
||||
# The program folder is self-contained: JoeyLib opens "ntpplayer" and
|
||||
# "DATA/..." relative to prefix 0, which GS/OS points at the launched
|
||||
# program's directory. So the same folder runs from a hard drive after a
|
||||
# plain Finder copy, and nothing in it names the boot volume.
|
||||
#
|
||||
# WHY STRIP: JoeyLib's whole toolbox usage on the IIgs is MMStartUp,
|
||||
# NewHandle, DisposeHandle, HLock, GetTick, SetHeartBeat, DelHeartBeat,
|
||||
# ReadBParam, SoundStartUp and SoundShutDown, plus GS/OS file calls and
|
||||
# QuitGS. Those live in the Memory Manager, the Misc Tool Set and the
|
||||
# Sound Tool Set, all three in ROM. Not one of the thirteen RAM tool sets
|
||||
# in /System/Tools is referenced, and neither are the desk accessories,
|
||||
# the control panels, ProDOS 8 or the Finder's icon database. On the stock
|
||||
# disk that is over 300 KB read at every boot for nothing, and it is also
|
||||
# the room the program needs in order to share the volume.
|
||||
#
|
||||
# Usage: scripts/make-iigs-game-disk.sh <out> <PROG> [volume] [data.2mg]
|
||||
# out .po for an 800KB floppy, .2mg for anything larger
|
||||
# PROG uppercase binary name, e.g. STAXI
|
||||
# volume boot volume name, default = PROG
|
||||
# data.2mg disk holding PROG + ntpplayer + DATA,
|
||||
# default build/iigs/bin/joey.2mg
|
||||
# Env: JOEY_GAME_DISK_SIZE cadius volume size, default "auto":
|
||||
# 800KB when the program fits beside a stripped system on a
|
||||
# floppy, otherwise 2048KB. cadius takes 800KB, 1440KB, 2048KB
|
||||
# and 32MB, nothing else. 800KB reuses the stock system image
|
||||
# in place; every other size builds a fresh volume and copies
|
||||
# the ProDOS boot blocks onto it. NOTE: MAME's emulated 3.5
|
||||
# drive is 800KB GCR and REJECTS a 1440KB image, so anything
|
||||
# larger has to boot from a CFFA2 card:
|
||||
# -sl7 cffa2 -hard1 <image>.
|
||||
# Requires: toolchains/env.sh sourced.
|
||||
set -euo pipefail
|
||||
|
||||
out=${1:?usage: make-iigs-game-disk.sh <out> <PROG> [volume] [data.2mg]}
|
||||
prog=${2:?usage: make-iigs-game-disk.sh <out> <PROG> [volume] [data.2mg]}
|
||||
vol=${3:-$prog}
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
: "${LLVM816_ROOT:?make-iigs-game-disk.sh: source toolchains/env.sh first}"
|
||||
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
|
||||
data=${4:-$repo/build/iigs/bin/joey.2mg}
|
||||
sysDisk="$repo/toolchains/emulators/support/gsos-system.po"
|
||||
size=${JOEY_GAME_DISK_SIZE:-auto}
|
||||
|
||||
[ -x "$CADIUS" ] || { echo "make-iigs-game-disk.sh: cadius not found at $CADIUS" >&2; exit 2; }
|
||||
[ -f "$sysDisk" ] || { echo "make-iigs-game-disk.sh: missing $sysDisk" >&2; exit 2; }
|
||||
[ -f "$data" ] || { echo "make-iigs-game-disk.sh: missing data disk $data" >&2; exit 2; }
|
||||
|
||||
work=$(mktemp -d -t joeylib-gamedisk.XXXXXX)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
# Volume name of an image, from the one whole line of the form "/NAME/".
|
||||
# An unanchored match also hits the absolute image path in cadius's header.
|
||||
volumeName() {
|
||||
"$CADIUS" CATALOG "$1" 2>/dev/null | grep -oE '^/[A-Za-z0-9._]+/$' | head -1 | tr -d '/'
|
||||
}
|
||||
freeBlocks() { "$CADIUS" CATALOG "$1" 2>/dev/null | grep -oE 'Free : [0-9]+' | grep -oE '[0-9]+'; }
|
||||
totalBlocks() { "$CADIUS" CATALOG "$1" 2>/dev/null | grep -oE 'Block : [0-9]+' | grep -oE '[0-9]+'; }
|
||||
|
||||
sysVol=$(volumeName "$sysDisk")
|
||||
[ -n "$sysVol" ] || { echo "make-iigs-game-disk.sh: cannot read system volume name" >&2; exit 1; }
|
||||
dataVol=$(volumeName "$data")
|
||||
[ -n "$dataVol" ] || { echo "make-iigs-game-disk.sh: cannot read data volume name" >&2; exit 1; }
|
||||
|
||||
# ----- the stripped system, staged on a copy of the stock disk -----
|
||||
staged="$work/system.po"
|
||||
cp "$sysDisk" "$staged"
|
||||
|
||||
drop=(
|
||||
"BASIC.System" # ProDOS 8 BASIC
|
||||
"Finder.Data"
|
||||
"Icons/FType.Apple" # Finder file-type database
|
||||
"System/Start" # the Finder itself
|
||||
"System/P8" # the ProDOS 8 kernel
|
||||
"System/Finder.Data"
|
||||
"System/CDevs/CDev.Data"
|
||||
"System/CDevs/Printer"
|
||||
"System/CDevs/Time"
|
||||
"System/Desk.Accs/ControlPanel"
|
||||
"System/Drivers/AppleDisk5.25"
|
||||
"System/Drivers/Finder.Data"
|
||||
"System/Fonts/Font.Lists"
|
||||
"System/Tools/Tool014" # Window Manager
|
||||
"System/Tools/Tool015" # Menu Manager
|
||||
"System/Tools/Tool016" # Control Manager
|
||||
"System/Tools/Tool018" # QuickDraw II Auxiliary
|
||||
"System/Tools/Tool019" # Print Manager
|
||||
"System/Tools/Tool020" # LineEdit
|
||||
"System/Tools/Tool021" # Dialog Manager
|
||||
"System/Tools/Tool022" # Scrap Manager
|
||||
"System/Tools/Tool023" # Standard File
|
||||
"System/Tools/Tool025" # Note Synthesizer
|
||||
"System/Tools/Tool027" # Font Manager
|
||||
"System/Tools/Tool028" # List Manager
|
||||
"System/Tools/Tool034" # TextEdit
|
||||
)
|
||||
# What canNOT come off, each one measured on a CFFA2 boot 2026-09-22:
|
||||
# System/Error.Msg GS/OS halts with "Error loading
|
||||
# ERROR.MSG file. Error=$0046".
|
||||
# System.Setup/Tool.Setup + TS3 dropping both hangs in the ROM
|
||||
# banner before GS/OS starts.
|
||||
# System.Setup/TS2 the ROM 01 half of the same tool
|
||||
# patches. Kept on purpose: plenty of
|
||||
# ROM 01 machines are still out there
|
||||
# and a game disk has to boot on them.
|
||||
# It costs 72 blocks.
|
||||
# System.Setup/Resource.Mgr,
|
||||
# Drivers/Console.Driver,
|
||||
# FSTs/Char.FST dropping all three hangs the same way.
|
||||
# The two groups were bisected separately and each one is load-bearing, so
|
||||
# the list above is the floor. Boot to the program went from 43.4 s to
|
||||
# 21.8 s in MAME, and the freed space is what lets the program share the
|
||||
# volume.
|
||||
for f in "${drop[@]}"; do
|
||||
"$CADIUS" DELETEFILE "$staged" "/$sysVol/$f" >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
# How many blocks the program folder will want, and what a stripped
|
||||
# floppy has left after the strip above. 24 blocks of slack covers the
|
||||
# launcher and the directory blocks the new subfolders need.
|
||||
progBlocks=$(( $(totalBlocks "$data") - $(freeBlocks "$data") ))
|
||||
floppyFree=$(freeBlocks "$staged")
|
||||
if [ "$size" = "auto" ]; then
|
||||
if [ $(( progBlocks + 24 )) -le "$floppyFree" ]; then
|
||||
size=800KB
|
||||
else
|
||||
size=2048KB
|
||||
fi
|
||||
fi
|
||||
|
||||
# ----- the output volume -----
|
||||
# Always a freshly created volume, whatever the size: that way cadius sets
|
||||
# up the image container the output's extension calls for (.po raw, .2mg
|
||||
# with its 64-byte header), and one code path covers every size. A
|
||||
# cadius-created volume has no boot blocks, so blocks 0 and 1 are copied
|
||||
# from the stock image. That code reads the volume directory and loads
|
||||
# /PRODOS by name, so it does not care how large the volume is or what it
|
||||
# is called.
|
||||
"$CADIUS" CREATEVOLUME "$out" "$vol" "$size" >/dev/null
|
||||
mkdir -p "$work/sys"
|
||||
"$CADIUS" EXTRACTVOLUME "$staged" "$work/sys" >/dev/null
|
||||
sysSrc="$work/sys/$sysVol"
|
||||
[ -d "$sysSrc" ] || { echo "make-iigs-game-disk.sh: extraction produced no /$sysVol" >&2; exit 1; }
|
||||
# ADDFOLDER copies a directory's CONTENTS into the named ProDOS folder,
|
||||
# keeping nested subfolders, and reads back the file types that
|
||||
# EXTRACTVOLUME recorded in each _FileInformation.txt. One call moves the
|
||||
# whole system across.
|
||||
"$CADIUS" ADDFOLDER "$out" "/$vol" "$sysSrc" >/dev/null
|
||||
srcOff=0; dstOff=0
|
||||
[ "$(head -c 4 "$staged")" = "2IMG" ] && srcOff=64
|
||||
[ "$(head -c 4 "$out")" = "2IMG" ] && dstOff=64
|
||||
dd if="$staged" of="$work/boot.blk" bs=1 skip=$srcOff count=1024 status=none
|
||||
dd if="$work/boot.blk" of="$out" bs=1 seek=$dstOff count=1024 conv=notrunc status=none
|
||||
|
||||
# ----- the launcher -----
|
||||
"$repo/toolchains/iigs/clang-build.sh" \
|
||||
-DJOEY_START_PATH="\"/$vol/$prog/$prog\"" \
|
||||
-o "$work/Start#B30000" \
|
||||
"$repo/tools/iigsStart/iigsStart.c" \
|
||||
"$repo/tools/iigsStart/iigsStartQuit.s" >/dev/null
|
||||
"$CADIUS" ADDFILE "$out" "/$vol/System" "$work/Start#B30000" >/dev/null
|
||||
|
||||
# ----- the program folder, lifted whole off the data disk -----
|
||||
mkdir -p "$work/game"
|
||||
"$CADIUS" EXTRACTVOLUME "$data" "$work/game" >/dev/null
|
||||
src="$work/game/$dataVol"
|
||||
[ -d "$src" ] || { echo "make-iigs-game-disk.sh: extraction produced no /$dataVol" >&2; exit 1; }
|
||||
|
||||
free=$(freeBlocks "$out")
|
||||
if [ "$free" -lt "$progBlocks" ]; then
|
||||
echo "make-iigs-game-disk.sh: /$prog needs $progBlocks blocks and a $size volume has only $free free." >&2
|
||||
echo " Build a larger one (JOEY_GAME_DISK_SIZE=2048KB) and boot it from a CFFA2 card:" >&2
|
||||
echo " mame apple2gs -sl7 cffa2 -hard1 <image>" >&2
|
||||
exit 1
|
||||
fi
|
||||
"$CADIUS" CREATEFOLDER "$out" "/$vol/$prog" >/dev/null
|
||||
"$CADIUS" ADDFOLDER "$out" "/$vol/$prog" "$src" >/dev/null
|
||||
|
||||
# Never trust an exit code here: cadius reports most failures on stdout.
|
||||
# Assert the three things the disk is worthless without.
|
||||
catalog=$("$CADIUS" CATALOG "$out" 2>/dev/null)
|
||||
for want in "ProDOS" "Start" "$prog"; do
|
||||
echo "$catalog" | grep -qE "^ +$want +" || {
|
||||
echo "make-iigs-game-disk.sh: $want is NOT on $out" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
if [ "$size" = "800KB" ]; then
|
||||
how="bootable 3.5\" floppy"
|
||||
else
|
||||
how="boot with: mame apple2gs -sl7 cffa2 -hard1 $(basename "$out")"
|
||||
fi
|
||||
echo "iigs-game-disk: $out"
|
||||
echo " /$vol boots into /$vol/$prog/$prog ($size, $(freeBlocks "$out") blocks free)"
|
||||
echo " $how"
|
||||
302
scripts/profile-x68000.sh
Executable file
302
scripts/profile-x68000.sh
Executable file
|
|
@ -0,0 +1,302 @@
|
|||
#!/usr/bin/env bash
|
||||
# profile-x68000.sh - PC-sampling profiler for a running X68000 example.
|
||||
#
|
||||
# Human68k relocates a .X anywhere it likes and the ELF beside it is linked at
|
||||
# zero, so the first job is finding where the program actually landed. Every
|
||||
# xdev68k binary opens with the same "#NEWLIB" marker two bytes into .text, so
|
||||
# the script scans RAM for that -- but the marker appears TWICE, because the
|
||||
# loader leaves a copy of the file in a disk buffer, and picking the wrong one
|
||||
# silently attributes every sample to whatever symbol happens to sit at that
|
||||
# offset. So it collects every candidate and then lets the program counter
|
||||
# choose -- except the two ranges OVERLAP, so containment is not enough. What
|
||||
# separates them is that Human68k RELOCATES the copy it runs: at a known
|
||||
# relocation site the running image holds fileValue + base, while the disk
|
||||
# buffer still holds fileValue. From then on
|
||||
# it samples the PC once per emulated frame, buckets by offset into .text, and
|
||||
# attributes the buckets to functions with nm.
|
||||
#
|
||||
# Sampling once per frame is coarse (55 Hz), but this port runs a game frame in
|
||||
# 150 ms or more, so each frame still gets several samples and the shape of the
|
||||
# profile is clear. Read it as "which functions dominate", never as a timing.
|
||||
#
|
||||
# Usage: scripts/profile-x68000.sh [example] [seconds] [skipSeconds]
|
||||
# example .X base name in build/x68000/bin, default staxi
|
||||
# seconds emulated seconds to run, default 240
|
||||
# skipSeconds ignore samples before this, default 130 (boot + menu)
|
||||
# Env: X68K_DIR dir holding HUMAN302.XDF + roms/ (default toolchains/x68mame)
|
||||
# Requires: the example built WITH symbols (build/x68000/bin/<NAME>.X.elf).
|
||||
set -uo pipefail
|
||||
|
||||
prog=${1:-staxi}
|
||||
seconds=${2:-240}
|
||||
skip=${3:-130}
|
||||
PROG=$(echo "$prog" | tr '[:lower:]' '[:upper:]')
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
bin=$repo/build/x68000/bin/$PROG.X
|
||||
elf=$bin.elf
|
||||
x68=${X68K_DIR:-$repo/toolchains/x68mame}
|
||||
template=$x68/HUMAN302.XDF
|
||||
export PATH="$repo/toolchains/x68000/m68k-xelf/bin:$PATH"
|
||||
|
||||
MAME="${MAME:-$repo/toolchains/cache/mame-mame0264/x68k}"
|
||||
[[ -x $MAME ]] || MAME="$(command -v mame || true)"
|
||||
[[ -n ${MAME:-} && -x $MAME ]] || { echo "profile-x68000: no MAME (set MAME=...)" >&2; exit 2; }
|
||||
[[ -f $bin ]] || { echo "profile-x68000: $bin not built" >&2; exit 2; }
|
||||
[[ -f $elf ]] || { echo "profile-x68000: $elf missing (need symbols)" >&2; exit 2; }
|
||||
|
||||
# .text size from the ELF, so a candidate base can be tested by containment.
|
||||
textSize=$(m68k-xelf-readelf -S "$elf" | awk '/\.text/ { print strtonum("0x" $7); exit }')
|
||||
[[ -n ${textSize:-} && $textSize -gt 0 ]] || { echo "profile-x68000: cannot read .text size" >&2; exit 2; }
|
||||
|
||||
# A 32-bit relocation site in .text, and the value the .X file carries there.
|
||||
# The .X header is 64 bytes, so the file offset is 64 + the .text offset.
|
||||
relocSite=$(m68k-xelf-readelf -r "$elf" | awk '$2 ~ /01$/ && $3 == "R_68K_32" { print strtonum("0x" $1); exit }')
|
||||
[[ -n ${relocSite:-} ]] || { echo "profile-x68000: no R_68K_32 relocation to fingerprint with" >&2; exit 2; }
|
||||
relocValue=$(python3 -c "
|
||||
import sys
|
||||
d = open('$bin','rb').read()
|
||||
o = 64 + $relocSite
|
||||
print(int.from_bytes(d[o:o+4],'big'))
|
||||
")
|
||||
|
||||
# gMouseX is written once per input poll, i.e. once per game frame.
|
||||
mouseXOff=$(m68k-xelf-nm "$elf" | awk '$3 == "gMouseX" { print strtonum("0x" $1); exit }')
|
||||
[[ -n ${mouseXOff:-} ]] || { echo "profile-x68000: gMouseX not found; cannot count frames" >&2; exit 2; }
|
||||
|
||||
work=$(mktemp -d -t joey-x68prof.XXXXXX)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
printf 'B:\r\nB:\\%s.X\r\n\x1a' "$PROG" > "$work/AUTOEXEC.BAT"
|
||||
cp "$template" "$work/boot.xdf"
|
||||
cp "$template" "$work/data.xdf"
|
||||
python3 "$repo/tools/xdftool.py" empty "$work/data.xdf" >/dev/null
|
||||
for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do
|
||||
python3 "$repo/tools/xdftool.py" delete "$work/boot.xdf" "$f" >/dev/null 2>&1 || true
|
||||
done
|
||||
for d in SYS HIS BIN BASIC2 ASK ETC; do
|
||||
python3 "$repo/tools/xdftool.py" deltree "$work/boot.xdf" "$d" >/dev/null 2>&1 || true
|
||||
done
|
||||
python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null
|
||||
python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$bin" "$PROG.X" >/dev/null
|
||||
data_dir=$repo/build/x68000/bin/DATA
|
||||
if [[ -d $data_dir ]]; then
|
||||
while IFS= read -r f; do
|
||||
rel=${f#"$data_dir"/}
|
||||
python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$f" \
|
||||
"DATA/$(echo "$rel" | tr '[:lower:]' '[:upper:]')" >/dev/null
|
||||
done < <(find "$data_dir" -type f | sort)
|
||||
fi
|
||||
|
||||
cat > "$work/prof.lua" <<LUA
|
||||
local cpu = manager.machine.devices[":maincpu"]
|
||||
local mem = cpu.spaces["program"]
|
||||
local out = io.open("prof.txt", "w")
|
||||
-- MAME's 68000 core does NOT expose a register called "PC": it has CURPC
|
||||
-- (where the CPU is now) and rPC. Asking for the wrong one throws inside the
|
||||
-- frame callback, once per frame, forever -- the run just gets slower and
|
||||
-- writes no samples. Resolve it once, loudly.
|
||||
local pcReg = nil
|
||||
for _, name in ipairs({ "CURPC", "rPC", "PC" }) do
|
||||
if cpu.state[name] ~= nil then
|
||||
pcReg = name
|
||||
break
|
||||
end
|
||||
end
|
||||
if pcReg == nil then
|
||||
out:write("no program-counter register on this CPU\n")
|
||||
out:close()
|
||||
manager.machine:exit()
|
||||
end
|
||||
|
||||
local base = nil
|
||||
local cands = {}
|
||||
local textSize = $textSize
|
||||
local mouseXOff = $mouseXOff
|
||||
local frames = 0
|
||||
local fpsStart = 0
|
||||
local relocSite = $relocSite
|
||||
local relocValue = $relocValue
|
||||
local scanAt = 0x1000
|
||||
local buckets = {}
|
||||
local total = 0
|
||||
local skipUntil = $skip
|
||||
local stopAt = $seconds
|
||||
|
||||
-- "#NEWLIB" sits two bytes into every xdev68k .text. 0x234E4557 is the long at
|
||||
-- offset 2, and 0x4C494200 follows it.
|
||||
-- A slice per frame so MAME keeps running, and it WRAPS: Human68k does not
|
||||
-- load the program until the boot finishes, which is a couple of minutes in,
|
||||
-- so a single pass over RAM finds nothing and must start again.
|
||||
local scanDone = false
|
||||
local function scanSlice()
|
||||
local limit = scanAt + 0x40000
|
||||
while scanAt < limit do
|
||||
if scanAt >= 0x400000 then
|
||||
scanDone = true
|
||||
return
|
||||
end
|
||||
if mem:read_u32(scanAt) == 0x234E4557 and mem:read_u32(scanAt + 4) == 0x4C494200 then
|
||||
cands[#cands + 1] = scanAt - 2
|
||||
end
|
||||
scanAt = scanAt + 2
|
||||
end
|
||||
end
|
||||
|
||||
-- Which candidate is the copy Human68k is RUNNING? At relocSite the .X file
|
||||
-- holds relocValue; the loaded image holds relocValue + its own base, because
|
||||
-- the loader patched it. The untouched disk buffer still matches the file.
|
||||
-- The PC must also be inside it, which rules out a stale image of an older
|
||||
-- build sitting in memory.
|
||||
local function pickBase()
|
||||
local pc = cpu.state[pcReg].value
|
||||
for _, c in ipairs(cands) do
|
||||
if pc >= c and pc < c + textSize then
|
||||
local got = mem:read_u32(c + relocSite)
|
||||
if got == ((relocValue + c) & 0xFFFFFFFF) then
|
||||
return c
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
emu.register_frame_done(function()
|
||||
local t = manager.machine.time:as_double()
|
||||
if base == nil then
|
||||
if not scanDone then
|
||||
scanSlice()
|
||||
return
|
||||
end
|
||||
base = pickBase()
|
||||
if base ~= nil then
|
||||
out:write(string.format("base %08X at %.1fs (of %d candidates)\n", base, t, #cands))
|
||||
out:flush()
|
||||
-- gMouseX is written once per jlpInputPoll, which the game calls
|
||||
-- once a frame, so counting writes to it counts game frames. That
|
||||
-- turns "how much of the frame is X" into "and how long is a
|
||||
-- frame", which is the difference between a real speedup and one
|
||||
-- that only buys more idle time.
|
||||
fpsTap = mem:install_write_tap(base + mouseXOff, base + mouseXOff + 1, "fps", function(offset, data, mask)
|
||||
frames = frames + 1
|
||||
return data
|
||||
end)
|
||||
fpsStart = t
|
||||
return
|
||||
end
|
||||
-- Nothing the PC is inside yet: the program is not loaded, or it is
|
||||
-- and the sweep predates it. Sweep again. The boot takes a couple of
|
||||
-- minutes, so the first few passes finding nothing is the normal case.
|
||||
cands = {}
|
||||
scanAt = 0x1000
|
||||
scanDone = false
|
||||
return
|
||||
end
|
||||
if t < skipUntil then return end
|
||||
local pc = cpu.state[pcReg].value
|
||||
local key
|
||||
if pc >= base and pc < base + textSize then
|
||||
-- The EXACT offset, not a 64-byte bucket. Buckets straddle function
|
||||
-- boundaries, and a bucket credited to the symbol at its start hands
|
||||
-- one function's samples to its neighbour: that is how a vblank wait
|
||||
-- came out looking like an expensive input poll.
|
||||
key = pc - base
|
||||
else
|
||||
key = -1 -- ROM, IOCS, anything not ours
|
||||
end
|
||||
buckets[key] = (buckets[key] or 0) + 1
|
||||
total = total + 1
|
||||
if t >= stopAt then
|
||||
out:write(string.format("fps %.2f frames %d window %.1f tapAt %08X\n",
|
||||
(t > fpsStart) and (frames / (t - fpsStart)) or 0.0, frames, t - fpsStart, base + mouseXOff))
|
||||
out:write(string.format("total %d\n", total))
|
||||
for k, v in pairs(buckets) do
|
||||
out:write(string.format("%d %d\n", k, v))
|
||||
end
|
||||
out:write("DONE\n")
|
||||
out:close()
|
||||
manager.machine:exit()
|
||||
end
|
||||
end)
|
||||
LUA
|
||||
|
||||
( cd "$work" && SDL_AUDIODRIVER=dummy timeout 3600 "$MAME" x68000 \
|
||||
-bios ipl10 -rompath "$x68/roms" \
|
||||
-flop1 boot.xdf -flop2 data.xdf \
|
||||
-video none -sound none -nothrottle -skip_gameinfo \
|
||||
-autoboot_script prof.lua </dev/null >mame.log 2>&1 ) || true
|
||||
|
||||
[[ -s $work/prof.txt ]] || { echo "profile-x68000: FAIL (no samples)" >&2; exit 1; }
|
||||
grep -q '^base ' "$work/prof.txt" || { echo "profile-x68000: FAIL (never found the load base)" >&2; exit 1; }
|
||||
|
||||
m68k-xelf-nm -n "$elf" > "$work/syms.txt"
|
||||
python3 - "$work/prof.txt" "$work/syms.txt" <<'PY'
|
||||
import sys
|
||||
prof, symfile = sys.argv[1], sys.argv[2]
|
||||
syms = []
|
||||
for line in open(symfile):
|
||||
p = line.split()
|
||||
if len(p) == 3 and p[1] in ('t', 'T'):
|
||||
syms.append((int(p[0], 16), p[2]))
|
||||
syms.sort()
|
||||
|
||||
def owner(off):
|
||||
lo, hi = 0, len(syms) - 1
|
||||
best = None
|
||||
while lo <= hi:
|
||||
mid = (lo + hi) // 2
|
||||
if syms[mid][0] <= off:
|
||||
best = syms[mid][1]
|
||||
lo = mid + 1
|
||||
else:
|
||||
hi = mid - 1
|
||||
return best or "?"
|
||||
|
||||
total = 0
|
||||
fps = None
|
||||
hits = {}
|
||||
buckets = {}
|
||||
outside = 0
|
||||
for line in open(prof):
|
||||
p = line.split()
|
||||
if p and p[0] == 'total':
|
||||
total = int(p[1])
|
||||
elif len(p) == 2 and p[0].lstrip('-').isdigit():
|
||||
off, n = int(p[0]), int(p[1])
|
||||
buckets[off] = buckets.get(off, 0) + n
|
||||
if off < 0:
|
||||
outside += n
|
||||
else:
|
||||
hits[owner(off)] = hits.get(owner(off), 0) + n
|
||||
if total == 0:
|
||||
sys.exit("profile-x68000: no samples recorded")
|
||||
rows = sorted(hits.items(), key=lambda kv: -kv[1])
|
||||
fpsText = f", game running at {fps:.1f} fps" if fps is not None and fps > 0.0 else " (frame count unavailable)"
|
||||
print(f"profile-x68000: {total} samples, {outside} ({outside*100.0/total:.1f}%) outside the program (ROM/IOCS){fpsText}")
|
||||
shown = 0
|
||||
for name, n in rows[:25]:
|
||||
print(f" {n*100.0/total:5.1f}% {n:6d} {name}")
|
||||
shown += n
|
||||
print(f" {(total-outside-shown)*100.0/total:5.1f}% {total-outside-shown:6d} (everything else)")
|
||||
|
||||
# The hottest 64-byte buckets, named as symbol+offset. Function totals say
|
||||
# which routine to look at; these say which LINE of it, which is the
|
||||
# difference between rewriting a loop and rewriting the wrong loop.
|
||||
print("\nhottest addresses (symbol + offset into it):")
|
||||
def ownerOff(off):
|
||||
lo, hi = 0, len(syms) - 1
|
||||
best = None
|
||||
while lo <= hi:
|
||||
mid = (lo + hi) // 2
|
||||
if syms[mid][0] <= off:
|
||||
best = syms[mid]
|
||||
lo = mid + 1
|
||||
else:
|
||||
hi = mid - 1
|
||||
return (best[1], off - best[0]) if best else ("?", 0)
|
||||
for off, n in sorted(buckets.items(), key=lambda kv: -kv[1])[:20]:
|
||||
if off < 0:
|
||||
continue
|
||||
name, delta = ownerOff(off)
|
||||
print(f" {n*100.0/total:5.1f}% {n:6d} {name}+0x{delta:x}")
|
||||
PY
|
||||
107
scripts/run-amiga-headless.sh
Executable file
107
scripts/run-amiga-headless.sh
Executable file
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env bash
|
||||
# run-amiga-headless.sh - Run one built Amiga example under FS-UAE with no
|
||||
# window, wait, then hand back the example's joeylog.txt and a screenshot.
|
||||
#
|
||||
# This is the smoke test bench-amiga.sh cannot be: that script runs UBER
|
||||
# and waits for its "press any key to exit" line, but a game never exits.
|
||||
# Here the run is simply given N seconds and whatever it logged and drew
|
||||
# is reported.
|
||||
#
|
||||
# The Amiga's joeylog.txt lands in the directory mounted as the hard
|
||||
# drive, so it can be read straight off the host while the machine runs.
|
||||
#
|
||||
# Usage: scripts/run-amiga-headless.sh [example] [seconds] [fastKB]
|
||||
# example binary name in build/amiga/bin, default Taxi
|
||||
# seconds how long to let it run, default 90
|
||||
# fastKB fast memory in KB, default 2048. Pass 0 for a
|
||||
# genuinely bare 512 KB-chip A500 (slow memory off
|
||||
# as well), which is the tight case.
|
||||
# Requires: fs-uae, Xvfb, toolchains/emulators/support/kickstart.rom.
|
||||
set -euo pipefail
|
||||
|
||||
prog=${1:-Taxi}
|
||||
seconds=${2:-90}
|
||||
fastKb=${3:-2048}
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
binDir=$repo/build/amiga/bin
|
||||
support=$repo/toolchains/emulators/support
|
||||
kickstart=$support/kickstart.rom
|
||||
outDir=${AMIGA_OUT_DIR:-$repo/build/amiga/headless}
|
||||
|
||||
# Most examples are a bare binary in bin/; Space Taxi ships as its own
|
||||
# drawer holding the binary and its DATA tree, so accept either shape.
|
||||
if [ -d "$binDir/$prog" ]; then
|
||||
progDir=$binDir/$prog
|
||||
[ -f "$progDir/$prog" ] || { echo "run-amiga-headless: $progDir/$prog not built" >&2; exit 2; }
|
||||
elif [ -f "$binDir/$prog" ]; then
|
||||
progDir=""
|
||||
else
|
||||
echo "run-amiga-headless: $binDir/$prog not built" >&2
|
||||
exit 2
|
||||
fi
|
||||
[ -f "$kickstart" ] || { echo "run-amiga-headless: missing $kickstart" >&2; exit 2; }
|
||||
|
||||
work=$(mktemp -d -t joeylib-amiga-run.XXXXXX)
|
||||
mkdir -p "$work/s" "$outDir"
|
||||
if [ -n "$progDir" ]; then
|
||||
cp -r "$progDir/." "$work/"
|
||||
else
|
||||
find "$binDir" -maxdepth 1 -type f -executable -exec cp -t "$work/" {} +
|
||||
[ -d "$binDir/DATA" ] && cp -r "$binDir/DATA" "$work/"
|
||||
fi
|
||||
printf 'stack 32768\n:%s\n' "$prog" > "$work/s/startup-sequence"
|
||||
|
||||
display=:$((90 + RANDOM % 9))
|
||||
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
|
||||
xvfbPid=$!
|
||||
sleep 1
|
||||
|
||||
fsArgs=(
|
||||
--amiga_model=A500
|
||||
--hard_drive_0="$work"
|
||||
--hard_drive_0_label=JOEYLIB
|
||||
--kickstart_file="$kickstart"
|
||||
--automatic_input_grab=0
|
||||
--fullscreen=0
|
||||
--fade_out_duration=0
|
||||
)
|
||||
# Always pass the memory explicitly. Leaving --fast_memory off does NOT
|
||||
# give a bare machine: FS-UAE's A500 default still fits the 512 KB
|
||||
# trapdoor expansion, which AvailMem reports as ~300 KB of FAST. A real
|
||||
# unexpanded A500 is 512 KB of CHIP and nothing else, so fastKB=0 has to
|
||||
# turn the slow memory off too.
|
||||
fsArgs+=(--fast_memory="$fastKb")
|
||||
[ "$fastKb" = "0" ] && fsArgs+=(--slow_memory=0)
|
||||
|
||||
DISPLAY=$display fs-uae "${fsArgs[@]}" > "$work/fsuae.log" 2>&1 &
|
||||
fsPid=$!
|
||||
cleanup() {
|
||||
kill "$fsPid" 2>/dev/null || true
|
||||
kill "$xvfbPid" 2>/dev/null || true
|
||||
wait 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "run-amiga-headless: $prog for ${seconds}s on an A500 with ${fastKb} KB fast..." >&2
|
||||
elapsed=0
|
||||
while [ "$elapsed" -lt "$seconds" ]; do
|
||||
sleep 5
|
||||
elapsed=$((elapsed + 5))
|
||||
kill -0 "$fsPid" 2>/dev/null || { echo "run-amiga-headless: FS-UAE exited early" >&2; break; }
|
||||
done
|
||||
|
||||
DISPLAY=$display import -window root "$outDir/$prog.png" 2>/dev/null || true
|
||||
cp "$work/joeylog.txt" "$outDir/$prog-joeylog.txt" 2>/dev/null || true
|
||||
cp "$work/fsuae.log" "$outDir/$prog-fsuae.log" 2>/dev/null || true
|
||||
cleanup
|
||||
trap - EXIT
|
||||
rm -rf "$work"
|
||||
|
||||
echo "run-amiga-headless: wrote $outDir/$prog.png"
|
||||
if [ -f "$outDir/$prog-joeylog.txt" ]; then
|
||||
echo "--- joeylog.txt (last 25 lines)"
|
||||
tail -25 "$outDir/$prog-joeylog.txt"
|
||||
else
|
||||
echo "run-amiga-headless: NO joeylog.txt -- the example never logged anything" >&2
|
||||
fi
|
||||
83
scripts/run-atarist-headless.sh
Executable file
83
scripts/run-atarist-headless.sh
Executable file
|
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env bash
|
||||
# run-atarist-headless.sh - Run one built Atari ST example under Hatari
|
||||
# with no window, wait, then hand back its joeylog.txt and a screenshot.
|
||||
#
|
||||
# The ST twin of run-amiga-headless.sh, and for the same reason:
|
||||
# bench-atarist.sh runs UBER and waits for its "press any key to exit"
|
||||
# line, which a game never prints.
|
||||
#
|
||||
# Hatari mounts the program's directory as GEMDOS drive C, so the app's
|
||||
# joeylog.txt lands there and can be read off the host while it runs.
|
||||
#
|
||||
# Usage: scripts/run-atarist-headless.sh [example] [seconds]
|
||||
# example PRG base name in build/atarist/bin, default STAXI
|
||||
# seconds how long to let it run, default 90
|
||||
# Env: ST_SOUND=on keep the YM running (default off, it is faster)
|
||||
# ST_MEMSIZE=n machine RAM in MB, passed to Hatari. Default is
|
||||
# Hatari's own; 1 is a stock 1040 ST.
|
||||
# Requires: hatari, Xvfb, toolchains/emulators/support/emutos-512k.img
|
||||
set -euo pipefail
|
||||
|
||||
prog=${1:-STAXI}
|
||||
seconds=${2:-90}
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
binDir=$repo/build/atarist/bin
|
||||
tos=$repo/toolchains/emulators/support/emutos-512k.img
|
||||
outDir=${ST_OUT_DIR:-$repo/build/atarist/headless}
|
||||
|
||||
# Most examples are a bare PRG in bin/; Space Taxi ships as its own
|
||||
# folder holding the PRG and its DATA tree.
|
||||
if [ -d "$binDir/$prog" ]; then
|
||||
driveDir=$binDir/$prog
|
||||
else
|
||||
driveDir=$binDir
|
||||
fi
|
||||
[ -f "$driveDir/$prog.PRG" ] || { echo "run-atarist-headless: $driveDir/$prog.PRG not built" >&2; exit 2; }
|
||||
[ -f "$tos" ] || { echo "run-atarist-headless: missing $tos" >&2; exit 2; }
|
||||
|
||||
mkdir -p "$outDir"
|
||||
rm -f "$driveDir/joeylog.txt"
|
||||
|
||||
display=:$((90 + RANDOM % 9))
|
||||
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
|
||||
xvfbPid=$!
|
||||
sleep 1
|
||||
|
||||
DISPLAY=$display hatari \
|
||||
--tos "$tos" \
|
||||
--harddrive "$driveDir" \
|
||||
--gemdos-drive C \
|
||||
--drive-a off --drive-b off \
|
||||
--sound "${ST_SOUND:-off}" \
|
||||
${ST_MEMSIZE:+--memsize "$ST_MEMSIZE"} \
|
||||
--fast-forward on \
|
||||
--auto "C:\\$prog.PRG" > "$outDir/$prog-hatari.log" 2>&1 &
|
||||
hatPid=$!
|
||||
cleanup() {
|
||||
kill "$hatPid" 2>/dev/null || true
|
||||
kill "$xvfbPid" 2>/dev/null || true
|
||||
wait 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "run-atarist-headless: $prog for ${seconds}s..." >&2
|
||||
elapsed=0
|
||||
while [ "$elapsed" -lt "$seconds" ]; do
|
||||
sleep 5
|
||||
elapsed=$((elapsed + 5))
|
||||
kill -0 "$hatPid" 2>/dev/null || { echo "run-atarist-headless: Hatari exited early" >&2; break; }
|
||||
done
|
||||
|
||||
DISPLAY=$display import -window root "$outDir/$prog.png" 2>/dev/null || true
|
||||
cp "$driveDir/joeylog.txt" "$outDir/$prog-joeylog.txt" 2>/dev/null || true
|
||||
cleanup
|
||||
trap - EXIT
|
||||
|
||||
echo "run-atarist-headless: wrote $outDir/$prog.png"
|
||||
if [ -f "$outDir/$prog-joeylog.txt" ]; then
|
||||
echo "--- joeylog.txt (last 25 lines)"
|
||||
tail -25 "$outDir/$prog-joeylog.txt"
|
||||
else
|
||||
echo "run-atarist-headless: NO joeylog.txt -- the example never logged anything" >&2
|
||||
fi
|
||||
117
scripts/verify-iigs-game-disk.sh
Executable file
117
scripts/verify-iigs-game-disk.sh
Executable file
|
|
@ -0,0 +1,117 @@
|
|||
#!/usr/bin/env bash
|
||||
# verify-iigs-game-disk.sh - Boot a disk made by make-iigs-game-disk.sh in
|
||||
# headless MAME and assert it reaches the program and draws.
|
||||
#
|
||||
# Two checkpoints, both read out of memory rather than off the screen:
|
||||
# 1. $E1:9DC8 == 0xAA. tools/iigsStart writes that marker immediately
|
||||
# before its QuitGS, so it means GS/OS booted and the launcher ran.
|
||||
# (0xEE would mean QuitGS returned, i.e. the program did not launch.)
|
||||
# 2. The Super Hi-Res framebuffer, sampled a while later: the count of
|
||||
# distinct nibble values, as scripts/verify-iigs.sh uses. A blank
|
||||
# screen is 1. There is no Finder on these disks, so unlike that
|
||||
# script's ~9-10 desktop baseline anything above 1 is the program's
|
||||
# own drawing.
|
||||
#
|
||||
# An 800KB image goes in the 3.5 drive; anything larger has to come off a
|
||||
# CFFA2 card because MAME's drive is 800KB GCR only.
|
||||
#
|
||||
# Usage: scripts/verify-iigs-game-disk.sh <image> [minDistinct] [ramsize]
|
||||
# Env: GAME_VERIFY_FRAMES frame at which to sample (default 5400, 90 s)
|
||||
# GAME_VERIFY_MACHINE MAME machine, default apple2gs (ROM 3).
|
||||
# Use apple2gsr1 to check a ROM 01 machine; these disks keep
|
||||
# TS2 so they must boot on both.
|
||||
# Requires: toolchains/env.sh sourced.
|
||||
set -euo pipefail
|
||||
|
||||
img=${1:?usage: verify-iigs-game-disk.sh <image> [minDistinct] [ramsize]}
|
||||
minDistinct=${2:-2}
|
||||
ramsize=${3:-4M}
|
||||
readFrame=${GAME_VERIFY_FRAMES:-5400}
|
||||
machine=${GAME_VERIFY_MACHINE:-apple2gs}
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
MAME="${MAME:-mame}"
|
||||
rompath="$HOME/.mame/roms;$repo/toolchains/emulators/support/mame-roms"
|
||||
name=$(basename "$img")
|
||||
|
||||
[ -f "$img" ] || { echo "verify-iigs-game-disk: no such image $img" >&2; exit 2; }
|
||||
|
||||
# MAME opens a hard-disk image read-write, so two runs must never share
|
||||
# one file. Work on a private copy.
|
||||
work=$(mktemp -d -t joeylib-gdverify.XXXXXX)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
cp "$img" "$work/$name"
|
||||
|
||||
bytes=$(stat -c %s "$img")
|
||||
if [ "$bytes" -le 900000 ]; then
|
||||
media=(-flop3 "$work/$name")
|
||||
how="3.5 drive"
|
||||
else
|
||||
media=(-sl7 cffa2 -hard1 "$work/$name")
|
||||
how="CFFA2 card"
|
||||
fi
|
||||
|
||||
cat > "$work/verify.lua" <<LUA
|
||||
local cpu = manager.machine.devices[":maincpu"]
|
||||
local mem = cpu.spaces["program"]
|
||||
local frame = 0
|
||||
local quitAt = nil
|
||||
emu.register_frame_done(function()
|
||||
frame = frame + 1
|
||||
if quitAt == nil then
|
||||
local mb = mem:read_u8(0xE19DC8)
|
||||
if mb == 0xAA or mb == 0xEE then
|
||||
quitAt = frame
|
||||
io.write(string.format("GAMEDISK-BOOT name=$name mailbox=%02X frame=%d seconds=%.1f\\n", mb, frame, frame / 60.0))
|
||||
io.flush()
|
||||
end
|
||||
end
|
||||
if frame == $readFrame then
|
||||
local seen = {}
|
||||
local distinct = 0
|
||||
local nonzero = 0
|
||||
for a = 0xE12000, 0xE12000 + 31999 do
|
||||
local b = mem:read_u8(a)
|
||||
if b ~= 0 then nonzero = nonzero + 1 end
|
||||
local hi = (b >> 4) & 0xF
|
||||
local lo = b & 0xF
|
||||
if not seen[hi] then seen[hi] = true; distinct = distinct + 1 end
|
||||
if not seen[lo] then seen[lo] = true; distinct = distinct + 1 end
|
||||
end
|
||||
io.write(string.format("GAMEDISK-DRAW name=$name frame=%d distinctNibbles=%d nonZeroBytes=%d scb0=%02X\\n",
|
||||
frame, distinct, nonzero, mem:read_u8(0xE19D00)))
|
||||
io.flush()
|
||||
manager.machine:exit()
|
||||
end
|
||||
end)
|
||||
LUA
|
||||
|
||||
out=$(cd "$work" && QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
|
||||
timeout 600 "$MAME" "$machine" \
|
||||
-ramsize "$ramsize" -rompath "$rompath" "${media[@]}" \
|
||||
-video none -sound none -nothrottle -skip_gameinfo \
|
||||
-autoboot_script "$work/verify.lua" </dev/null 2>&1) || true
|
||||
|
||||
boot=$(echo "$out" | grep -m1 'GAMEDISK-BOOT' || true)
|
||||
draw=$(echo "$out" | grep -m1 'GAMEDISK-DRAW' || true)
|
||||
[ -n "$boot" ] && echo "$boot"
|
||||
[ -n "$draw" ] && echo "$draw"
|
||||
|
||||
if [ -z "$boot" ]; then
|
||||
echo "verify-iigs-game-disk: FAIL ($name never reached the launcher's QuitGS marker; GS/OS did not boot from the $how)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if echo "$boot" | grep -q 'mailbox=EE'; then
|
||||
echo "verify-iigs-game-disk: FAIL ($name QuitGS returned, so the program did not launch)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$draw" ]; then
|
||||
echo "verify-iigs-game-disk: FAIL ($name booted but the screen was never sampled)" >&2
|
||||
exit 1
|
||||
fi
|
||||
distinct=$(echo "$draw" | sed -E 's/.*distinctNibbles=([0-9]+).*/\1/')
|
||||
if [ "$distinct" -lt "$minDistinct" ]; then
|
||||
echo "verify-iigs-game-disk: FAIL ($name distinctNibbles=$distinct < $minDistinct - screen looks blank)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "verify-iigs-game-disk: PASS ($name booted off the $how on $machine and drew, distinctNibbles=$distinct >= $minDistinct)"
|
||||
219
scripts/verify-x68000-audio.sh
Executable file
219
scripts/verify-x68000-audio.sh
Executable file
|
|
@ -0,0 +1,219 @@
|
|||
#!/usr/bin/env bash
|
||||
# verify-x68000-audio.sh - X68000 audio gate, read off the chips.
|
||||
#
|
||||
# Boots a built example on a Human68k floppy pair under headless MAME and
|
||||
# watches the two sound devices through memory taps, so the checks are on
|
||||
# what the hardware was actually told, not on a log line:
|
||||
#
|
||||
# 1. PPI port C ($E9A005) low nibble. That nibble IS the MSM6258's
|
||||
# sample rate (bits 3,2) and channel gating (bits 1,0): the gate
|
||||
# wants rate index 2, divider 512, 7812 Hz -- the rate the mixer
|
||||
# renders at -- and pan 0, both channels. IOCS's own mode byte does
|
||||
# not set these correctly (measured: mode 0x05 leaves 0x01, which is
|
||||
# 3906 Hz out of the right speaker only), so audioPcm.c writes the
|
||||
# register itself and this checks that it stuck.
|
||||
# 2. Every YM2151 noise write ($E90000 register 0x0F). The low five
|
||||
# bits must be a value from kOpmNoiseFreq in src/x68000/audio.c,
|
||||
# which is read out of the source here rather than duplicated: a
|
||||
# pitch that escapes the table is the bug this gate exists for
|
||||
# (a plain `31 - pitch` ran the generator an octave bright).
|
||||
# 3. The ADPCM byte feed rate WHILE A SOUND IS PLAYING. Two samples per
|
||||
# byte at 7812 Hz is 3906 bytes/s; half that means the divider is
|
||||
# wrong again and every sample the port plays is an octave low. It
|
||||
# is the rate inside one burst of feeding that counts, not the
|
||||
# average: the port stops the chip between sounds, so an average
|
||||
# over a mostly silent run would say nothing.
|
||||
#
|
||||
# Space Taxi is the default because its attract demo exercises the whole
|
||||
# noise curve (all seventeen distinct table values) and speaks.
|
||||
#
|
||||
# Usage: scripts/verify-x68000-audio.sh [example] [seconds]
|
||||
# Env: X68K_DIR dir holding HUMAN302.XDF + roms/ (default toolchains/x68mame)
|
||||
# Requires: the example built (make -f make/x68000.mk), MAME with x68000.
|
||||
set -uo pipefail
|
||||
|
||||
prog=${1:-staxi}
|
||||
seconds=${2:-160}
|
||||
PROG=$(echo "$prog" | tr '[:lower:]' '[:upper:]')
|
||||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
bin=$repo/build/x68000/bin/$PROG.X
|
||||
x68=${X68K_DIR:-$repo/toolchains/x68mame}
|
||||
template=$x68/HUMAN302.XDF
|
||||
source=$repo/src/x68000/audio.c
|
||||
|
||||
MAME="${MAME:-$repo/toolchains/cache/mame-mame0264/x68k}"
|
||||
if [[ ! -x $MAME ]]; then
|
||||
MAME="$(command -v mame || true)"
|
||||
fi
|
||||
[[ -n ${MAME:-} && -x $MAME ]] || { echo "verify-x68000-audio: no MAME (set MAME=...)" >&2; exit 2; }
|
||||
[[ -f $bin ]] || { echo "verify-x68000-audio: $bin not built" >&2; exit 2; }
|
||||
[[ -f $template ]] || { echo "verify-x68000-audio: missing $template" >&2; exit 2; }
|
||||
|
||||
# The noise table, straight out of the port so the two cannot drift apart.
|
||||
table=$(python3 - "$source" <<'PY'
|
||||
import re, sys
|
||||
src = open(sys.argv[1]).read()
|
||||
m = re.search(r'kOpmNoiseFreq\[32\]\s*=\s*\{(.*?)\}', src, re.S)
|
||||
if not m:
|
||||
sys.exit("kOpmNoiseFreq not found")
|
||||
print(",".join(v.strip() for v in m.group(1).split(",") if v.strip()))
|
||||
PY
|
||||
) || { echo "verify-x68000-audio: cannot read kOpmNoiseFreq from $source" >&2; exit 2; }
|
||||
|
||||
work=$(mktemp -d -t joey-x68audio.XXXXXX)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
printf 'B:\r\nB:\\%s.X\r\n\x1a' "$PROG" > "$work/AUTOEXEC.BAT"
|
||||
cp "$template" "$work/boot.xdf"
|
||||
cp "$template" "$work/data.xdf"
|
||||
python3 "$repo/tools/xdftool.py" empty "$work/data.xdf" >/dev/null
|
||||
for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do
|
||||
python3 "$repo/tools/xdftool.py" delete "$work/boot.xdf" "$f" >/dev/null 2>&1 || true
|
||||
done
|
||||
for d in SYS HIS BIN BASIC2 ASK ETC; do
|
||||
python3 "$repo/tools/xdftool.py" deltree "$work/boot.xdf" "$d" >/dev/null 2>&1 || true
|
||||
done
|
||||
python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null
|
||||
python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$bin" "$PROG.X" >/dev/null
|
||||
data_dir=$repo/build/x68000/bin/DATA
|
||||
if [[ -d $data_dir ]]; then
|
||||
while IFS= read -r f; do
|
||||
rel=${f#"$data_dir"/}
|
||||
python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$f" \
|
||||
"DATA/$(echo "$rel" | tr '[:lower:]' '[:upper:]')" >/dev/null
|
||||
done < <(find "$data_dir" -type f | sort)
|
||||
fi
|
||||
|
||||
# The taps keep to arithmetic and file writes: a tap that reads memory
|
||||
# takes MAME down with it.
|
||||
cat > "$work/tap.lua" <<LUA
|
||||
local cpu = manager.machine.devices[":maincpu"]
|
||||
local mem = cpu.spaces["program"]
|
||||
local out = io.open("audio.txt", "w")
|
||||
local sel = -1
|
||||
local pcmWrites = 0
|
||||
local burstCount = 0
|
||||
local burstStart = 0
|
||||
local burstLast = -1
|
||||
local bestRate = 0
|
||||
local stop = $seconds
|
||||
-- A local tap object is garbage collected and stops firing. Keep globals.
|
||||
opmTap = mem:install_write_tap(0xE90000, 0xE90003, "opm", function(offset, data, mask)
|
||||
local v = data & 0xFF
|
||||
if (mask & 0xFF) == 0 then v = (data >> 8) & 0xFF end
|
||||
if offset < 0xE90002 then
|
||||
sel = v
|
||||
elseif sel == 0x0F then
|
||||
out:write(string.format("NOISE %02X\\n", v))
|
||||
end
|
||||
return data
|
||||
end)
|
||||
ppiTap = mem:install_write_tap(0xE9A004, 0xE9A005, "ppi", function(offset, data, mask)
|
||||
local v = data & 0xFF
|
||||
if (mask & 0xFF) == 0 then v = (data >> 8) & 0xFF end
|
||||
out:write(string.format("PORTC %02X\\n", v))
|
||||
return data
|
||||
end)
|
||||
-- The port stops the chip between sounds, so the honest figure is the rate
|
||||
-- inside one burst of feeding, not an average across the silence. A gap of
|
||||
-- more than 100 ms ends a burst; bursts shorter than 300 ms are too short to
|
||||
-- time accurately and are skipped.
|
||||
local function closeBurst()
|
||||
if burstLast > burstStart + 0.3 and burstCount > 0 then
|
||||
local r = burstCount / (burstLast - burstStart)
|
||||
if r > bestRate then bestRate = r end
|
||||
end
|
||||
burstCount = 0
|
||||
end
|
||||
pcmTap = mem:install_write_tap(0xE92000, 0xE92003, "pcm", function(offset, data, mask)
|
||||
local t = manager.machine.time:as_double()
|
||||
pcmWrites = pcmWrites + 1
|
||||
if burstLast < 0 or (t - burstLast) > 0.1 then
|
||||
closeBurst()
|
||||
burstStart = t
|
||||
end
|
||||
burstLast = t
|
||||
burstCount = burstCount + 1
|
||||
return data
|
||||
end)
|
||||
emu.register_frame_done(function()
|
||||
local t = manager.machine.time:as_double()
|
||||
if t >= stop then
|
||||
closeBurst()
|
||||
out:write(string.format("PCMTOTAL %d\\n", pcmWrites))
|
||||
out:write(string.format("PCMRATE %.0f\\n", bestRate))
|
||||
out:write("DONE\\n")
|
||||
out:close()
|
||||
manager.machine:exit()
|
||||
end
|
||||
end)
|
||||
LUA
|
||||
|
||||
( cd "$work" && SDL_AUDIODRIVER=dummy timeout 1800 "$MAME" x68000 \
|
||||
-bios ipl10 -rompath "$x68/roms" \
|
||||
-flop1 boot.xdf -flop2 data.xdf \
|
||||
-video none -sound none -nothrottle -skip_gameinfo \
|
||||
-autoboot_script tap.lua </dev/null >mame.log 2>&1 ) || true
|
||||
|
||||
[[ -f $work/audio.txt ]] || { echo "verify-x68000-audio: FAIL (MAME produced no tap output)" >&2; exit 1; }
|
||||
|
||||
python3 - "$work/audio.txt" "$table" "$PROG" <<'PY'
|
||||
import sys
|
||||
path, table, prog = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
allowed = {int(v) for v in table.split(",")}
|
||||
noise, portc, rate, done = [], [], None, False
|
||||
for line in open(path):
|
||||
p = line.split()
|
||||
if not p:
|
||||
continue
|
||||
if p[0] == "NOISE":
|
||||
noise.append(int(p[1], 16))
|
||||
elif p[0] == "PORTC":
|
||||
portc.append(int(p[1], 16))
|
||||
elif p[0] == "PCMRATE":
|
||||
rate = float(p[1])
|
||||
elif p[0] == "DONE":
|
||||
done = True
|
||||
|
||||
fails = []
|
||||
if not done:
|
||||
fails.append("the run never reached its time limit")
|
||||
|
||||
# 1. The ADPCM rate/pan nibble. The last value written wins; the ROM's own
|
||||
# boot value (0x0B) does not count as ours.
|
||||
ours = [v for v in portc if v != 0x0B]
|
||||
if not ours:
|
||||
fails.append("PPI port C was never programmed, so the ADPCM never played")
|
||||
else:
|
||||
got = ours[-1] & 0x0F
|
||||
if got != 0x08:
|
||||
fails.append(f"port C low nibble {got:#04x}, want 0x08 "
|
||||
f"(rate index {(got >> 2) & 3} pan {got & 3}; want rate 2 = 7812 Hz, pan 0 = both)")
|
||||
|
||||
# 2. Every enabled noise write must name a table entry.
|
||||
on = {v & 0x1F for v in noise if v & 0x80}
|
||||
if not on:
|
||||
fails.append("no noise writes seen at all")
|
||||
stray = sorted(on - allowed)
|
||||
if stray:
|
||||
fails.append(f"noise codes outside kOpmNoiseFreq: {stray}")
|
||||
|
||||
# 3. Byte feed rate: two samples per byte at 7812 Hz.
|
||||
if rate is None:
|
||||
fails.append("the ADPCM channel was never fed")
|
||||
elif not (3500.0 <= rate <= 4300.0):
|
||||
fails.append(f"ADPCM in-burst feed {rate:.0f} bytes/s, want ~3906 "
|
||||
f"({'half rate, the divider is wrong' if rate < 2600 else 'off rate'})")
|
||||
|
||||
portcText = "/".join(f"{v:02X}" for v in ours[-2:]) if ours else "never set"
|
||||
rateText = f"{rate:.0f} bytes/s in-burst" if rate is not None else "silent"
|
||||
print(f"verify-x68000-audio: {prog} noise writes {len(noise)} "
|
||||
f"({len(on)} distinct codes, {len(allowed - on)} table entries unused), "
|
||||
f"port C {portcText}, ADPCM {rateText}")
|
||||
if fails:
|
||||
for f in fails:
|
||||
print(f"verify-x68000-audio: FAIL ({f})", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print("verify-x68000-audio: PASS")
|
||||
PY
|
||||
|
|
@ -16,6 +16,10 @@
|
|||
#define AMIGA_BITPLANES 4
|
||||
#define AMIGA_BYTES_PER_ROW (SURFACE_WIDTH / 8)
|
||||
#define AMIGA_PLANE_SIZE (AMIGA_BYTES_PER_ROW * SURFACE_HEIGHT)
|
||||
// What a surface actually allocates per plane: one byte of slack past the end,
|
||||
// which src/m68k/planarFill.s relies on. See the note at the AllocMem in
|
||||
// jlpSurfaceAllocPortData.
|
||||
#define AMIGA_PLANE_ALLOC (AMIGA_PLANE_SIZE + 1u)
|
||||
|
||||
// jlSurfaceT.portData points to this on Amiga: the 4 plane base
|
||||
// pointers + stride. Every surface (including the stage, which gets
|
||||
|
|
|
|||
|
|
@ -118,11 +118,26 @@ static BYTE *gNoiseChip = NULL; // chip-RAM LFSR noise for channel 3
|
|||
// ~28.6 kHz bright hiss, pitch 31 = ~2.0 kHz sample-and-hold rumble.
|
||||
// Every entry is inside Paula's legal 124..65535 range by
|
||||
// construction. Derivation: round(124 * (1773/124)^(i/31)).
|
||||
// Paula periods for noise pitch 0..31, chosen so the sample plays at the
|
||||
// SAME rate the other ports give that pitch: rate = 26000 - pitch * 750,
|
||||
// period = 3546895 / rate (PAL colour clock). That is the IIgs curve
|
||||
// (NOISE_RATE_MAX / NOISE_RATE_STEP in src/iigs/audio_full.c), and it
|
||||
// also lands within about a tenth of the ST's YM2149 at the top of the
|
||||
// range, so all three agree on what a given pitch sounds like.
|
||||
//
|
||||
// The old table was geometric, an even ratio per step from 124 to 1773.
|
||||
// It matched at pitch 0 and drifted ever darker above that: at the
|
||||
// pitches Space Taxi's level-intro tune actually uses -- 28, 29 and 30,
|
||||
// its only noise notes -- it ran 2587/2376/2180 Hz against the IIgs's
|
||||
// 5000/4250/3500 and the ST's 4464/4310/4167. An octave too dark, which
|
||||
// is why that one screen's music sounded wrong on the Amiga while every
|
||||
// other song, none of which touches the noise voice, sounded right.
|
||||
// Smallest period here is 136, comfortably above Paula's 124 floor.
|
||||
static const UWORD kNoisePeriod[NOISE_PITCH_MAX + 1] = {
|
||||
124, 135, 147, 160, 175, 190, 208, 226,
|
||||
246, 268, 292, 319, 347, 378, 412, 449,
|
||||
489, 533, 581, 633, 690, 752, 819, 892,
|
||||
972, 1060, 1154, 1258, 1371, 1493, 1627, 1773
|
||||
136, 140, 145, 149, 154, 159, 165, 171,
|
||||
177, 184, 192, 200, 209, 218, 229, 240,
|
||||
253, 268, 284, 302, 322, 346, 373, 405,
|
||||
443, 489, 546, 617, 709, 835, 1013, 1290
|
||||
};
|
||||
|
||||
// ----- Internal helpers -----
|
||||
|
|
|
|||
|
|
@ -1,270 +0,0 @@
|
|||
| Amiga planar circle outline V4 -- 16-way color-specialized.
|
||||
|
|
||||
| Per Bresenham iter:
|
||||
| 1. Precompute 4 xp records (xp_byte_w + bitMask_b + notMask_b) for
|
||||
| cx +/- bx and cx +/- by, stored at sp+0..15 (4 records x 4 bytes).
|
||||
| 2. Precompute 4 yp40 words for cy +/- by and cy +/- bx, stored at
|
||||
| sp+16..23 (4 words x 2 bytes).
|
||||
| 3. Plot 8 octant pixels with hardcoded color: each pixel does 4
|
||||
| branchless plane RMW ops (or.b for set bits, and.b for clear
|
||||
| bits) -- no btst, no per-plane branch.
|
||||
| 4. Bresenham step.
|
||||
|
|
||||
| At function entry the color is masked to 4 bits and used as the index
|
||||
| into a 16-entry jump table that selects the matching main loop.
|
||||
| Each main loop has the color hardcoded into the per-plane RMW ops.
|
||||
|
|
||||
| The branchless plot saves ~20-28 cyc per plane vs V3's btst+branch
|
||||
| pattern -- ~640-900 cyc per Bresenham iter.
|
||||
|
|
||||
| ABI: cdecl. d2-d7/a2-a6 callee-save.
|
||||
|
|
||||
| void surface68kAmigaCircleOutline(uint8_t *p0, uint8_t *p1,
|
||||
| uint8_t *p2, uint8_t *p3,
|
||||
| uint16_t cx, uint16_t cy,
|
||||
| uint16_t r, uint8_t color);
|
||||
|
|
||||
| Register allocation across the iter loop:
|
||||
| d2.w = bx (Bresenham)
|
||||
| d3.w = by (Bresenham)
|
||||
| d4.w = err (Bresenham)
|
||||
| d5.w = cx (cached)
|
||||
| a4 = cy (cached, sign-extended)
|
||||
| a0..a3 = plane bases
|
||||
| a5 = bitMaskLut
|
||||
| d0,d1,d6,d7 = scratch in precompute / plot
|
||||
|
|
||||
| Scratch block (24 bytes) at sp+0..23:
|
||||
| sp+0..3: xp1 record [xp_byte_w, bitMask_b, notMask_b] for cx+bx
|
||||
| sp+4..7: xp2 record for cx-bx
|
||||
| sp+8..11: xp3 record for cx+by
|
||||
| sp+12..15: xp4 record for cx-by
|
||||
| sp+16..17: yp1 word (cy+by) * 40
|
||||
| sp+18..19: yp2 word (cy-by) * 40
|
||||
| sp+20..21: yp3 word (cy+bx) * 40
|
||||
| sp+22..23: yp4 word (cy-bx) * 40
|
||||
|
||||
.text
|
||||
|
||||
|
||||
| ---- XP_REC: build xp record at sp+slot for xp = cx <signOp> <xreg> ----
|
||||
| signOp: add or sub
|
||||
| xreg: %d2 (bx) or %d3 (by)
|
||||
| slot: 0, 4, 8, or 12
|
||||
| Trashes: d0, d1, d6, d7
|
||||
|
||||
.macro XP_REC slot, signOp, xreg
|
||||
move.w %d5,%d6
|
||||
\signOp\().w \xreg,%d6 | d6 = xp
|
||||
move.w %d6,%d7
|
||||
lsr.w #3,%d7 | d7 = xp >> 3 (xp_byte)
|
||||
and.w #7,%d6 | d6 = xp & 7
|
||||
move.b (%a5,%d6.w),%d6 | d6 = bitMask
|
||||
move.b %d6,%d1
|
||||
not.b %d1 | d1 = notMask
|
||||
move.w %d7,\slot(%sp) | xp_byte word
|
||||
move.b %d6,\slot+2(%sp) | bitMask byte
|
||||
move.b %d1,\slot+3(%sp) | notMask byte
|
||||
.endm
|
||||
|
||||
|
||||
| ---- YP_REC: build yp40 word at sp+slot for yp = cy <signOp> <yreg> ----
|
||||
|
||||
.macro YP_REC slot, signOp, yreg
|
||||
move.l %a4,%d6
|
||||
\signOp\().w \yreg,%d6 | d6.w = yp
|
||||
move.w %d6,%d0
|
||||
lsl.w #3,%d6 | d6 = yp << 3
|
||||
lsl.w #5,%d0 | d0 = yp << 5
|
||||
add.w %d6,%d0 | d0 = yp * 40
|
||||
move.w %d0,\slot(%sp)
|
||||
.endm
|
||||
|
||||
|
||||
| ---- PLOT_FIXED: plot one pixel with hardcoded 4-bit color ----
|
||||
| slotYp: 16, 18, 20, or 22 (yp40 word slot)
|
||||
| slotXp: 0, 4, 8, or 12 (xp record slot)
|
||||
| color: literal 0..15
|
||||
| Trashes: d0, d1, d7
|
||||
|
||||
.macro PLOT_FIXED slotYp, slotXp, color
|
||||
move.w \slotYp(%sp),%d0 | d0 = yp40
|
||||
add.w \slotXp(%sp),%d0 | d0 += xp_byte
|
||||
move.b \slotXp+2(%sp),%d1 | d1.b = bitMask
|
||||
move.b \slotXp+3(%sp),%d7 | d7.b = notMask
|
||||
.if ((\color) & 1)
|
||||
or.b %d1,(%a0,%d0.w)
|
||||
.else
|
||||
and.b %d7,(%a0,%d0.w)
|
||||
.endif
|
||||
.if ((\color) & 2)
|
||||
or.b %d1,(%a1,%d0.w)
|
||||
.else
|
||||
and.b %d7,(%a1,%d0.w)
|
||||
.endif
|
||||
.if ((\color) & 4)
|
||||
or.b %d1,(%a2,%d0.w)
|
||||
.else
|
||||
and.b %d7,(%a2,%d0.w)
|
||||
.endif
|
||||
.if ((\color) & 8)
|
||||
or.b %d1,(%a3,%d0.w)
|
||||
.else
|
||||
and.b %d7,(%a3,%d0.w)
|
||||
.endif
|
||||
.endm
|
||||
|
||||
|
||||
| ---- PLOT_8: plot all 8 octant pixels for a given hardcoded color ----
|
||||
|
||||
.macro PLOT_8 color
|
||||
PLOT_FIXED 16, 0, \color | (cx+bx, cy+by)
|
||||
PLOT_FIXED 16, 4, \color | (cx-bx, cy+by)
|
||||
PLOT_FIXED 18, 0, \color | (cx+bx, cy-by)
|
||||
PLOT_FIXED 18, 4, \color | (cx-bx, cy-by)
|
||||
PLOT_FIXED 20, 8, \color | (cx+by, cy+bx)
|
||||
PLOT_FIXED 20, 12, \color | (cx-by, cy+bx)
|
||||
PLOT_FIXED 22, 8, \color | (cx+by, cy-bx)
|
||||
PLOT_FIXED 22, 12, \color | (cx-by, cy-bx)
|
||||
.endm
|
||||
|
||||
|
||||
| ---- CO_BODY: full Bresenham loop body for a hardcoded color ----
|
||||
| Generates the per-iter precompute, branchless plot, and Bresenham
|
||||
| step. Uses unique labels via \color suffix.
|
||||
|
||||
.macro CO_BODY color
|
||||
XP_REC 0, add, %d2 | xp1 = cx+bx
|
||||
XP_REC 4, sub, %d2 | xp2 = cx-bx
|
||||
XP_REC 8, add, %d3 | xp3 = cx+by
|
||||
XP_REC 12, sub, %d3 | xp4 = cx-by
|
||||
YP_REC 16, add, %d3 | yp1 = cy+by
|
||||
YP_REC 18, sub, %d3 | yp2 = cy-by
|
||||
YP_REC 20, add, %d2 | yp3 = cy+bx
|
||||
YP_REC 22, sub, %d2 | yp4 = cy-bx
|
||||
|
||||
PLOT_8 \color
|
||||
|
||||
addq.w #1,%d3
|
||||
tst.w %d4
|
||||
bgt .LcoDecX_\color
|
||||
add.w %d3,%d4
|
||||
add.w %d3,%d4
|
||||
addq.w #1,%d4
|
||||
bra.w .LcoLoop_\color
|
||||
.LcoDecX_\color:
|
||||
subq.w #1,%d2
|
||||
add.w %d3,%d4
|
||||
add.w %d3,%d4
|
||||
sub.w %d2,%d4
|
||||
sub.w %d2,%d4
|
||||
addq.w #1,%d4
|
||||
bra.w .LcoLoop_\color
|
||||
.endm
|
||||
|
||||
|
||||
| ---- CO_LOOP_HDR: emit a labelled loop header for a color ----
|
||||
|
||||
.macro CO_LOOP_HDR color
|
||||
.LcoLoop_\color:
|
||||
cmp.w %d3,%d2
|
||||
bcs.w .LcoDone
|
||||
CO_BODY \color
|
||||
.endm
|
||||
|
||||
|
||||
| ---- Function entry ----
|
||||
|
||||
.equ SP_SAVED, 44
|
||||
.equ SP_LOCAL, 24
|
||||
|
||||
.equ SP_OFF, (SP_SAVED + 4 + SP_LOCAL)
|
||||
|
||||
.equ SP_P0, SP_OFF + 0
|
||||
.equ SP_P1, SP_OFF + 4
|
||||
.equ SP_P2, SP_OFF + 8
|
||||
.equ SP_P3, SP_OFF + 12
|
||||
.equ SP_CX, SP_OFF + 16 + 2
|
||||
.equ SP_CY, SP_OFF + 20 + 2
|
||||
.equ SP_R, SP_OFF + 24 + 2
|
||||
.equ SP_COLOR, SP_OFF + 28 + 3
|
||||
|
||||
.globl _surface68kAmigaCircleOutline
|
||||
|
||||
_surface68kAmigaCircleOutline:
|
||||
movem.l %d2-%d7/%a2-%a6,-(%sp)
|
||||
lea -SP_LOCAL(%sp),%sp
|
||||
|
||||
| Plane bases.
|
||||
move.l SP_P0(%sp),%a0
|
||||
move.l SP_P1(%sp),%a1
|
||||
move.l SP_P2(%sp),%a2
|
||||
move.l SP_P3(%sp),%a3
|
||||
lea bitMaskLut(%pc),%a5
|
||||
|
||||
| Cache cx in d5, cy (sign-extended) in a4.
|
||||
move.w SP_CX(%sp),%d5
|
||||
move.w SP_CY(%sp),%d6
|
||||
ext.l %d6
|
||||
movea.l %d6,%a4
|
||||
|
||||
| Bresenham init.
|
||||
move.w SP_R(%sp),%d2 | bx = r
|
||||
moveq #0,%d3 | by = 0
|
||||
moveq #1,%d4
|
||||
sub.w %d2,%d4 | err = 1 - bx
|
||||
|
||||
| Dispatch on color (low 4 bits) -> one of 16 main loops.
|
||||
| Each table entry is a bra.w (4 bytes), so index *= 4.
|
||||
moveq #0,%d6
|
||||
move.b SP_COLOR(%sp),%d6
|
||||
and.w #0x0F,%d6
|
||||
add.w %d6,%d6
|
||||
add.w %d6,%d6
|
||||
lea .LcoTable(%pc),%a6
|
||||
jmp 0(%a6,%d6.w)
|
||||
|
||||
.LcoTable:
|
||||
bra.w .LcoLoop_0
|
||||
bra.w .LcoLoop_1
|
||||
bra.w .LcoLoop_2
|
||||
bra.w .LcoLoop_3
|
||||
bra.w .LcoLoop_4
|
||||
bra.w .LcoLoop_5
|
||||
bra.w .LcoLoop_6
|
||||
bra.w .LcoLoop_7
|
||||
bra.w .LcoLoop_8
|
||||
bra.w .LcoLoop_9
|
||||
bra.w .LcoLoop_10
|
||||
bra.w .LcoLoop_11
|
||||
bra.w .LcoLoop_12
|
||||
bra.w .LcoLoop_13
|
||||
bra.w .LcoLoop_14
|
||||
bra.w .LcoLoop_15
|
||||
|
||||
CO_LOOP_HDR 0
|
||||
CO_LOOP_HDR 1
|
||||
CO_LOOP_HDR 2
|
||||
CO_LOOP_HDR 3
|
||||
CO_LOOP_HDR 4
|
||||
CO_LOOP_HDR 5
|
||||
CO_LOOP_HDR 6
|
||||
CO_LOOP_HDR 7
|
||||
CO_LOOP_HDR 8
|
||||
CO_LOOP_HDR 9
|
||||
CO_LOOP_HDR 10
|
||||
CO_LOOP_HDR 11
|
||||
CO_LOOP_HDR 12
|
||||
CO_LOOP_HDR 13
|
||||
CO_LOOP_HDR 14
|
||||
CO_LOOP_HDR 15
|
||||
|
||||
.LcoDone:
|
||||
lea SP_LOCAL(%sp),%sp
|
||||
movem.l (%sp)+,%d2-%d7/%a2-%a6
|
||||
rts
|
||||
|
||||
|
||||
.align 2
|
||||
bitMaskLut:
|
||||
.byte 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01
|
||||
228
src/amiga/hal.c
228
src/amiga/hal.c
|
|
@ -633,6 +633,15 @@ bool jlpInit(const jlConfigT *config) {
|
|||
|
||||
(void)config;
|
||||
|
||||
// What the machine actually has, before anything is taken. Worth a
|
||||
// line: on a bare 512 KB A500 everything competes for CHIP, so a
|
||||
// later AllocMem failure (a sprite bank, Space Taxi's ~120 KB of
|
||||
// speech) reads as a mystery unless the starting budget is on
|
||||
// record. FAST is 0 on an unexpanded machine.
|
||||
jlLogF("amiga: free at init CHIP=%lu FAST=%lu",
|
||||
(unsigned long)AvailMem((ULONG)MEMF_CHIP),
|
||||
(unsigned long)AvailMem((ULONG)MEMF_FAST));
|
||||
|
||||
// Allocate our own BitMap with explicitly-non-interleaved planes,
|
||||
// then hand it to OpenScreen via SA_BitMap. Why not let Intuition
|
||||
// build one for us:
|
||||
|
|
@ -1146,15 +1155,18 @@ void jlpShutdown(void) {
|
|||
}
|
||||
|
||||
|
||||
// Shared 68k fast paths from src/shared68k/surface68k.s. Only the
|
||||
// planar span fill and the planar circle outline apply on Amiga (pure
|
||||
// planar after Phase 9); the chunky-layout primitives that file also
|
||||
// exports are unused here.
|
||||
extern void surface68kFillSpan4Planes(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3,
|
||||
uint16_t numMid, uint8_t leftMask, uint8_t rightMask,
|
||||
uint8_t fb0, uint8_t fb1, uint8_t fb2, uint8_t fb3);
|
||||
extern void surface68kAmigaCircleOutline(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3,
|
||||
uint16_t cx, uint16_t cy, uint16_t r, uint8_t color);
|
||||
// Hand-written 68k fast paths, all shared with the X68000: both ports keep
|
||||
// four separate plane buffers of 40 bytes per row, so a span of pixels is the
|
||||
// same bytes in the same places. src/m68k/planarFill.s does the area fills,
|
||||
// src/m68k/planarPlot.s the pixel walkers.
|
||||
extern void surface68kFillCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color);
|
||||
// src/m68k/planarPlot.s, shared with the X68000: Bresenham walkers with 16-way
|
||||
// colour dispatch, so every plane write is one memory read-modify-write with
|
||||
// the set-or-clear choice already made. Both are bit-identical to draw.c's own
|
||||
// walkers. Straight lines never reach jlpDrawLine -- draw.c diverts those to
|
||||
// jlFillRect -- so the line body walks the general diagonal case only.
|
||||
extern void surface68kPlanarCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color);
|
||||
extern void surface68kPlanarLine(uint8_t *const *planes, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color);
|
||||
|
||||
|
||||
// Amiga is pure planar (s->pixels is NULL). Each jlp<Op> below does the planar
|
||||
|
|
@ -1697,32 +1709,6 @@ void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint
|
|||
// the four shadow planes. Caller (the per-primitive walker below) has
|
||||
// already validated (x, y) is on-surface. byteOff is computed once
|
||||
// and reused across all four planes since stride is the same in each.
|
||||
/* Per-pixel plane RMW with the color-bit classification hoisted by
|
||||
* the caller. set0..set3 are 0xFF if that plane's color bit is 1
|
||||
* (OR-in bitMask), 0 if 0 (AND-out bitMask). Each plane updates with
|
||||
* `*pn = (*pn & ~bitMask) | (setN & bitMask)` -- branch-free, 4
|
||||
* RMWs per pixel. Inlined into shape walkers so the outer loop pays
|
||||
* no function-call overhead. */
|
||||
/* Bit-mask LUT for the per-pixel macro -- replaces a runtime
|
||||
* `0x80u >> (x & 7)` (8 cyc per bit shifted on 68000) with a single
|
||||
* byte load. Saves ~25 cyc per pixel inside circle / line walkers. */
|
||||
static const uint8_t kAmigaPlanePutPixelBitLut[8] = {
|
||||
0x80u, 0x40u, 0x20u, 0x10u, 0x08u, 0x04u, 0x02u, 0x01u
|
||||
};
|
||||
|
||||
#define AMIGA_PLANE_PUT_PIXEL(pd_, x_, y_, set0_, set1_, set2_, set3_) do { \
|
||||
uint16_t byteOff_ = (uint16_t)((uint16_t)(y_) * AMIGA_BYTES_PER_ROW + ((uint16_t)(x_) >> 3)); \
|
||||
uint8_t bitMask_ = kAmigaPlanePutPixelBitLut[(uint16_t)(x_) & 7u]; \
|
||||
uint8_t notMask_ = (uint8_t)~bitMask_; \
|
||||
uint8_t *p0_ = &(pd_)->planes[0][byteOff_]; \
|
||||
uint8_t *p1_ = &(pd_)->planes[1][byteOff_]; \
|
||||
uint8_t *p2_ = &(pd_)->planes[2][byteOff_]; \
|
||||
uint8_t *p3_ = &(pd_)->planes[3][byteOff_]; \
|
||||
*p0_ = (uint8_t)((*p0_ & notMask_) | ((set0_) & bitMask_)); \
|
||||
*p1_ = (uint8_t)((*p1_ & notMask_) | ((set1_) & bitMask_)); \
|
||||
*p2_ = (uint8_t)((*p2_ & notMask_) | ((set2_) & bitMask_)); \
|
||||
*p3_ = (uint8_t)((*p3_ & notMask_) | ((set3_) & bitMask_)); \
|
||||
} while (0)
|
||||
|
||||
|
||||
static void amigaPlanarSetPixel(AmigaPlanarT *pd, int16_t x, int16_t y, uint8_t color) {
|
||||
|
|
@ -1766,56 +1752,13 @@ void jlpDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
|
|||
}
|
||||
|
||||
|
||||
// Bresenham's diagonal line, planar-only walk. Same algorithm as
|
||||
// cross-platform jlDrawLine's fallback so the pixel set matches it
|
||||
// bit-for-bit.
|
||||
static void amigaPlanarLine(AmigaPlanarT *pd, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color) {
|
||||
int16_t dx;
|
||||
int16_t dy;
|
||||
int16_t sx;
|
||||
int16_t sy;
|
||||
int16_t err;
|
||||
int16_t e2;
|
||||
uint8_t set0;
|
||||
uint8_t set1;
|
||||
uint8_t set2;
|
||||
uint8_t set3;
|
||||
|
||||
set0 = (color & 0x01u) ? 0xFFu : 0x00u;
|
||||
set1 = (color & 0x02u) ? 0xFFu : 0x00u;
|
||||
set2 = (color & 0x04u) ? 0xFFu : 0x00u;
|
||||
set3 = (color & 0x08u) ? 0xFFu : 0x00u;
|
||||
|
||||
dx = (int16_t)((x1 > x0) ? (x1 - x0) : (x0 - x1));
|
||||
dy = (int16_t)(-((y1 > y0) ? (y1 - y0) : (y0 - y1)));
|
||||
sx = (int16_t)((x0 < x1) ? 1 : -1);
|
||||
sy = (int16_t)((y0 < y1) ? 1 : -1);
|
||||
err = (int16_t)(dx + dy);
|
||||
while (1) {
|
||||
AMIGA_PLANE_PUT_PIXEL(pd, x0, y0, set0, set1, set2, set3);
|
||||
if (x0 == x1 && y0 == y1) {
|
||||
break;
|
||||
}
|
||||
e2 = (int16_t)(2 * err);
|
||||
if (e2 >= dy) {
|
||||
err = (int16_t)(err + dy);
|
||||
x0 = (int16_t)(x0 + sx);
|
||||
}
|
||||
if (e2 <= dx) {
|
||||
err = (int16_t)(err + dx);
|
||||
y0 = (int16_t)(y0 + sy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void jlpDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
|
||||
AmigaPlanarT *pd;
|
||||
if (s->portData == NULL) {
|
||||
return;
|
||||
}
|
||||
pd = (AmigaPlanarT *)s->portData;
|
||||
amigaPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
|
||||
surface68kPlanarLine(pd->planes, x0, y0, x1, y1, (uint16_t)(colorIndex & 0x0Fu));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1825,112 +1768,32 @@ void jlpDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t co
|
|||
return;
|
||||
}
|
||||
pd = (AmigaPlanarT *)s->portData;
|
||||
surface68kAmigaCircleOutline(pd->planes[0], pd->planes[1],
|
||||
pd->planes[2], pd->planes[3],
|
||||
(uint16_t)cx, (uint16_t)cy, r,
|
||||
(uint8_t)(colorIndex & 0x0Fu));
|
||||
}
|
||||
|
||||
|
||||
/* Single-row 4-plane span fill via shared68k asm. Caller pre-computes
|
||||
* the left/right partial-byte masks; fillByte per plane is just
|
||||
* 0xFF/0x00 based on colorIndex bit. The asm body avoids per-byte
|
||||
* function-call dispatch and the C compiler's per-iter overhead --
|
||||
* critical for jlFillCircle r=40 which pre-asm was paying ~50 ms/call
|
||||
* for 80 spans. */
|
||||
static inline __attribute__((always_inline))
|
||||
void amigaFillSpanInline(AmigaPlanarT *pd, int16_t spanX, int16_t spanY,
|
||||
uint16_t spanW, uint8_t colorIndex) {
|
||||
static const uint8_t kLM[8] = {0xFFu,0x7Fu,0x3Fu,0x1Fu,0x0Fu,0x07u,0x03u,0x01u};
|
||||
static const uint8_t kRM[8] = {0x80u,0xC0u,0xE0u,0xF0u,0xF8u,0xFCu,0xFEu,0xFFu};
|
||||
|
||||
uint16_t byteFirst = (uint16_t)((uint16_t)spanX >> 3);
|
||||
uint16_t lastBit = (uint16_t)(spanX + spanW - 1);
|
||||
uint16_t byteLast = (uint16_t)(lastBit >> 3);
|
||||
uint8_t leftMask = kLM[(uint16_t)spanX & 7u];
|
||||
uint8_t rightMask = kRM[lastBit & 7u];
|
||||
uint16_t rowOff = (uint16_t)((uint16_t)spanY * AMIGA_BYTES_PER_ROW + byteFirst);
|
||||
uint8_t fb0 = ((colorIndex >> 0) & 1u) ? 0xFFu : 0x00u;
|
||||
uint8_t fb1 = ((colorIndex >> 1) & 1u) ? 0xFFu : 0x00u;
|
||||
uint8_t fb2 = ((colorIndex >> 2) & 1u) ? 0xFFu : 0x00u;
|
||||
uint8_t fb3 = ((colorIndex >> 3) & 1u) ? 0xFFu : 0x00u;
|
||||
uint8_t *p0 = pd->planes[0] + rowOff;
|
||||
uint8_t *p1 = pd->planes[1] + rowOff;
|
||||
uint8_t *p2 = pd->planes[2] + rowOff;
|
||||
uint8_t *p3 = pd->planes[3] + rowOff;
|
||||
|
||||
if (byteFirst == byteLast) {
|
||||
/* Single-byte case kept in C: the asm path post-increments
|
||||
* the pointer between leading and trailing RMW, which would
|
||||
* read the wrong byte if both edges land on the same byte.
|
||||
* One-byte spans are rare anyway (~1 of 80 in jlFillCircle r=40)
|
||||
* so the C overhead is fine here. */
|
||||
uint8_t mask = (uint8_t)(leftMask & rightMask);
|
||||
uint8_t notMask = (uint8_t)~mask;
|
||||
uint8_t *pp[AMIGA_BITPLANES];
|
||||
uint8_t fb[AMIGA_BITPLANES];
|
||||
uint8_t i;
|
||||
pp[0] = p0; pp[1] = p1; pp[2] = p2; pp[3] = p3;
|
||||
fb[0] = fb0; fb[1] = fb1; fb[2] = fb2; fb[3] = fb3;
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
*pp[i] = (uint8_t)((*pp[i] & notMask) | (fb[i] & mask));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
uint16_t numMid = (uint16_t)(byteLast - byteFirst - 1u);
|
||||
surface68kFillSpan4Planes(p0, p1, p2, p3, numMid, leftMask, rightMask,
|
||||
fb0, fb1, fb2, fb3);
|
||||
}
|
||||
surface68kPlanarCircle(pd->planes, (uint16_t)cx, (uint16_t)cy, r, (uint16_t)(colorIndex & 0x0Fu));
|
||||
}
|
||||
|
||||
|
||||
// src/m68k/planarFill.s does the whole exact-disk walk: for each y in [0, r]
|
||||
// the largest x with x*x + y*y <= r*r, then one span at cy+y and one at cy-y,
|
||||
// each row filled exactly once. Row coverage is identical to draw.c's own
|
||||
// jlFillCircle, so the golden hashes hold by construction.
|
||||
//
|
||||
// This replaced a C walk that called an assembly span filler per span. The
|
||||
// call and the per-span geometry cost more than the pixels did: the masks are
|
||||
// now computed once for a row's two spans and the walk is 16-bit throughout.
|
||||
//
|
||||
// The asm keeps r*r in a word, so it needs r <= 181. draw.c only calls this
|
||||
// when the bounding box is on-surface and every planar surface here is
|
||||
// SURFACE_HEIGHT rows tall, which caps r at half of that; the check below
|
||||
// fails the build rather than let a taller surface wrap silently.
|
||||
bool jlpFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
|
||||
AmigaPlanarT *pd;
|
||||
uint32_t r2;
|
||||
uint32_t xx;
|
||||
uint32_t yy;
|
||||
int32_t x;
|
||||
uint16_t y;
|
||||
enum { kMaxWordRadius = 181 };
|
||||
|
||||
_Static_assert((SURFACE_HEIGHT / 2) <= kMaxWordRadius, "r*r must fit a word in src/m68k/planarFill.s");
|
||||
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
pd = (AmigaPlanarT *)s->portData;
|
||||
// Exact-disk span walk (perf-hunt-2 rank 14, the recorded "true
|
||||
// lever"; algorithm copied from the ST stPlanarCircleFill): for
|
||||
// each y offset track the largest x with x^2 + y^2 <= r^2
|
||||
// incrementally, and fill each row exactly ONCE. The old midpoint
|
||||
// loop emitted 4 octant spans per iteration whose horizontal spans
|
||||
// overlapped (~14-16% redundant bytes at r=40) and re-dispatched
|
||||
// amigaFillSpanInline for each. Row coverage is identical to
|
||||
// draw.c's jlFillCircle and the ST walker, so the golden hashes
|
||||
// are unchanged by construction. Spans stay unclipped -- draw.c
|
||||
// routes every off-surface circle through its own clipped loop
|
||||
// before jlpFillCircle is called (same contract as before).
|
||||
r2 = (uint32_t)r * (uint32_t)r;
|
||||
xx = r2;
|
||||
yy = 0u;
|
||||
x = (int32_t)r;
|
||||
for (y = 0u; y <= r; y++) {
|
||||
uint32_t xxLimit;
|
||||
int16_t spanX;
|
||||
uint16_t spanW;
|
||||
|
||||
xxLimit = r2 - yy;
|
||||
while (xx > xxLimit) {
|
||||
xx = xx - ((uint32_t)x + (uint32_t)x - 1u);
|
||||
x--;
|
||||
}
|
||||
spanX = (int16_t)(cx - (int16_t)x);
|
||||
spanW = (uint16_t)((int16_t)x + (int16_t)x + 1);
|
||||
amigaFillSpanInline(pd, spanX, (int16_t)(cy + (int16_t)y), spanW, colorIndex);
|
||||
if (y > 0u) {
|
||||
amigaFillSpanInline(pd, spanX, (int16_t)(cy - (int16_t)y), spanW, colorIndex);
|
||||
}
|
||||
yy = yy + ((uint32_t)y + (uint32_t)y + 1u);
|
||||
}
|
||||
surface68kFillCircle(((AmigaPlanarT *)s->portData)->planes, (uint16_t)cx, (uint16_t)cy, r, colorIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2280,7 +2143,12 @@ void *jlpSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
|
|||
* must be MEMF_CHIP unconditionally now -- a fast-RAM fallback
|
||||
* would put the blitter helpers out of reach and corrupt output. */
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
pd->planes[i] = (uint8_t *)AllocMem((ULONG)AMIGA_PLANE_SIZE,
|
||||
// AMIGA_PLANE_ALLOC, not AMIGA_PLANE_SIZE: one byte of slack past the
|
||||
// end. A span in src/m68k/planarFill.s whose two ends land on the same
|
||||
// byte writes the FOLLOWING byte back unchanged rather than branching
|
||||
// to its own code path, which on the last row would otherwise be one
|
||||
// byte past the buffer.
|
||||
pd->planes[i] = (uint8_t *)AllocMem((ULONG)AMIGA_PLANE_ALLOC,
|
||||
(ULONG)(MEMF_CHIP | MEMF_CLEAR));
|
||||
jlLogF("amiga: shadow plane %u in CHIP (blitter, addr=$%08lX)",
|
||||
(unsigned)i, (unsigned long)pd->planes[i]);
|
||||
|
|
@ -2288,7 +2156,7 @@ void *jlpSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
|
|||
/* Roll back any planes already allocated. */
|
||||
while (i > 0u) {
|
||||
i--;
|
||||
FreeMem(pd->planes[i], (ULONG)AMIGA_PLANE_SIZE);
|
||||
FreeMem(pd->planes[i], (ULONG)AMIGA_PLANE_ALLOC);
|
||||
}
|
||||
FreeMem(pd, (ULONG)sizeof(AmigaPlanarT));
|
||||
return NULL;
|
||||
|
|
@ -2312,7 +2180,7 @@ void jlpSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) {
|
|||
if (pd->ownsPlanes) {
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
if (pd->planes[i] != NULL) {
|
||||
FreeMem(pd->planes[i], (ULONG)AMIGA_PLANE_SIZE);
|
||||
FreeMem(pd->planes[i], (ULONG)AMIGA_PLANE_ALLOC);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,6 +188,8 @@ static inline __attribute__((always_inline)) uint16_t stFloodStopBits(const uint
|
|||
static uint16_t quantizeColorToSt(uint16_t orgb);
|
||||
static void flattenScbPalettes(const jlSurfaceT *src);
|
||||
static void setShifterVideoBase(const uint8_t *page);
|
||||
extern void stLeaveSuper(void *savedSsp);
|
||||
|
||||
static void writeDiagnostics(void);
|
||||
static long writePrevPaletteRegs(void);
|
||||
|
||||
|
|
@ -887,7 +889,12 @@ void jlpShutdown(void) {
|
|||
// Drop back to user mode so control returns to GEMDOS the way it
|
||||
// launched us (mirrors the Super(0L) in jlpInit).
|
||||
if (gSuperEntered) {
|
||||
(void)Super(gSavedSsp);
|
||||
// NOT Super(gSavedSsp) from C: the stack pointer the trap hands
|
||||
// back is not the one it was given, so the compiler's epilogue
|
||||
// reads the saved registers and the return address from the
|
||||
// wrong slot and rts lands in nothing. superExit.s parks the
|
||||
// entry SP in a GEMDOS-preserved register and puts it back.
|
||||
stLeaveSuper(gSavedSsp);
|
||||
gSuperEntered = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
src/atarist/superExit.s
Normal file
43
src/atarist/superExit.s
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
| Atari ST -- leave supervisor mode without losing the caller's frame.
|
||||
|
|
||||
| jlpInit enters supervisor for the program's lifetime (the shifter and
|
||||
| MFP registers are supervisor-only) and jlpShutdown has to hand user
|
||||
| mode back before returning to GEMDOS. The obvious
|
||||
|
|
||||
| (void)Super(gSavedSsp);
|
||||
|
|
||||
| in C does leave supervisor, but the stack pointer the trap hands back
|
||||
| is NOT the one it was called with, so the compiler's epilogue then
|
||||
| pops the saved registers and the return address from the wrong place
|
||||
| and rts jumps into nothing. It is a heisenbug: whether the wrong slot
|
||||
| happens to hold something survivable depends on what the last few
|
||||
| calls left on the stack, so the same binary can exit cleanly today and
|
||||
| bus-error tomorrow, and an added printf can "fix" it. EmuTOS reports
|
||||
| it as a Panic in the caller of Super, one instruction past the rts.
|
||||
|
|
||||
| The cure is to note the stack position before the trap and to restore
|
||||
| it afterwards. In user mode interrupts run on the supervisor stack, so
|
||||
| the bytes just below the restored SP stay ours to read.
|
||||
|
|
||||
| GAS-syntax (binutils m68k); assembled by m68k-atari-mint-as via the
|
||||
| gcc driver. GEMDOS preserves d3-d7/a3-a6 across trap #1, which is why
|
||||
| the entry SP is parked in a3.
|
||||
|
||||
.text
|
||||
.globl _stLeaveSuper
|
||||
|
||||
| void stLeaveSuper(void *savedSsp);
|
||||
| Must be called in supervisor mode with savedSsp as returned by
|
||||
| Super(0L). Returns in user mode.
|
||||
_stLeaveSuper:
|
||||
movel %d3,%sp@- | 8 bytes of our own
|
||||
movel %a3,%sp@-
|
||||
movel %sp@(12),%d3 | savedSsp
|
||||
lea %sp@(8),%a3 | entry SP (the rts slot)
|
||||
movel %d3,%sp@- | Super(savedSsp)
|
||||
movew #0x20,%sp@-
|
||||
trap #1
|
||||
movel %a3,%sp | undo whatever SP came back
|
||||
movel %sp@(-4),%d3 | our saves are still below
|
||||
movel %sp@(-8),%a3
|
||||
rts
|
||||
|
|
@ -727,6 +727,7 @@ static const uint8_t kAdlibSlotMap[ADLIB_VOICE_COUNT * 2] = {
|
|||
#define ADLIB_BD_RHYTHM 0x20u // reg 0xBD bit 5: rhythm mode enable
|
||||
#define ADLIB_BD_KEY_SD 0x08u // reg 0xBD bit 3: snare key
|
||||
#define ADLIB_NOISE_MAX_HZ 6200u // pitch 0 tops out the bright end
|
||||
#define ADLIB_NOISE_MIN_HZ 194u // pitch 31, the deepest the snare goes
|
||||
|
||||
static bool gAdlibInited = false;
|
||||
static bool gAdlibNoiseInited = false;
|
||||
|
|
@ -1042,10 +1043,25 @@ void jlpAudioNoise(uint8_t pitch, uint8_t atten) {
|
|||
}
|
||||
adlibWrite((uint8_t)(0x40u + ADLIB_SLOT_SD), tl);
|
||||
|
||||
// pitch 0..31 -> hyperbolic Hz, the YM noise-period semantic:
|
||||
// pitch 0 = 6200 Hz bright hiss, pitch 31 = ~194 Hz rumble.
|
||||
// (Core clamps pitch to <= 31 before the HAL sees it.)
|
||||
freqHz = (uint16_t)(ADLIB_NOISE_MAX_HZ / ((uint16_t)pitch + 1u));
|
||||
// pitch 0..31 spread evenly from ADLIB_NOISE_MAX_HZ down to
|
||||
// ADLIB_NOISE_MIN_HZ. (Core clamps pitch to <= 31 before the HAL
|
||||
// sees it.)
|
||||
//
|
||||
// Evenly, because the shared curve every other port follows --
|
||||
// rate = 26000 - pitch * 750, from src/iigs/audio_full.c -- is
|
||||
// linear in pitch, so a linear spread here puts a given pitch at the
|
||||
// same place in the range as it sits on those machines. The absolute
|
||||
// rates cannot be matched: the OPL2's snare is a tuned drum voice
|
||||
// rather than a sampled noise source, and its band tops out near
|
||||
// 6 kHz where the curve asks for 26 kHz.
|
||||
//
|
||||
// It replaces MAX / (pitch + 1), a hyperbola that spent nearly all
|
||||
// its resolution on the first few pitches and squashed everything
|
||||
// from 10 upward into 194..563 Hz -- pitches 28, 29 and 30 came out
|
||||
// 213, 206 and 200 Hz, near enough identical, so a tune written
|
||||
// across them lost its shape entirely.
|
||||
freqHz = (uint16_t)(ADLIB_NOISE_MAX_HZ
|
||||
- (uint16_t)(((uint32_t)pitch * (ADLIB_NOISE_MAX_HZ - ADLIB_NOISE_MIN_HZ)) / 31u));
|
||||
block = adlibFreqToFnum(freqHz, &fnum);
|
||||
|
||||
// Bit 5 (melodic key-on) must stay clear on 0xB7: in rhythm mode
|
||||
|
|
|
|||
|
|
@ -230,11 +230,20 @@ static uint16_t docFreqWord(uint16_t rateHz) {
|
|||
#define NOISE_SUSTAIN_MUL 41u // len = rate * 41 / 512 -> ~80 ms sustain
|
||||
#define NOISE_LFSR_SEED 0xACE1u
|
||||
#define NOISE_LFSR_TAPS 0xB400u // 16-bit maximal Galois LFSR, period 65535
|
||||
// How long one noise burst sounds, in 60 Hz GetTick units.
|
||||
// jlpAudioNoise plays NOISE_SUSTAIN_MUL/512 of a second of sample
|
||||
// (41/512 s = 80 ms = 4.8 ticks). 4 retriggers a shade early, which
|
||||
// keeps the sound gapless.
|
||||
#define NOISE_HOLD_TICKS 4u
|
||||
// How long a noise burst is left alone before a repeat call retriggers
|
||||
// it, in 60 Hz GetTick units. jlpAudioNoise plays NOISE_SUSTAIN_MUL/512
|
||||
// of a second of sample (41/512 s = 80 ms = 4.8 ticks), so anything
|
||||
// below that retriggers before the burst ends and the sound is gapless.
|
||||
//
|
||||
// It has to hold for less than HALF the burst, not just under all of it.
|
||||
// At 4 ticks a caller running at 60 Hz retriggered at 4 ticks and was
|
||||
// gapless, but a caller running at the ~19 fps Space Taxi manages on the
|
||||
// IIgs lands at 3.2 ticks -- inside the hold, so its call did nothing and
|
||||
// the burst then expired at 4.8 with the next call not due until 6.3.
|
||||
// That 25 ms hole per frame was the stuttering jet. 2 ticks retriggers
|
||||
// for any caller slower than 33 ms and merely costs a held music note
|
||||
// one extra trigger per 33 ms.
|
||||
#define NOISE_HOLD_TICKS 2u
|
||||
|
||||
// ----- Streaming SFX (jlpAudioPlaySfxStream) -----
|
||||
//
|
||||
|
|
|
|||
503
src/m68k/planarFill.s
Normal file
503
src/m68k/planarFill.s
Normal file
|
|
@ -0,0 +1,503 @@
|
|||
| Shared 68000 separate-plane area fills -- hand-rolled.
|
||||
|
|
||||
| Used by BOTH separate-plane 68k ports, the Amiga and the X68000: a 320px
|
||||
| 4bpp surface is four plane buffers of 40 bytes per row on either machine, so
|
||||
| a span of pixels is the same bytes in the same places. (The Atari ST is
|
||||
| word-interleaved and keeps its own St* routines.)
|
||||
|
|
||||
| Home of the span-filling primitives that write a run of pixels into all
|
||||
| four plane buffers at once. Currently:
|
||||
|
|
||||
| surface68kFillCircle jlFillCircle: the whole exact-disk walk
|
||||
| surface68kFillRect jlFillRect: a rect that does not span whole rows
|
||||
|
|
||||
| A span is a partial left byte, a run of whole middle bytes, and a partial
|
||||
| right byte, in each of four planes. Three things make this much cheaper
|
||||
| than the general case, and the PLANEFILL macro below is built around them:
|
||||
|
|
||||
| * A PLANE'S FILL VALUE IS ONLY EVER ALL-ONES OR ALL-ZEROS. It is one bit
|
||||
| of the colour smeared across a byte. So a masked edge is not a read,
|
||||
| two ANDs, an OR and a store; it is `or.b mask,(a0)` for a set plane and
|
||||
| `and.b ~mask,(a0)` for a clear one -- one instruction, one bus cycle.
|
||||
| * THE COLOUR IS FIXED FOR THE WHOLE SHAPE, so which of those two it is
|
||||
| need not be decided at run time. The body is emitted once per colour
|
||||
| value and dispatched to through a jump table, exactly as
|
||||
| src/x68000/circle.s does. Set planes then run as a group with d6 = -1
|
||||
| and clear planes as a group with d6 = 0 and the masks complemented
|
||||
| once, so the middle run is the same code for both.
|
||||
| * THE MIDDLE RUN IS WHOLE BYTES of that value, so it goes out a long at
|
||||
| a time through a dbra loop once the pointer is aligned.
|
||||
|
|
||||
| ABI: cdecl (gcc-amigaos / elf2x68k), d2-d7/a2-a6 callee-save. 16-bit
|
||||
| arguments are sign-extended to 4 bytes on the call stack.
|
||||
|
|
||||
| Every entry point carries BOTH symbol spellings at the same address:
|
||||
| gcc-amigaos and gcc-atari-mint prepend an underscore to C names and
|
||||
| elf2x68k does not, and one extra unused symbol is cheaper than forking the
|
||||
| file over a naming convention.
|
||||
|
||||
.text
|
||||
.even
|
||||
.globl surface68kFillCircle
|
||||
.globl _surface68kFillCircle
|
||||
.globl surface68kFillRect
|
||||
.globl _surface68kFillRect
|
||||
|
||||
| Bytes per plane row: SURFACE_WIDTH / 8, which is AMIGA_BYTES_PER_ROW and
|
||||
| X68K_BYTES_PER_ROW both. Every planar surface on either port is
|
||||
| SURFACE_WIDTH x SURFACE_HEIGHT, so it is a constant here as well as in the
|
||||
| C, and both ports' C guards the radius bound that depends on it.
|
||||
.equ ROW_BYTES, 40
|
||||
|
||||
| Stack locals. The walk state lives here rather than in registers because it
|
||||
| is touched once per row while the span body runs twice, so the registers are
|
||||
| worth more to the span.
|
||||
.equ FC_XX, 0 | x*x tracker, word
|
||||
.equ FC_XLIMIT, 2 | r*r - y*y, word
|
||||
.equ FC_X, 4 | current x, word
|
||||
.equ FC_T, 6 | 2*y + 1, word
|
||||
.equ FC_COUNT, 8 | rows left, word
|
||||
.equ FC_DOWN, 10 | (cy + y) * 40, word
|
||||
.equ FC_UP, 12 | (cy - y) * 40, word
|
||||
.equ FC_FIRST, 14 | span's first byte in a row
|
||||
.equ FC_LOCAL, 16
|
||||
| 16 locals + 44 saved registers + 4 return address
|
||||
.equ FC_ARG, (FC_LOCAL + 44 + 4)
|
||||
.equ FC_PLANES, FC_ARG + 0
|
||||
.equ FC_CX, FC_ARG + 4 + 2
|
||||
.equ FC_CY, FC_ARG + 8 + 2
|
||||
.equ FC_R, FC_ARG + 12 + 2
|
||||
.equ FC_COLOR, FC_ARG + 16 + 2
|
||||
|
||||
|
||||
| ---- PLANEFILL: one span in one plane ----
|
||||
| \areg = that plane's base, \op = or (set) or and (clear).
|
||||
| d7.w = span's byte offset, d2 = left mask, d3 = right mask, d4 = middle
|
||||
| byte count, d6 = the fill value (0 or -1, matching \op).
|
||||
| Trashes d0, d1, d5, a0.
|
||||
.macro PLANEFILL areg, op
|
||||
lea (\areg,%d7.w),%a0
|
||||
\op\().b %d2,(%a0)+ | left edge
|
||||
move.w %d4,%d5
|
||||
beq.s 9f
|
||||
1: move.l %a0,%d1 | byte-align, then longs
|
||||
and.w #3,%d1
|
||||
beq.s 2f
|
||||
move.b %d6,(%a0)+
|
||||
subq.w #1,%d5
|
||||
bne.s 1b
|
||||
bra.s 9f
|
||||
2: move.w %d5,%d0
|
||||
and.w #3,%d5 | d5 = trailing bytes
|
||||
lsr.w #2,%d0 | d0 = whole longs
|
||||
beq.s 5f
|
||||
subq.w #1,%d0
|
||||
3: move.l %d6,(%a0)+
|
||||
dbra %d0,3b
|
||||
5: tst.w %d5
|
||||
beq.s 9f
|
||||
6: move.b %d6,(%a0)+
|
||||
subq.w #1,%d5
|
||||
bne.s 6b
|
||||
9: \op\().b %d3,(%a0) | right edge
|
||||
.endm
|
||||
|
||||
|
||||
| ---- SPAN: one span in all four planes, for a known colour ----
|
||||
| Set planes first with d6 = -1, then the clear planes with d6 = 0 and the
|
||||
| masks complemented, so `and.b %d2` is a single instruction there too.
|
||||
.macro SPAN colour
|
||||
.if (\colour) != 0
|
||||
moveq #-1,%d6
|
||||
.if ((\colour) & 1)
|
||||
PLANEFILL %a2, or
|
||||
.endif
|
||||
.if ((\colour) & 2)
|
||||
PLANEFILL %a3, or
|
||||
.endif
|
||||
.if ((\colour) & 4)
|
||||
PLANEFILL %a4, or
|
||||
.endif
|
||||
.if ((\colour) & 8)
|
||||
PLANEFILL %a5, or
|
||||
.endif
|
||||
.endif
|
||||
.if (\colour) != 15
|
||||
moveq #0,%d6
|
||||
not.b %d2
|
||||
not.b %d3
|
||||
.if !((\colour) & 1)
|
||||
PLANEFILL %a2, and
|
||||
.endif
|
||||
.if !((\colour) & 2)
|
||||
PLANEFILL %a3, and
|
||||
.endif
|
||||
.if !((\colour) & 4)
|
||||
PLANEFILL %a4, and
|
||||
.endif
|
||||
.if !((\colour) & 8)
|
||||
PLANEFILL %a5, and
|
||||
.endif
|
||||
not.b %d2 | the row's second span wants
|
||||
not.b %d3 | the masks back
|
||||
.endif
|
||||
.endm
|
||||
|
||||
|
||||
| ---- CIRCLEBODY: the whole exact-disk walk for one colour ----
|
||||
|
|
||||
| Bit-identical to the chunky reference loop in src/core/draw.c jlFillCircle
|
||||
| and to the C walk this replaces: for each y in [0, r] find the largest x
|
||||
| with x*x + y*y <= r*r incrementally, then fill the span (cx-x, cx+x) at
|
||||
| rows cy+y and cy-y. Each row is filled exactly once -- no octant overlap.
|
||||
|
|
||||
| The arithmetic is 16-bit throughout. r*r must fit a word, which holds for
|
||||
| r <= 181; the caller's contract (the bounding box is on-surface, and a
|
||||
| surface is 200 rows tall) caps r at 99, and halPlanar.c has a build-time
|
||||
| check on that.
|
||||
.macro CIRCLEBODY colour
|
||||
.Lrow\colour:
|
||||
| walk x down until x*x + y*y <= r*r again
|
||||
move.w FC_XX(%sp),%d0
|
||||
move.w FC_XLIMIT(%sp),%d1
|
||||
cmp.w %d1,%d0
|
||||
bls.s .Lgeom\colour
|
||||
move.w FC_X(%sp),%d4
|
||||
.Lshrink\colour:
|
||||
move.w %d4,%d5
|
||||
add.w %d5,%d5
|
||||
subq.w #1,%d5 | 2x - 1
|
||||
sub.w %d5,%d0 | xx -= 2x - 1
|
||||
subq.w #1,%d4 | x--
|
||||
cmp.w %d1,%d0
|
||||
bhi.s .Lshrink\colour
|
||||
move.w %d0,FC_XX(%sp)
|
||||
move.w %d4,FC_X(%sp)
|
||||
.Lgeom\colour:
|
||||
| span geometry, computed once for the row's two spans
|
||||
move.w FC_X(%sp),%d0
|
||||
move.w %a6,%d1
|
||||
sub.w %d0,%d1 | left = cx - x
|
||||
add.w %a6,%d0 | right = cx + x
|
||||
move.w %d1,%d4
|
||||
lsr.w #3,%d4 | first byte
|
||||
move.w %d4,FC_FIRST(%sp)
|
||||
and.w #7,%d1
|
||||
move.b (%a1,%d1.w),%d2 | left mask
|
||||
move.w %d0,%d1
|
||||
lsr.w #3,%d1 | last byte
|
||||
and.w #7,%d0
|
||||
move.b 8(%a1,%d0.w),%d3 | right mask
|
||||
sub.w %d4,%d1
|
||||
bne.s .Lmulti\colour
|
||||
| Both ends in one byte. Combining the masks covers it, and
|
||||
| zeroing the right mask makes the right edge write the NEXT
|
||||
| byte back unchanged instead of needing its own code path.
|
||||
| (The plane allocation carries a spare byte for this.)
|
||||
and.b %d3,%d2
|
||||
moveq #0,%d3
|
||||
moveq #0,%d4
|
||||
bra.s .Ldown\colour
|
||||
.Lmulti\colour:
|
||||
subq.w #1,%d1
|
||||
move.w %d1,%d4 | middle byte count
|
||||
.Ldown\colour:
|
||||
move.w FC_DOWN(%sp),%d7
|
||||
add.w FC_FIRST(%sp),%d7
|
||||
SPAN \colour
|
||||
| the mirrored row, unless y is zero and it IS this row
|
||||
move.w FC_UP(%sp),%d7
|
||||
cmp.w FC_DOWN(%sp),%d7
|
||||
beq.w .Lstep\colour
|
||||
add.w FC_FIRST(%sp),%d7
|
||||
SPAN \colour
|
||||
.Lstep\colour:
|
||||
addi.w #ROW_BYTES,FC_DOWN(%sp)
|
||||
subi.w #ROW_BYTES,FC_UP(%sp)
|
||||
| y*y grows by 2y+1, so the x*x limit shrinks by it
|
||||
move.w FC_T(%sp),%d0
|
||||
sub.w %d0,FC_XLIMIT(%sp)
|
||||
addq.w #2,%d0
|
||||
move.w %d0,FC_T(%sp)
|
||||
subq.w #1,FC_COUNT(%sp)
|
||||
bne.w .Lrow\colour
|
||||
bra.w fcDone
|
||||
.endm
|
||||
|
||||
|
||||
| void surface68kFillCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy,
|
||||
| uint16_t r, uint16_t color);
|
||||
surface68kFillCircle:
|
||||
_surface68kFillCircle:
|
||||
movem.l %d2-%d7/%a2-%a6,%sp@- | 11 registers = 44 bytes
|
||||
sub.l #FC_LOCAL,%sp
|
||||
move.l FC_PLANES(%sp),%a0
|
||||
move.l (%a0),%a2
|
||||
move.l 4(%a0),%a3
|
||||
move.l 8(%a0),%a4
|
||||
move.l 12(%a0),%a5
|
||||
move.w FC_CX(%sp),%a6
|
||||
move.w FC_CY(%sp),%d0
|
||||
move.w FC_R(%sp),%d1
|
||||
|
||||
move.w %d1,%d2
|
||||
mulu.w %d2,%d2 | r*r, a word for r <= 181
|
||||
move.w %d2,FC_XX(%sp)
|
||||
move.w %d2,FC_XLIMIT(%sp)
|
||||
move.w %d1,FC_X(%sp)
|
||||
move.w #1,FC_T(%sp)
|
||||
addq.w #1,%d1
|
||||
move.w %d1,FC_COUNT(%sp) | r + 1 rows
|
||||
| cy * 40 as (cy << 5) + (cy << 3)
|
||||
move.w %d0,%d1
|
||||
lsl.w #5,%d0
|
||||
lsl.w #3,%d1
|
||||
add.w %d1,%d0
|
||||
move.w %d0,FC_DOWN(%sp)
|
||||
move.w %d0,FC_UP(%sp)
|
||||
lea gFillMaskLut,%a1
|
||||
|
||||
move.w FC_COLOR(%sp),%d0
|
||||
and.w #15,%d0
|
||||
add.w %d0,%d0
|
||||
move.w fcJump(%pc,%d0.w),%d0
|
||||
jmp fcJump(%pc,%d0.w)
|
||||
|
||||
fcJump:
|
||||
.word fcC0-fcJump, fcC1-fcJump, fcC2-fcJump, fcC3-fcJump
|
||||
.word fcC4-fcJump, fcC5-fcJump, fcC6-fcJump, fcC7-fcJump
|
||||
.word fcC8-fcJump, fcC9-fcJump, fcC10-fcJump, fcC11-fcJump
|
||||
.word fcC12-fcJump, fcC13-fcJump, fcC14-fcJump, fcC15-fcJump
|
||||
|
||||
fcC0: CIRCLEBODY 0
|
||||
fcC1: CIRCLEBODY 1
|
||||
fcC2: CIRCLEBODY 2
|
||||
fcC3: CIRCLEBODY 3
|
||||
fcC4: CIRCLEBODY 4
|
||||
fcC5: CIRCLEBODY 5
|
||||
fcC6: CIRCLEBODY 6
|
||||
fcC7: CIRCLEBODY 7
|
||||
fcC8: CIRCLEBODY 8
|
||||
fcC9: CIRCLEBODY 9
|
||||
fcC10: CIRCLEBODY 10
|
||||
fcC11: CIRCLEBODY 11
|
||||
fcC12: CIRCLEBODY 12
|
||||
fcC13: CIRCLEBODY 13
|
||||
fcC14: CIRCLEBODY 14
|
||||
fcC15: CIRCLEBODY 15
|
||||
|
||||
fcDone:
|
||||
add.l #FC_LOCAL,%sp
|
||||
movem.l %sp@+,%d2-%d7/%a2-%a6
|
||||
rts
|
||||
|
||||
|
||||
|
||||
| ---- RECTBODY: every row of the rect, for one colour ----
|
||||
|
|
||||
| A rect is one span repeated, so the masks and the middle byte count are
|
||||
| computed once at entry and stay in d2/d3/d4 for the whole shape; the row
|
||||
| loop only walks the offset. d7 counts up by a row's worth of bytes until it
|
||||
| meets a6, which is one row past the last.
|
||||
.macro RECTBODY colour
|
||||
.Lrect\colour:
|
||||
SPAN \colour
|
||||
addi.w #ROW_BYTES,%d7
|
||||
cmp.w %a6,%d7
|
||||
bne.w .Lrect\colour
|
||||
bra.w frDone
|
||||
.endm
|
||||
|
||||
|
||||
| ---- THINPLANE / THINBODY: a rect only one byte wide ----
|
||||
|
|
||||
| Worth its own body rather than folding into SPAN. A vertical line is this
|
||||
| shape (draw.c turns one into a 1px-wide rect), and it has no middle run and
|
||||
| only one masked byte per row, so the generic span pays for a middle-run test
|
||||
| and a second edge it does not need. Here the plane loop is outside the row
|
||||
| loop, which leaves a dbra over a single read-modify-write: d2 holds the one
|
||||
| combined mask and d4 the row count less one.
|
||||
.macro THINPLANE areg, op
|
||||
move.w %d4,%d5
|
||||
lea (\areg,%d7.w),%a0
|
||||
1: \op\().b %d2,(%a0)
|
||||
lea ROW_BYTES(%a0),%a0
|
||||
dbra %d5,1b
|
||||
.endm
|
||||
|
||||
|
||||
.macro THINBODY colour
|
||||
.if (\colour) != 0
|
||||
.if ((\colour) & 1)
|
||||
THINPLANE %a2, or
|
||||
.endif
|
||||
.if ((\colour) & 2)
|
||||
THINPLANE %a3, or
|
||||
.endif
|
||||
.if ((\colour) & 4)
|
||||
THINPLANE %a4, or
|
||||
.endif
|
||||
.if ((\colour) & 8)
|
||||
THINPLANE %a5, or
|
||||
.endif
|
||||
.endif
|
||||
.if (\colour) != 15
|
||||
not.b %d2
|
||||
.if !((\colour) & 1)
|
||||
THINPLANE %a2, and
|
||||
.endif
|
||||
.if !((\colour) & 2)
|
||||
THINPLANE %a3, and
|
||||
.endif
|
||||
.if !((\colour) & 4)
|
||||
THINPLANE %a4, and
|
||||
.endif
|
||||
.if !((\colour) & 8)
|
||||
THINPLANE %a5, and
|
||||
.endif
|
||||
.endif
|
||||
bra.w frDone
|
||||
.endm
|
||||
|
||||
|
||||
| 44 saved registers + 4 return address
|
||||
.equ FR_ARG, 48
|
||||
.equ FR_PLANES, FR_ARG + 0
|
||||
.equ FR_X, FR_ARG + 4 + 2
|
||||
.equ FR_Y, FR_ARG + 8 + 2
|
||||
.equ FR_W, FR_ARG + 12 + 2
|
||||
.equ FR_H, FR_ARG + 16 + 2
|
||||
.equ FR_COLOR, FR_ARG + 20 + 2
|
||||
|
||||
| void surface68kFillRect(uint8_t *const *planes, uint16_t x, uint16_t y,
|
||||
| uint16_t w, uint16_t h, uint16_t color);
|
||||
|
|
||||
| w and h must both be at least 1 -- draw.c clips before it calls, so an empty
|
||||
| rect never arrives. A rect covering whole rows is better served by one flat
|
||||
| run of words per plane and halPlanar.c keeps that case to itself; everything
|
||||
| else, partial pixel edges included, lands here.
|
||||
surface68kFillRect:
|
||||
_surface68kFillRect:
|
||||
movem.l %d2-%d7/%a2-%a6,%sp@- | 11 registers = 44 bytes
|
||||
move.l FR_PLANES(%sp),%a0
|
||||
move.l (%a0),%a2
|
||||
move.l 4(%a0),%a3
|
||||
move.l 8(%a0),%a4
|
||||
move.l 12(%a0),%a5
|
||||
lea gFillMaskLut,%a1
|
||||
|
||||
| span geometry, once for the whole rect
|
||||
move.w FR_X(%sp),%d0 | left
|
||||
move.w FR_W(%sp),%d1
|
||||
add.w %d0,%d1
|
||||
subq.w #1,%d1 | right
|
||||
move.w %d0,%d5
|
||||
lsr.w #3,%d5 | first byte
|
||||
and.w #7,%d0
|
||||
move.b (%a1,%d0.w),%d2 | left mask
|
||||
move.w %d1,%d0
|
||||
lsr.w #3,%d0 | last byte
|
||||
and.w #7,%d1
|
||||
move.b 8(%a1,%d1.w),%d3 | right mask
|
||||
sub.w %d5,%d0
|
||||
bne.s 1f
|
||||
| One byte wide: one combined mask, and d4 becomes the dbra
|
||||
| row count for THINBODY instead of a middle-run length.
|
||||
and.b %d3,%d2
|
||||
move.w FR_H(%sp),%d4
|
||||
subq.w #1,%d4
|
||||
moveq #1,%d6 | take the thin dispatch
|
||||
bra.s 2f
|
||||
1: subq.w #1,%d0
|
||||
move.w %d0,%d4 | middle byte count
|
||||
moveq #0,%d6
|
||||
2: | d7 = y * ROW_BYTES + firstByte, a6 = one row past the last
|
||||
move.w FR_Y(%sp),%d0
|
||||
move.w %d0,%d1
|
||||
lsl.w #5,%d0
|
||||
lsl.w #3,%d1
|
||||
add.w %d1,%d0 | y * 40, as (y << 5) + (y << 3)
|
||||
add.w %d5,%d0
|
||||
move.w %d0,%d7
|
||||
move.w FR_H(%sp),%d0
|
||||
move.w %d0,%d1
|
||||
lsl.w #5,%d0
|
||||
lsl.w #3,%d1
|
||||
add.w %d1,%d0 | h * 40
|
||||
add.w %d7,%d0
|
||||
move.w %d0,%a6
|
||||
|
||||
move.w FR_COLOR(%sp),%d0
|
||||
and.w #15,%d0
|
||||
add.w %d0,%d0
|
||||
| The jump table goes through a1: a PC-relative index has only
|
||||
| an 8-bit displacement, and the thin table is 4 KB out.
|
||||
tst.w %d6
|
||||
bne.s .Lthin
|
||||
lea frJump(%pc),%a1
|
||||
bra.s .Ldispatch
|
||||
.Lthin:
|
||||
lea ftJump(%pc),%a1
|
||||
.Ldispatch:
|
||||
move.w (%a1,%d0.w),%d1
|
||||
jmp (%a1,%d1.w)
|
||||
|
||||
frJump:
|
||||
.word frC0-frJump, frC1-frJump, frC2-frJump, frC3-frJump
|
||||
.word frC4-frJump, frC5-frJump, frC6-frJump, frC7-frJump
|
||||
.word frC8-frJump, frC9-frJump, frC10-frJump, frC11-frJump
|
||||
.word frC12-frJump, frC13-frJump, frC14-frJump, frC15-frJump
|
||||
|
||||
frC0: RECTBODY 0
|
||||
frC1: RECTBODY 1
|
||||
frC2: RECTBODY 2
|
||||
frC3: RECTBODY 3
|
||||
frC4: RECTBODY 4
|
||||
frC5: RECTBODY 5
|
||||
frC6: RECTBODY 6
|
||||
frC7: RECTBODY 7
|
||||
frC8: RECTBODY 8
|
||||
frC9: RECTBODY 9
|
||||
frC10: RECTBODY 10
|
||||
frC11: RECTBODY 11
|
||||
frC12: RECTBODY 12
|
||||
frC13: RECTBODY 13
|
||||
frC14: RECTBODY 14
|
||||
frC15: RECTBODY 15
|
||||
|
||||
ftJump:
|
||||
.word ftC0-ftJump, ftC1-ftJump, ftC2-ftJump, ftC3-ftJump
|
||||
.word ftC4-ftJump, ftC5-ftJump, ftC6-ftJump, ftC7-ftJump
|
||||
.word ftC8-ftJump, ftC9-ftJump, ftC10-ftJump, ftC11-ftJump
|
||||
.word ftC12-ftJump, ftC13-ftJump, ftC14-ftJump, ftC15-ftJump
|
||||
|
||||
ftC0: THINBODY 0
|
||||
ftC1: THINBODY 1
|
||||
ftC2: THINBODY 2
|
||||
ftC3: THINBODY 3
|
||||
ftC4: THINBODY 4
|
||||
ftC5: THINBODY 5
|
||||
ftC6: THINBODY 6
|
||||
ftC7: THINBODY 7
|
||||
ftC8: THINBODY 8
|
||||
ftC9: THINBODY 9
|
||||
ftC10: THINBODY 10
|
||||
ftC11: THINBODY 11
|
||||
ftC12: THINBODY 12
|
||||
ftC13: THINBODY 13
|
||||
ftC14: THINBODY 14
|
||||
ftC15: THINBODY 15
|
||||
|
||||
frDone:
|
||||
movem.l %sp@+,%d2-%d7/%a2-%a6
|
||||
rts
|
||||
|
||||
|
||||
.data
|
||||
.even
|
||||
| Eight left-edge masks (bits at and to the right of x&7), then eight
|
||||
| right-edge masks (bits at and to the left of it). The span body reaches the
|
||||
| second half at 8(%a1,%dn.w), so the two must stay adjacent.
|
||||
gFillMaskLut:
|
||||
.byte 0xFF, 0x7F, 0x3F, 0x1F, 0x0F, 0x07, 0x03, 0x01
|
||||
.byte 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 0xFF
|
||||
428
src/m68k/planarPlot.s
Normal file
428
src/m68k/planarPlot.s
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
| Shared 68000 separate-plane pixel walkers -- hand-rolled.
|
||||
|
|
||||
| surface68kPlanarCircle jlDrawCircle: Bresenham midpoint outline
|
||||
| surface68kPlanarLine jlDrawLine: Bresenham diagonal
|
||||
|
|
||||
| Used by BOTH separate-plane 68k ports, the Amiga and the X68000, which keep
|
||||
| four plane buffers of 40 bytes per row. Same shape as src/atarist/circle.s,
|
||||
| adapted from that port's memory layout: the ST keeps one word-interleaved
|
||||
| buffer, so a pixel there is four words in one group, while here it is a BYTE
|
||||
| mask at the same offset in all four planes.
|
||||
|
|
||||
| Pixel x: byte = x >> 3, bit = 0x80 >> (x & 7).
|
||||
| Plane N at row y: planes[N] + y*40 + (x >> 3).
|
||||
|
|
||||
| Why assembly. The C version plotted through a helper that recomputed the
|
||||
| row offset, the mask and its complement for every one of the eight
|
||||
| symmetric points, then did a load-modify-store per plane: four reads and
|
||||
| four writes where the machine can do four read-modify-writes. Measured at
|
||||
| 0.65x of the Atari ST on a CPU that is 25% faster.
|
||||
|
|
||||
| The two things that make this quick are both borrowed from the ST:
|
||||
|
|
||||
| * 16-WAY COLOUR DISPATCH. The colour is fixed for the whole circle, so
|
||||
| the plot is emitted once per colour value with the set-or-clear choice
|
||||
| already made. Every plane write is then a single `or.b` or `and.b`
|
||||
| straight to memory -- no test, no branch, no read-back.
|
||||
| * PER-ITERATION PRECOMPUTE. One Bresenham step touches four distinct
|
||||
| rows and four distinct x positions, so their offsets and masks are
|
||||
| built once into a scratch block and the eight plots just read them.
|
||||
|
|
||||
| Bit-exact with the C it replaces, including the tie: err <= 0 must take the
|
||||
| no-shrink branch, or 114 of the 199 reachable radii plot a different pixel
|
||||
| set (see the note in halPlanar.c). No clipping, matching the C -- the
|
||||
| caller guarantees the circle is on-surface.
|
||||
|
|
||||
| ABI: cdecl (gcc-amigaos / elf2x68k), d2-d7/a2-a6 callee-save. Both symbol
|
||||
| spellings are defined at each entry point, because gcc-amigaos prepends an
|
||||
| underscore to C names and elf2x68k does not.
|
||||
|
|
||||
| void surface68kPlanarCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy,
|
||||
| uint16_t r, uint16_t color);
|
||||
|
|
||||
| Register allocation:
|
||||
| a0..a3 = plane 0..3 base pointers
|
||||
| a5 = gPlanarBitLut (byte masks)
|
||||
| d2.w = bx, d3.w = by, d4.w = err
|
||||
| d5.w = cx, a6.w = cy
|
||||
| d0,d1,d6,d7 = scratch
|
||||
|
|
||||
| Scratch block (24 bytes) at sp+0..23:
|
||||
| sp+0,+2,+4,+6 xByte for cx+bx, cx-bx, cx+by, cx-by (x >> 3)
|
||||
| sp+8,+9,+10,+11 bit mask for the same four (byte each)
|
||||
| sp+12,+14,+16,+18 row offset for cy+by, cy-by, cy+bx, cy-bx (y * 40)
|
||||
| sp+20,+22 unused padding, keeps the block even
|
||||
|
||||
| Bytes per plane row: SURFACE_WIDTH / 8 on both ports.
|
||||
.equ ROW_BYTES, 40
|
||||
|
||||
.text
|
||||
.even
|
||||
.globl surface68kPlanarCircle
|
||||
.globl _surface68kPlanarCircle
|
||||
.globl surface68kPlanarLine
|
||||
.globl _surface68kPlanarLine
|
||||
|
||||
| ---- XREC: xByte and bit mask for cx <op> <reg>, into slots ----
|
||||
| Trashes d0, d6, d7.
|
||||
.macro XREC slotB, slotM, op, reg
|
||||
move.w %d5,%d6
|
||||
\op\().w \reg,%d6 | d6 = x
|
||||
move.w %d6,%d7
|
||||
lsr.w #3,%d7 | d7 = x >> 3
|
||||
move.w %d7,\slotB(%sp)
|
||||
and.w #7,%d6
|
||||
move.b (%a5,%d6.w),%d0 | d0 = 0x80 >> (x & 7)
|
||||
move.b %d0,\slotM(%sp)
|
||||
.endm
|
||||
|
||||
|
||||
| ---- YREC: row offset for cy <op> <reg>, into slot ----
|
||||
| y * 40 as (y << 5) + (y << 3): a 68000 has no fast multiply and a table
|
||||
| would cost an address register this routine does not have spare.
|
||||
| Trashes d0, d6.
|
||||
.macro YREC slot, op, reg
|
||||
move.w %a6,%d6
|
||||
\op\().w \reg,%d6 | d6 = y
|
||||
move.w %d6,%d0
|
||||
lsl.w #5,%d0 | y * 32
|
||||
lsl.w #3,%d6 | y * 8
|
||||
add.w %d0,%d6 | y * 40
|
||||
move.w %d6,\slot(%sp)
|
||||
.endm
|
||||
|
||||
|
||||
| ---- PLOT: one pixel, colour known at assembly time ----
|
||||
| slotY: row offset slot, slotB: xByte slot, slotM: mask slot.
|
||||
| Trashes d0, d1, d7.
|
||||
.macro PLOT slotY, slotB, slotM, colour
|
||||
move.w \slotY(%sp),%d0
|
||||
add.w \slotB(%sp),%d0 | d0 = y*40 + (x >> 3)
|
||||
move.b \slotM(%sp),%d1 | d1 = bit
|
||||
.if (\colour) != 15
|
||||
move.b %d1,%d7
|
||||
not.b %d7 | d7 = ~bit, for the clears
|
||||
.endif
|
||||
.if ((\colour) & 1)
|
||||
or.b %d1,(%a0,%d0.w)
|
||||
.else
|
||||
and.b %d7,(%a0,%d0.w)
|
||||
.endif
|
||||
.if ((\colour) & 2)
|
||||
or.b %d1,(%a1,%d0.w)
|
||||
.else
|
||||
and.b %d7,(%a1,%d0.w)
|
||||
.endif
|
||||
.if ((\colour) & 4)
|
||||
or.b %d1,(%a2,%d0.w)
|
||||
.else
|
||||
and.b %d7,(%a2,%d0.w)
|
||||
.endif
|
||||
.if ((\colour) & 8)
|
||||
or.b %d1,(%a3,%d0.w)
|
||||
.else
|
||||
and.b %d7,(%a3,%d0.w)
|
||||
.endif
|
||||
.endm
|
||||
|
||||
|
||||
| ---- BODY: the whole Bresenham loop for one colour ----
|
||||
.macro BODY colour
|
||||
.Lloop\colour:
|
||||
XREC 0, 8, add, %d2 | cx + bx
|
||||
XREC 2, 9, sub, %d2 | cx - bx
|
||||
XREC 4, 10, add, %d3 | cx + by
|
||||
XREC 6, 11, sub, %d3 | cx - by
|
||||
YREC 12, add, %d3 | cy + by
|
||||
YREC 14, sub, %d3 | cy - by
|
||||
YREC 16, add, %d2 | cy + bx
|
||||
YREC 18, sub, %d2 | cy - bx
|
||||
|
||||
PLOT 12, 0, 8, \colour | (cx+bx, cy+by)
|
||||
PLOT 12, 2, 9, \colour | (cx-bx, cy+by)
|
||||
PLOT 14, 0, 8, \colour | (cx+bx, cy-by)
|
||||
PLOT 14, 2, 9, \colour | (cx-bx, cy-by)
|
||||
PLOT 16, 4, 10, \colour | (cx+by, cy+bx)
|
||||
PLOT 16, 6, 11, \colour | (cx-by, cy+bx)
|
||||
PLOT 18, 4, 10, \colour | (cx+by, cy-bx)
|
||||
PLOT 18, 6, 11, \colour | (cx-by, cy-bx)
|
||||
|
||||
| err <= 0 must NOT shrink the major axis: the tie is what
|
||||
| keeps this pixel-identical to src/core/draw.c.
|
||||
tst.w %d4
|
||||
bgt.s .Lshrink\colour
|
||||
move.w %d2,%d0
|
||||
add.w %d0,%d0
|
||||
addq.w #3,%d0
|
||||
add.w %d0,%d4 | err += 2*bx + 3
|
||||
bra.s .Lstep\colour
|
||||
.Lshrink\colour:
|
||||
move.w %d2,%d0
|
||||
sub.w %d3,%d0
|
||||
add.w %d0,%d0
|
||||
addq.w #5,%d0
|
||||
add.w %d0,%d4 | err += 2*(bx - by) + 5
|
||||
subq.w #1,%d3 | by--
|
||||
.Lstep\colour:
|
||||
addq.w #1,%d2 | bx++
|
||||
cmp.w %d3,%d2
|
||||
ble.w .Lloop\colour
|
||||
bra.w circleDone
|
||||
.endm
|
||||
|
||||
|
||||
surface68kPlanarCircle:
|
||||
_surface68kPlanarCircle:
|
||||
movem.l %d2-%d7/%a2-%a6,%sp@- | 11 registers = 44 bytes
|
||||
sub.l #24,%sp | scratch block
|
||||
| args: 24 scratch + 44 saved + 4 return = 72
|
||||
move.l 72(%sp),%a4 | planes[]
|
||||
move.l (%a4),%a0
|
||||
move.l 4(%a4),%a1
|
||||
move.l 8(%a4),%a2
|
||||
move.l 12(%a4),%a3
|
||||
move.w 78(%sp),%d5 | cx (low word of the int arg)
|
||||
move.w 82(%sp),%d6 | cy
|
||||
move.w %d6,%a6
|
||||
move.w 86(%sp),%d3 | r -> by
|
||||
move.w 90(%sp),%d0 | colour
|
||||
lea gPlanarBitLut,%a5
|
||||
|
||||
moveq #0,%d2 | bx = 0
|
||||
move.w %d3,%d4
|
||||
neg.w %d4
|
||||
addq.w #1,%d4 | err = 1 - r
|
||||
cmp.w %d3,%d2
|
||||
bgt.w circleDone | never taken for r >= 0; kept as a guard
|
||||
|
||||
and.w #15,%d0
|
||||
add.w %d0,%d0
|
||||
move.w circleJump(%pc,%d0.w),%d0
|
||||
jmp circleJump(%pc,%d0.w)
|
||||
|
||||
circleJump:
|
||||
.word circleC0-circleJump, circleC1-circleJump
|
||||
.word circleC2-circleJump, circleC3-circleJump
|
||||
.word circleC4-circleJump, circleC5-circleJump
|
||||
.word circleC6-circleJump, circleC7-circleJump
|
||||
.word circleC8-circleJump, circleC9-circleJump
|
||||
.word circleC10-circleJump, circleC11-circleJump
|
||||
.word circleC12-circleJump, circleC13-circleJump
|
||||
.word circleC14-circleJump, circleC15-circleJump
|
||||
|
||||
circleC0: BODY 0
|
||||
circleC1: BODY 1
|
||||
circleC2: BODY 2
|
||||
circleC3: BODY 3
|
||||
circleC4: BODY 4
|
||||
circleC5: BODY 5
|
||||
circleC6: BODY 6
|
||||
circleC7: BODY 7
|
||||
circleC8: BODY 8
|
||||
circleC9: BODY 9
|
||||
circleC10: BODY 10
|
||||
circleC11: BODY 11
|
||||
circleC12: BODY 12
|
||||
circleC13: BODY 13
|
||||
circleC14: BODY 14
|
||||
circleC15: BODY 15
|
||||
|
||||
circleDone:
|
||||
add.l #24,%sp
|
||||
movem.l %sp@+,%d2-%d7/%a2-%a6
|
||||
rts
|
||||
|
||||
|
||||
| ---------------- surface68kPlanarLine ----------------
|
||||
|
|
||||
| Bresenham's diagonal line. Straight runs never arrive: src/core/draw.c
|
||||
| diverts horizontal and vertical lines to jlFillRect, so this walks the
|
||||
| general case only. Bit-identical to draw.c's own walker, including the
|
||||
| e2 >= dy / e2 <= dx tie handling that decides which pixels a 45-degree
|
||||
| line lands on.
|
||||
|
|
||||
| Same two ideas as the circle above. The colour is fixed for the whole line,
|
||||
| so the body is emitted once per colour value and every plane write is a bare
|
||||
| `or.b` or `and.b` to memory. And the row offset is CARRIED in a6 rather than
|
||||
| recomputed: y moves by one row at a time, so it is one add of +/-40 per step
|
||||
| instead of the two shifts and an add a multiply-free y*40 would cost per
|
||||
| pixel.
|
||||
|
|
||||
| void surface68kPlanarLine(uint8_t *const *planes, int16_t x0, int16_t y0,
|
||||
| int16_t x1, int16_t y1, uint16_t color);
|
||||
|
|
||||
| Register allocation:
|
||||
| a0..a3 = plane 0..3 bases, a5 = gPlanarBitLut, a6.w = y0 * 40
|
||||
| d2.w = x0, d3.w = y0, d4.w = err, d5.w = dx, d6.w = dy (negative)
|
||||
| d0, d1, d7 = scratch
|
||||
|
|
||||
| Locals (12 bytes): x1, y1, sx, sy, sy40
|
||||
|
||||
.equ LN_X1, 0
|
||||
.equ LN_Y1, 2
|
||||
.equ LN_SX, 4
|
||||
.equ LN_SY, 6
|
||||
.equ LN_SY40, 8
|
||||
.equ LN_LOCAL, 12
|
||||
| 12 locals + 44 saved registers + 4 return address
|
||||
.equ LN_ARG, (LN_LOCAL + 44 + 4)
|
||||
.equ LN_PLANES, LN_ARG + 0
|
||||
.equ LN_X0, LN_ARG + 4 + 2
|
||||
.equ LN_Y0, LN_ARG + 8 + 2
|
||||
.equ LN_AX1, LN_ARG + 12 + 2
|
||||
.equ LN_AY1, LN_ARG + 16 + 2
|
||||
.equ LN_COLOR, LN_ARG + 20 + 2
|
||||
|
||||
|
||||
| ---- LPLOT: one pixel at (d2, a6), colour known at assembly time ----
|
||||
| Trashes d0, d1, d7.
|
||||
.macro LPLOT colour
|
||||
move.w %d2,%d0
|
||||
lsr.w #3,%d0
|
||||
add.w %a6,%d0 | y*40 + (x >> 3)
|
||||
move.w %d2,%d1
|
||||
and.w #7,%d1
|
||||
move.b (%a5,%d1.w),%d1 | 0x80 >> (x & 7)
|
||||
.if (\colour) != 15
|
||||
move.b %d1,%d7
|
||||
not.b %d7
|
||||
.endif
|
||||
.if ((\colour) & 1)
|
||||
or.b %d1,(%a0,%d0.w)
|
||||
.else
|
||||
and.b %d7,(%a0,%d0.w)
|
||||
.endif
|
||||
.if ((\colour) & 2)
|
||||
or.b %d1,(%a1,%d0.w)
|
||||
.else
|
||||
and.b %d7,(%a1,%d0.w)
|
||||
.endif
|
||||
.if ((\colour) & 4)
|
||||
or.b %d1,(%a2,%d0.w)
|
||||
.else
|
||||
and.b %d7,(%a2,%d0.w)
|
||||
.endif
|
||||
.if ((\colour) & 8)
|
||||
or.b %d1,(%a3,%d0.w)
|
||||
.else
|
||||
and.b %d7,(%a3,%d0.w)
|
||||
.endif
|
||||
.endm
|
||||
|
||||
|
||||
| ---- LINEBODY: the whole Bresenham walk for one colour ----
|
||||
.macro LINEBODY colour
|
||||
.Lline\colour:
|
||||
LPLOT \colour
|
||||
| done when both coordinates have arrived
|
||||
cmp.w LN_X1(%sp),%d2
|
||||
bne.s .LlineStep\colour
|
||||
cmp.w LN_Y1(%sp),%d3
|
||||
beq.w lineDone
|
||||
.LlineStep\colour:
|
||||
move.w %d4,%d0
|
||||
add.w %d0,%d0 | e2 = 2 * err
|
||||
cmp.w %d6,%d0
|
||||
blt.s .LlineNoX\colour
|
||||
add.w %d6,%d4 | err += dy
|
||||
add.w LN_SX(%sp),%d2 | x += sx
|
||||
.LlineNoX\colour:
|
||||
cmp.w %d5,%d0
|
||||
bgt.w .Lline\colour
|
||||
add.w %d5,%d4 | err += dx
|
||||
add.w LN_SY(%sp),%d3 | y += sy
|
||||
adda.w LN_SY40(%sp),%a6 | and its row offset
|
||||
bra.w .Lline\colour
|
||||
.endm
|
||||
|
||||
|
||||
surface68kPlanarLine:
|
||||
_surface68kPlanarLine:
|
||||
movem.l %d2-%d7/%a2-%a6,%sp@- | 11 registers = 44 bytes
|
||||
sub.l #LN_LOCAL,%sp
|
||||
move.l LN_PLANES(%sp),%a4
|
||||
move.l (%a4),%a0
|
||||
move.l 4(%a4),%a1
|
||||
move.l 8(%a4),%a2
|
||||
move.l 12(%a4),%a3
|
||||
lea gPlanarBitLut,%a5
|
||||
move.w LN_X0(%sp),%d2
|
||||
move.w LN_Y0(%sp),%d3
|
||||
move.w LN_AX1(%sp),%d0
|
||||
move.w %d0,LN_X1(%sp)
|
||||
move.w LN_AY1(%sp),%d1
|
||||
move.w %d1,LN_Y1(%sp)
|
||||
|
||||
| dx = |x1 - x0|, sx = sign
|
||||
move.w #1,LN_SX(%sp)
|
||||
move.w %d0,%d5
|
||||
sub.w %d2,%d5
|
||||
bge.s 1f
|
||||
neg.w %d5
|
||||
move.w #-1,LN_SX(%sp)
|
||||
1: | dy = -|y1 - y0|, sy = sign
|
||||
move.w #1,LN_SY(%sp)
|
||||
move.w #ROW_BYTES,LN_SY40(%sp)
|
||||
move.w %d1,%d6
|
||||
sub.w %d3,%d6
|
||||
bge.s 2f
|
||||
neg.w %d6
|
||||
move.w #-1,LN_SY(%sp)
|
||||
move.w #-ROW_BYTES,LN_SY40(%sp)
|
||||
2: neg.w %d6 | dy is carried negative
|
||||
move.w %d5,%d4
|
||||
add.w %d6,%d4 | err = dx + dy
|
||||
|
||||
| a6 = y0 * 40, as (y << 5) + (y << 3)
|
||||
move.w %d3,%d0
|
||||
move.w %d3,%d1
|
||||
lsl.w #5,%d0
|
||||
lsl.w #3,%d1
|
||||
add.w %d1,%d0
|
||||
move.w %d0,%a6
|
||||
|
||||
move.w LN_COLOR(%sp),%d0
|
||||
and.w #15,%d0
|
||||
add.w %d0,%d0
|
||||
lea lineJump(%pc),%a4
|
||||
move.w (%a4,%d0.w),%d1
|
||||
jmp (%a4,%d1.w)
|
||||
|
||||
lineJump:
|
||||
.word lineC0-lineJump, lineC1-lineJump
|
||||
.word lineC2-lineJump, lineC3-lineJump
|
||||
.word lineC4-lineJump, lineC5-lineJump
|
||||
.word lineC6-lineJump, lineC7-lineJump
|
||||
.word lineC8-lineJump, lineC9-lineJump
|
||||
.word lineC10-lineJump, lineC11-lineJump
|
||||
.word lineC12-lineJump, lineC13-lineJump
|
||||
.word lineC14-lineJump, lineC15-lineJump
|
||||
|
||||
lineC0: LINEBODY 0
|
||||
lineC1: LINEBODY 1
|
||||
lineC2: LINEBODY 2
|
||||
lineC3: LINEBODY 3
|
||||
lineC4: LINEBODY 4
|
||||
lineC5: LINEBODY 5
|
||||
lineC6: LINEBODY 6
|
||||
lineC7: LINEBODY 7
|
||||
lineC8: LINEBODY 8
|
||||
lineC9: LINEBODY 9
|
||||
lineC10: LINEBODY 10
|
||||
lineC11: LINEBODY 11
|
||||
lineC12: LINEBODY 12
|
||||
lineC13: LINEBODY 13
|
||||
lineC14: LINEBODY 14
|
||||
lineC15: LINEBODY 15
|
||||
|
||||
lineDone:
|
||||
add.l #LN_LOCAL,%sp
|
||||
movem.l %sp@+,%d2-%d7/%a2-%a6
|
||||
rts
|
||||
|
||||
|
||||
.data
|
||||
.even
|
||||
| 0x80 >> (x & 7), the byte mask for a pixel's position within its byte.
|
||||
gPlanarBitLut:
|
||||
.byte 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01
|
||||
|
|
@ -1,401 +0,0 @@
|
|||
| Shared 68000 fast path for the planar surface buffer.
|
||||
|
|
||||
| Holds surface68kFillSpan4Planes -- the per-row body of the 4-plane
|
||||
| planar rect/circle fill. Only the Amiga port calls it (ST is
|
||||
| word-interleaved and uses its own St* routines); it lives here
|
||||
| because the file is globbed into both ports' builds. The earlier
|
||||
| chunky clear/fill primitives that this file once also held were
|
||||
| removed once both ports went native-planar.
|
||||
|
|
||||
| Calling convention: cdecl (gcc-amigaos / gcc-atari-mint).
|
||||
| d0/d1/a0/a1 are caller-save (scratch).
|
||||
| d2-d7/a2-a6 are callee-save (must be preserved).
|
||||
| 16-bit shorts are sign-extended to int (4 bytes) on the call stack.
|
||||
|
|
||||
| GAS m68k syntax. Assembled by the gcc driver via binutils m68k-as.
|
||||
|
||||
.text
|
||||
|
||||
|
||||
| ----------------------------------------------------------------
|
||||
| void surface68kFillSpan4Planes(uint8_t *p0, uint8_t *p1,
|
||||
| uint8_t *p2, uint8_t *p3,
|
||||
| uint16_t numMid,
|
||||
| uint8_t leftMask, uint8_t rightMask,
|
||||
| uint8_t fb0, uint8_t fb1,
|
||||
| uint8_t fb2, uint8_t fb3);
|
||||
|
|
||||
| Fill ONE planar row across 4 planes -- the per-row body of
|
||||
| jlpFillRect lifted into asm. Each pN points at the leading
|
||||
| byte (already advanced by planeBase + y*40 + byteFirst on the C
|
||||
| side). leftMask and rightMask are the partial-byte masks for the
|
||||
| left/right edges; numMid is the count of full bytes between them.
|
||||
| fbN is 0x00 or 0xFF, the per-plane fill byte (caller pre-classifies
|
||||
| (colorIndex >> N) & 1 -> 0xFF or 0x00).
|
||||
|
|
||||
| Used by Amiga jlpFillCircle (one call per scanline span) and
|
||||
| Amiga jlpFillRect (one call per row of the rect). Replaces
|
||||
| the C inner loop whose ~13 cyc/byte was the gating cost on
|
||||
| jlFillCircle r=40 even after C-side inlining.
|
||||
|
|
||||
| Mask convention is uniform for all planes:
|
||||
| leading byte := (*p & ~leftMask) | (fbN & leftMask)
|
||||
| middle bytes := fbN
|
||||
| trailing byte := (*p & ~rightMask) | (fbN & rightMask)
|
||||
| -- branchless: the same arithmetic produces "set" or "clear" based
|
||||
| on whether fbN is 0xFF or 0x00.
|
||||
|
|
||||
| ABI: m68k cdecl. d2-d7/a2-a6 callee-save. Only a2/a3 are actually
|
||||
| used by the body (planes 2/3); a4-a6 are not touched, so only
|
||||
| d2-d7/a2-a3 are movem'd here.
|
||||
| Stack offset to first arg after MOVEM: 8 regs * 4 = 32 bytes saved
|
||||
| + 4 ret PC = 36.
|
||||
| ----------------------------------------------------------------
|
||||
.globl _surface68kFillSpan4Planes
|
||||
|
||||
.equ SP_SAVED, 32
|
||||
.equ SP_RPC, 4
|
||||
.equ SP_OFF, (SP_SAVED + SP_RPC)
|
||||
|
||||
.equ SP_P0, SP_OFF + 0
|
||||
.equ SP_P1, SP_OFF + 4
|
||||
.equ SP_P2, SP_OFF + 8
|
||||
.equ SP_P3, SP_OFF + 12
|
||||
.equ SP_NMID, SP_OFF + 16 + 2 | int -> low word at +2
|
||||
.equ SP_LMASK, SP_OFF + 20 + 3 | int -> low byte at +3
|
||||
.equ SP_RMASK, SP_OFF + 24 + 3
|
||||
.equ SP_FB0, SP_OFF + 28 + 3
|
||||
.equ SP_FB1, SP_OFF + 32 + 3
|
||||
.equ SP_FB2, SP_OFF + 36 + 3
|
||||
.equ SP_FB3, SP_OFF + 40 + 3
|
||||
|
||||
| Per-plane work is fully inlined (hand-unrolled 4x rather than
|
||||
| bsr+rts, to dodge ~12 cyc per return + the per-plane re-test of
|
||||
| numMid that an earlier build paid). Register convention across all
|
||||
| four planes:
|
||||
| d1 = leftMask, d2 = ~leftMask, d3 = rightMask, d4 = ~rightMask
|
||||
| d5 = this plane's fb byte, d6/d0/d7 = per-plane scratch.
|
||||
|
|
||||
| The unmasked middle run is filled with long stores rather than
|
||||
| byte-at-a-time. fb is 0x00 or 0xFF, so the long is built with a
|
||||
| byte->word->long sign-extend (ext.w / ext.l). The mid pointer is
|
||||
| advanced to a long boundary by storing 0-3 fb bytes (align run),
|
||||
| then move.l (an)+ fills (midRem >> 2) longs, then a 0-3 byte tail
|
||||
| drains the remainder. The masked left/right edge bytes keep their
|
||||
| per-plane read-modify-write; only the unmasked middle changed.
|
||||
|
|
||||
| Plain text version of the per-plane body (translate to asm 4x with
|
||||
| different a-regs and fb stack offsets):
|
||||
|
|
||||
| move.b (an),%d6 | --- leading masked edge byte ---
|
||||
| and.b %d2,%d6
|
||||
| move.b fb,%d5
|
||||
| move.b %d5,%d7
|
||||
| and.b %d1,%d7
|
||||
| or.b %d7,%d6
|
||||
| move.b %d6,(an)+
|
||||
| < if has-middle path: >
|
||||
| move.b %d5,%d7 | build replicated fill long in d7
|
||||
| ext.w %d7
|
||||
| ext.l %d7
|
||||
| move.l an,%d6 | align = (-an) & 3, clamped to numMid
|
||||
| neg.l %d6
|
||||
| and.w #3,%d6
|
||||
| move.w numMid,%d0
|
||||
| < clamp d6 to d0; d0 = midRem = numMid - align >
|
||||
| < store d6 fb bytes to reach a long boundary >
|
||||
| < midLongs = midRem >> 2; tail = midRem & 3 >
|
||||
| .longN:
|
||||
| move.l %d7,(an)+
|
||||
| dbra midLongs,.longN
|
||||
| < store tail fb bytes >
|
||||
| < trailing masked edge byte: >
|
||||
| move.b (an),%d6
|
||||
| and.b %d4,%d6
|
||||
| move.b %d5,%d7
|
||||
| and.b %d3,%d7
|
||||
| or.b %d7,%d6
|
||||
| move.b %d6,(an)
|
||||
|
||||
_surface68kFillSpan4Planes:
|
||||
movem.l %d2-%d7/%a2-%a3,-(%sp)
|
||||
|
||||
move.b SP_LMASK(%sp),%d1
|
||||
move.b %d1,%d2
|
||||
not.b %d2
|
||||
move.b SP_RMASK(%sp),%d3
|
||||
move.b %d3,%d4
|
||||
not.b %d4
|
||||
|
||||
move.l SP_P0(%sp),%a0
|
||||
move.l SP_P1(%sp),%a1
|
||||
move.l SP_P2(%sp),%a2
|
||||
move.l SP_P3(%sp),%a3
|
||||
|
||||
| One-time numMid test. d0.w = numMid; if 0 jump to
|
||||
| the no-middle entry. The with-middle path now fills
|
||||
| the unmasked middle run with long stores (move.l
|
||||
| (an)+) instead of byte-at-a-time, after aligning the
|
||||
| mid pointer to a long boundary and draining a 0-3
|
||||
| byte tail. The masked left/right edge bytes keep their
|
||||
| per-plane read-modify-write exactly as before.
|
||||
|
|
||||
| The mid fill byte fbN is 0x00 or 0xFF, so the long
|
||||
| replication is a byte->word->long sign-extend (ext.w
|
||||
| then ext.l): 0xFF -> 0xFFFFFFFF, 0x00 -> 0x00000000.
|
||||
|
|
||||
| Per-plane register use during the mid run:
|
||||
| d5 = fbN byte (held for tail + trailing edge)
|
||||
| d7 = fbN replicated to a long (move.l fill value)
|
||||
| d6 = scratch / align count / midLongs count
|
||||
| d0 = numMid, then midRem, then tail-byte count
|
||||
| Masks d1-d4 stay live across all four planes. Local
|
||||
| numeric labels 1f-7f are reused per plane; each
|
||||
| reference resolves to the nearest copy.
|
||||
move.w SP_NMID(%sp),%d0
|
||||
beq .LfsNoMid
|
||||
|
||||
| ---- WITH-MIDDLE PATH ----
|
||||
| Plane 0
|
||||
move.b (%a0),%d6
|
||||
and.b %d2,%d6
|
||||
move.b SP_FB0(%sp),%d5
|
||||
move.b %d5,%d7
|
||||
and.b %d1,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a0)+
|
||||
move.b %d5,%d7
|
||||
ext.w %d7
|
||||
ext.l %d7
|
||||
move.l %a0,%d6
|
||||
neg.l %d6
|
||||
and.w #3,%d6
|
||||
move.w SP_NMID(%sp),%d0
|
||||
cmp.w %d0,%d6
|
||||
bls.s 1f
|
||||
move.w %d0,%d6
|
||||
1: sub.w %d6,%d0
|
||||
tst.w %d6
|
||||
beq.s 2f
|
||||
3: move.b %d5,(%a0)+
|
||||
subq.w #1,%d6
|
||||
bne.s 3b
|
||||
2: move.w %d0,%d6
|
||||
lsr.w #2,%d6
|
||||
and.w #3,%d0
|
||||
tst.w %d6
|
||||
beq.s 5f
|
||||
subq.w #1,%d6
|
||||
4: move.l %d7,(%a0)+
|
||||
dbra %d6,4b
|
||||
5: tst.w %d0
|
||||
beq.s 7f
|
||||
6: move.b %d5,(%a0)+
|
||||
subq.w #1,%d0
|
||||
bne.s 6b
|
||||
7: move.b (%a0),%d6
|
||||
and.b %d4,%d6
|
||||
move.b %d5,%d7
|
||||
and.b %d3,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a0)
|
||||
|
||||
| Plane 1
|
||||
move.b (%a1),%d6
|
||||
and.b %d2,%d6
|
||||
move.b SP_FB1(%sp),%d5
|
||||
move.b %d5,%d7
|
||||
and.b %d1,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a1)+
|
||||
move.b %d5,%d7
|
||||
ext.w %d7
|
||||
ext.l %d7
|
||||
move.l %a1,%d6
|
||||
neg.l %d6
|
||||
and.w #3,%d6
|
||||
move.w SP_NMID(%sp),%d0
|
||||
cmp.w %d0,%d6
|
||||
bls.s 1f
|
||||
move.w %d0,%d6
|
||||
1: sub.w %d6,%d0
|
||||
tst.w %d6
|
||||
beq.s 2f
|
||||
3: move.b %d5,(%a1)+
|
||||
subq.w #1,%d6
|
||||
bne.s 3b
|
||||
2: move.w %d0,%d6
|
||||
lsr.w #2,%d6
|
||||
and.w #3,%d0
|
||||
tst.w %d6
|
||||
beq.s 5f
|
||||
subq.w #1,%d6
|
||||
4: move.l %d7,(%a1)+
|
||||
dbra %d6,4b
|
||||
5: tst.w %d0
|
||||
beq.s 7f
|
||||
6: move.b %d5,(%a1)+
|
||||
subq.w #1,%d0
|
||||
bne.s 6b
|
||||
7: move.b (%a1),%d6
|
||||
and.b %d4,%d6
|
||||
move.b %d5,%d7
|
||||
and.b %d3,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a1)
|
||||
|
||||
| Plane 2
|
||||
move.b (%a2),%d6
|
||||
and.b %d2,%d6
|
||||
move.b SP_FB2(%sp),%d5
|
||||
move.b %d5,%d7
|
||||
and.b %d1,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a2)+
|
||||
move.b %d5,%d7
|
||||
ext.w %d7
|
||||
ext.l %d7
|
||||
move.l %a2,%d6
|
||||
neg.l %d6
|
||||
and.w #3,%d6
|
||||
move.w SP_NMID(%sp),%d0
|
||||
cmp.w %d0,%d6
|
||||
bls.s 1f
|
||||
move.w %d0,%d6
|
||||
1: sub.w %d6,%d0
|
||||
tst.w %d6
|
||||
beq.s 2f
|
||||
3: move.b %d5,(%a2)+
|
||||
subq.w #1,%d6
|
||||
bne.s 3b
|
||||
2: move.w %d0,%d6
|
||||
lsr.w #2,%d6
|
||||
and.w #3,%d0
|
||||
tst.w %d6
|
||||
beq.s 5f
|
||||
subq.w #1,%d6
|
||||
4: move.l %d7,(%a2)+
|
||||
dbra %d6,4b
|
||||
5: tst.w %d0
|
||||
beq.s 7f
|
||||
6: move.b %d5,(%a2)+
|
||||
subq.w #1,%d0
|
||||
bne.s 6b
|
||||
7: move.b (%a2),%d6
|
||||
and.b %d4,%d6
|
||||
move.b %d5,%d7
|
||||
and.b %d3,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a2)
|
||||
|
||||
| Plane 3
|
||||
move.b (%a3),%d6
|
||||
and.b %d2,%d6
|
||||
move.b SP_FB3(%sp),%d5
|
||||
move.b %d5,%d7
|
||||
and.b %d1,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a3)+
|
||||
move.b %d5,%d7
|
||||
ext.w %d7
|
||||
ext.l %d7
|
||||
move.l %a3,%d6
|
||||
neg.l %d6
|
||||
and.w #3,%d6
|
||||
move.w SP_NMID(%sp),%d0
|
||||
cmp.w %d0,%d6
|
||||
bls.s 1f
|
||||
move.w %d0,%d6
|
||||
1: sub.w %d6,%d0
|
||||
tst.w %d6
|
||||
beq.s 2f
|
||||
3: move.b %d5,(%a3)+
|
||||
subq.w #1,%d6
|
||||
bne.s 3b
|
||||
2: move.w %d0,%d6
|
||||
lsr.w #2,%d6
|
||||
and.w #3,%d0
|
||||
tst.w %d6
|
||||
beq.s 5f
|
||||
subq.w #1,%d6
|
||||
4: move.l %d7,(%a3)+
|
||||
dbra %d6,4b
|
||||
5: tst.w %d0
|
||||
beq.s 7f
|
||||
6: move.b %d5,(%a3)+
|
||||
subq.w #1,%d0
|
||||
bne.s 6b
|
||||
7: move.b (%a3),%d6
|
||||
and.b %d4,%d6
|
||||
move.b %d5,%d7
|
||||
and.b %d3,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a3)
|
||||
|
||||
movem.l (%sp)+,%d2-%d7/%a2-%a3
|
||||
rts
|
||||
|
||||
.LfsNoMid:
|
||||
| ---- NO-MIDDLE PATH (just leading + trailing) ----
|
||||
| Plane 0
|
||||
move.b (%a0),%d6
|
||||
and.b %d2,%d6
|
||||
move.b SP_FB0(%sp),%d5
|
||||
move.b %d5,%d7
|
||||
and.b %d1,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a0)+
|
||||
move.b (%a0),%d6
|
||||
and.b %d4,%d6
|
||||
move.b %d5,%d7
|
||||
and.b %d3,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a0)
|
||||
|
||||
| Plane 1
|
||||
move.b (%a1),%d6
|
||||
and.b %d2,%d6
|
||||
move.b SP_FB1(%sp),%d5
|
||||
move.b %d5,%d7
|
||||
and.b %d1,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a1)+
|
||||
move.b (%a1),%d6
|
||||
and.b %d4,%d6
|
||||
move.b %d5,%d7
|
||||
and.b %d3,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a1)
|
||||
|
||||
| Plane 2
|
||||
move.b (%a2),%d6
|
||||
and.b %d2,%d6
|
||||
move.b SP_FB2(%sp),%d5
|
||||
move.b %d5,%d7
|
||||
and.b %d1,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a2)+
|
||||
move.b (%a2),%d6
|
||||
and.b %d4,%d6
|
||||
move.b %d5,%d7
|
||||
and.b %d3,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a2)
|
||||
|
||||
| Plane 3
|
||||
move.b (%a3),%d6
|
||||
and.b %d2,%d6
|
||||
move.b SP_FB3(%sp),%d5
|
||||
move.b %d5,%d7
|
||||
and.b %d1,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a3)+
|
||||
move.b (%a3),%d6
|
||||
and.b %d4,%d6
|
||||
move.b %d5,%d7
|
||||
and.b %d3,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a3)
|
||||
|
||||
movem.l (%sp)+,%d2-%d7/%a2-%a3
|
||||
rts
|
||||
|
|
@ -179,6 +179,26 @@ bool jlpAudioInit(void) {
|
|||
}
|
||||
|
||||
|
||||
// OPM noise-frequency codes for jlAudioNoise's pitch 0..31, picked so the
|
||||
// generator runs at the rate every other port gives that pitch:
|
||||
// rate = 26000 - pitch * 750, the curve src/iigs/audio_full.c defines and
|
||||
// src/amiga/audio.c follows. The YM2151's generator ticks at
|
||||
// clock / (32 * (32 - NFRQ)) and the X68000 clocks it at 4 MHz, so
|
||||
// NFRQ = 32 - 125000 / rate. That tracks the curve within a few percent
|
||||
// down to pitch 29; the chip's floor is 3906 Hz (NFRQ 0), so pitches 30
|
||||
// and 31 clamp there rather than reaching 3500 and 2750.
|
||||
//
|
||||
// It replaces a plain `31 - pitch`, which ran the generator from 125 kHz
|
||||
// down to 3906 Hz -- far brighter than the other ports across the whole
|
||||
// middle of the range.
|
||||
static const uint8_t kOpmNoiseFreq[32] = {
|
||||
27, 27, 27, 27, 27, 26, 26, 26,
|
||||
26, 26, 25, 25, 25, 24, 24, 24,
|
||||
23, 23, 22, 21, 21, 20, 19, 18,
|
||||
16, 15, 13, 10, 7, 3, 0, 0
|
||||
};
|
||||
|
||||
|
||||
void jlpAudioNoise(uint8_t pitch, uint8_t atten) {
|
||||
uint8_t slot;
|
||||
|
||||
|
|
@ -194,7 +214,7 @@ void jlpAudioNoise(uint8_t pitch, uint8_t atten) {
|
|||
}
|
||||
// NFRQ is inverted: 0 is the highest pitch, 31 the lowest. jlAudioNoise
|
||||
// takes 0..31 with 0 lowest, matching the other ports, so flip it.
|
||||
opmWrite(OPM_REG_NOISE, (uint8_t)(OPM_NOISE_ENABLE | ((31u - (pitch & 31u)))));
|
||||
opmWrite(OPM_REG_NOISE, (uint8_t)(OPM_NOISE_ENABLE | kOpmNoiseFreq[pitch & 31u]));
|
||||
opmWrite((uint8_t)(OPM_REG_TL + slot), (uint8_t)((atten & 15u) * 4u));
|
||||
opmWrite(OPM_REG_KEYON, (uint8_t)(OPM_NOISE_CHANNEL | OPM_SLOTS_ALL));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,17 @@
|
|||
// circular, giving gapless playback; jlpAudioFrameTick refills the block the
|
||||
// chip is not currently walking.
|
||||
//
|
||||
// NOT YET VERIFIED AUDIBLY. This compiles and the control flow is exercised,
|
||||
// but no waveform has been captured out of the emulator, so treat the encoder
|
||||
// output and the block hand-off as unproven.
|
||||
// VERIFIED AUDIBLY 2026-09-23 by capturing -wavwrite out of MAME while Space
|
||||
// Taxi's attract demo spoke; the captured burst cross-correlates with the
|
||||
// utterances in the game's speech.bin at 1:1. See the port C notes below for
|
||||
// what that turned up, and scripts/verify-x68000-audio.sh for the gate.
|
||||
//
|
||||
// ONE ARTEFACT REMAINS: the decoded signal sits on a wandering DC offset of
|
||||
// about 11% of full scale, so the chip's predictor is not where the encoder
|
||||
// thinks it is. A round trip of this encoder against the chip's exact decode
|
||||
// returns to its start, so the encoder is not the cause; something in the
|
||||
// chain hand-off feeds the chip a stream we did not encode. Inaudible by
|
||||
// itself, but it is a disagreement and it costs headroom.
|
||||
|
||||
#include <string.h>
|
||||
|
||||
|
|
@ -43,8 +51,34 @@
|
|||
// makes it affordable: the SFX+encode path benched at ~17 cycles/sample, so
|
||||
// doubling the rate is ~13% of a 10 MHz 68000 rather than the ~6.6% at 3906.
|
||||
#define MSM6258_RATE 7812
|
||||
// _iocs_adpcmmod / _iocs_adpcmlot mode byte: rate selector low, pan high.
|
||||
// 0x05 = 7.8 kHz, both channels.
|
||||
// The MSM6258's sample rate and panning do not live in the chip at all --
|
||||
// they are four bits of PPI port C, which is why they survive a chain start
|
||||
// and why IOCS is not the only way to set them. Bits 1,0 gate the channels
|
||||
// (0 both, 1 right only, 2 left only, 3 silent) and bits 3,2 pick the
|
||||
// divider on the 4 MHz ADPCM clock: 1024, 768 and 512 for 3906, 5208 and
|
||||
// 7812 Hz (measured: divider 512 gives the 7812 Hz the mixer renders). The upper nibble drives the joystick outputs, so write it back
|
||||
// unchanged.
|
||||
//
|
||||
// _iocs_adpcmlot takes a mode byte meant to carry the same two fields, but
|
||||
// it does not pass them through: measured in MAME, mode 0x05 left port C at
|
||||
// 0x01 and both 0x0C and 0x20 left it at 0x03, which gates the chip off
|
||||
// entirely. So the chain start asks IOCS for a mode it accepts and then
|
||||
// states the rate and pan directly, which is exact and needs no IOCS table.
|
||||
//
|
||||
// This matters: the shipped 0x05 ran every sample this port played at
|
||||
// 3906 Hz -- half speed, an octave low against the 7812 Hz the mixer
|
||||
// renders -- and out of the right speaker only. Measured, not read off a
|
||||
// datasheet: a 48 kHz capture of the attract demo stepped every 12.29
|
||||
// samples instead of 6.14.
|
||||
#define PPI_PORT_C ((volatile uint8_t *)0xE9A005)
|
||||
// DMAC channel 3's memory address register, high and low halves: the chip's
|
||||
// current read position in the chain.
|
||||
#define DMAC3_MAR_HI ((volatile uint16_t *)0xE840CC)
|
||||
#define DMAC3_MAR_LO ((volatile uint16_t *)0xE840CE)
|
||||
#define PPI_ADPCM_BITS 0x0Fu
|
||||
#define ADPCM_PAN_BOTH 0u
|
||||
#define ADPCM_RATE_7812 2u
|
||||
#define ADPCM_PORT_C (uint8_t)((ADPCM_RATE_7812 << 2) | ADPCM_PAN_BOTH)
|
||||
#define ADPCM_MODE 0x05
|
||||
|
||||
// Catch the 3906 Hz mistake at COMPILE time rather than as silence at runtime.
|
||||
|
|
@ -52,11 +86,23 @@
|
|||
// and the only symptom is that MOD playback never starts.
|
||||
typedef char x68kAdpcmRateAboveXmpFloor[(MSM6258_RATE >= XMP_MIN_SRATE) ? 1 : -1];
|
||||
|
||||
// Two blocks of PCM per chain entry. 1024 PCM samples = 512 ADPCM bytes =
|
||||
// ~131 ms at 7812 Hz, so a dropped frame cannot starve the chip.
|
||||
#define PCM_BLOCK 1024
|
||||
// Chain geometry, set by how often the game actually gets here rather than
|
||||
// by a round number. MEASURED on the X68000 across 250 s of Space Taxi: the
|
||||
// audio tick arrives every 50-300 ms in the ordinary case, with a tail out
|
||||
// to 500 ms and a handful of 1-2 s hitches when the game is loading. A chain
|
||||
// shorter than the gap between ticks is replayed by the DMAC, and a replay
|
||||
// is not silence -- it hands the decoder nibbles the encoder never meant, so
|
||||
// the audio repeats a word AND the predictor takes a step it never loses.
|
||||
//
|
||||
// Eight 512-sample blocks is 524 ms of chain, 458 ms of which can sit ahead
|
||||
// of the chip. That covers every gap but the loading hitches, which no
|
||||
// affordable chain would cover. The chain length is also the worst-case
|
||||
// delay before newly mixed audio is heard, but only for a sound that starts
|
||||
// while the chip is already running: one that starts from idle restarts the
|
||||
// chain and is heard at once.
|
||||
#define PCM_BLOCK 512
|
||||
#define ADPCM_BLOCK (PCM_BLOCK / 2)
|
||||
#define BLOCK_COUNT 2
|
||||
#define BLOCK_COUNT 8
|
||||
|
||||
static xmp_context gXmpCtx = NULL;
|
||||
static bool gXmpLoaded = false;
|
||||
|
|
@ -71,7 +117,12 @@ static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS];
|
|||
static uint8_t gMixBuf[PCM_BLOCK];
|
||||
static uint8_t gAdpcm[BLOCK_COUNT][ADPCM_BLOCK];
|
||||
static struct iocs_chain2 gChain[BLOCK_COUNT];
|
||||
static uint8_t gFillBlock = 0;
|
||||
// The next chain block to fill, in playback order. Everything between it
|
||||
// and the block the chip is reading is stale and gets refilled.
|
||||
static uint8_t gNextFill = 0;
|
||||
// Consecutive silent blocks encoded. At BLOCK_COUNT the chain holds nothing
|
||||
// but silence and the chip is stopped -- see jlpAudioPcmFrameTick.
|
||||
static uint8_t gIdleBlocks = 0;
|
||||
// Diagnostics: how many times each stage actually ran. Exposed (non-static) so
|
||||
// a probe can read them without a debugger -- silent no-ops in the refill path
|
||||
// are exactly the failure this port hit.
|
||||
|
|
@ -165,6 +216,22 @@ static void adpcmEncodeBlock(const uint8_t *pcm, uint8_t *out) {
|
|||
}
|
||||
|
||||
|
||||
// Nothing to play: no module running and every SFX slot finished.
|
||||
static bool pcmIdle(void) {
|
||||
uint8_t s;
|
||||
|
||||
if (gXmpStarted) {
|
||||
return false;
|
||||
}
|
||||
for (s = 0u; s < JOEY_AUDIO_SFX_SLOTS; s++) {
|
||||
if (gSfxSlots[s].active) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Render one block: module (if playing) then the SFX overlay on top.
|
||||
static void renderBlock(uint8_t *dst) {
|
||||
if (gXmpStarted) {
|
||||
|
|
@ -176,6 +243,31 @@ static void renderBlock(uint8_t *dst) {
|
|||
}
|
||||
|
||||
|
||||
// Which chain block is the chip reading? DMAC channel 3 feeds it, and that
|
||||
// channel's memory address register is the read pointer, so the answer is
|
||||
// exact and costs two register reads. Returns BLOCK_COUNT when the pointer
|
||||
// is not in the chain at all, which is how a caller learns the chain is not
|
||||
// running rather than acting on a garbage index.
|
||||
static uint8_t adpcmPlayingBlock(void) {
|
||||
uint32_t mar = ((uint32_t)*DMAC3_MAR_HI << 16) | (uint32_t)*DMAC3_MAR_LO;
|
||||
uint32_t offset = mar - (uint32_t)&gAdpcm[0][0];
|
||||
|
||||
if (offset >= (uint32_t)(BLOCK_COUNT * ADPCM_BLOCK)) {
|
||||
return BLOCK_COUNT; // not in the chain: say so, do nothing
|
||||
}
|
||||
return (uint8_t)(offset / ADPCM_BLOCK);
|
||||
}
|
||||
|
||||
|
||||
// State the ADPCM rate and panning in the hardware register that holds
|
||||
// them, rather than hoping IOCS reads the same meaning out of a mode byte.
|
||||
static void adpcmSetRateAndPan(void) {
|
||||
uint8_t portC = *PPI_PORT_C;
|
||||
|
||||
*PPI_PORT_C = (uint8_t)((portC & (uint8_t)~PPI_ADPCM_BITS) | ADPCM_PORT_C);
|
||||
}
|
||||
|
||||
|
||||
static void startChain(void) {
|
||||
uint8_t b;
|
||||
|
||||
|
|
@ -186,8 +278,13 @@ static void startChain(void) {
|
|||
gChain[b].len = ADPCM_BLOCK;
|
||||
gChain[b].next = &gChain[(b + 1u) % BLOCK_COUNT]; // circular
|
||||
}
|
||||
gFillBlock = 0u;
|
||||
// The chip's decoder starts from a known state, so the encoder that has
|
||||
// to mirror it must too, or the two are offset for the whole run.
|
||||
gEncPredictor = 0;
|
||||
gEncStepIndex = 0;
|
||||
gNextFill = 0u; // the chain starts playing block 0
|
||||
_iocs_adpcmlot(&gChain[0], ADPCM_MODE);
|
||||
adpcmSetRateAndPan(); // AFTER the IOCS call, which sets them wrong
|
||||
gPlaying = true;
|
||||
gX68kPcmPlaying = 1u;
|
||||
gX68kPcmChainStarts++;
|
||||
|
|
@ -288,17 +385,47 @@ void jlpAudioStopSfx(uint8_t slot) {
|
|||
}
|
||||
|
||||
|
||||
// Refill one block per frame. At 3906 Hz a block is ~262 ms and there are two,
|
||||
// so the chip has roughly half a second of audio queued -- ample margin over a
|
||||
// 55 Hz frame tick even if several frames are missed.
|
||||
// Top the chain back up behind the chip, however many blocks that takes.
|
||||
// Refilling a fixed number of blocks per call was the bug: tie production to
|
||||
// the caller's rate and it is wrong at both ends -- too fast on a title
|
||||
// screen at 31 Hz, where it overwrote the block being read, and too slow in
|
||||
// a ride at 6 Hz, where the DMAC lapped it and replayed stale audio. The
|
||||
// chip's own read pointer is the only honest clock, so ask it.
|
||||
void jlpAudioPcmFrameTick(void) {
|
||||
uint8_t playing;
|
||||
|
||||
if (!gPcmReady || !gPlaying) {
|
||||
return;
|
||||
}
|
||||
playing = adpcmPlayingBlock();
|
||||
if (playing >= BLOCK_COUNT) {
|
||||
return; // the chain is not running yet
|
||||
}
|
||||
// Everything from gNextFill up to the block being played is behind the
|
||||
// chip and safe to rewrite; the block being played is not.
|
||||
while (gNextFill != playing) {
|
||||
renderBlock(gMixBuf);
|
||||
adpcmEncodeBlock(gMixBuf, gAdpcm[gFillBlock]);
|
||||
gFillBlock = (uint8_t)((gFillBlock + 1u) % BLOCK_COUNT);
|
||||
adpcmEncodeBlock(gMixBuf, gAdpcm[gNextFill]);
|
||||
gNextFill = (uint8_t)((gNextFill + 1u) % BLOCK_COUNT);
|
||||
gX68kPcmRefills++;
|
||||
if (!pcmIdle()) {
|
||||
gIdleBlocks = 0u;
|
||||
} else if (gIdleBlocks < BLOCK_COUNT) {
|
||||
gIdleBlocks++;
|
||||
}
|
||||
}
|
||||
// A whole chain of silence: stop the chip rather than keep a DMA and a
|
||||
// 4-bit encoder busy saying nothing. Silence is not free here -- ADPCM
|
||||
// cannot encode "no change", so the smallest step still dithers, and any
|
||||
// slip between the chip's clock and the DMA leaves the decoder sitting
|
||||
// on a DC offset that only grows. Stopping resets both sides, and the
|
||||
// next sound restarts the chain and is heard immediately.
|
||||
if (gIdleBlocks >= BLOCK_COUNT) {
|
||||
_iocs_adpcmmod(0);
|
||||
gPlaying = false;
|
||||
gIdleBlocks = 0u;
|
||||
gX68kPcmPlaying = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
167
src/x68000/hal.c
167
src/x68000/hal.c
|
|
@ -123,6 +123,12 @@
|
|||
#define X68K_MFP_CLOCK_HZ 4000000ul
|
||||
#define X68K_TCDCR_C_SHIFT 4u
|
||||
#define X68K_TCDCR_C_MASK 0x07u
|
||||
// Display frames to count the tick against at init. Sixteen is ~290 ms, long
|
||||
// enough that one frame of slop is under a percent.
|
||||
#define X68K_TICK_CAL_FRAMES 16u
|
||||
// Spin bound for that calibration: comfortably more polls than sixteen frames
|
||||
// need, so only a machine that is not going to toggle V-DISP ever hits it.
|
||||
#define X68K_TICK_CAL_GUARD 4000000ul
|
||||
|
||||
// ----- Module state ---------------------------------------------------------
|
||||
|
||||
|
|
@ -154,6 +160,8 @@ static uint32_t tickHzFromMfp(void);
|
|||
uint32_t jlpMillisElapsed(void);
|
||||
uint16_t jlpFrameHz(void);
|
||||
static void buildSpreadTables(void);
|
||||
static void spreadSetPalBase(uint8_t palBase);
|
||||
static void calibrateTickHz(void);
|
||||
static void installTickIsr(void);
|
||||
|
||||
static void uploadGfxPalette(const jlSurfaceT *src);
|
||||
|
|
@ -289,6 +297,64 @@ static void installTickIsr(void) {
|
|||
x68kTickChainAddr = *vec;
|
||||
*vec = (uint32_t)x68kTickIsr;
|
||||
gTickHooked = true;
|
||||
calibrateTickHz();
|
||||
}
|
||||
|
||||
|
||||
// Count real ticks across a known number of display frames and believe THAT
|
||||
// rather than the MFP registers.
|
||||
//
|
||||
// Reading the prescale and data registers looks exact and is not: two runs of
|
||||
// the same benchmark reported 50 s and 116 s of elapsed time for runs that
|
||||
// really took 118 s and 100 s, a 2.7x disagreement between them, because the
|
||||
// rate Human68k leaves Timer C at is not the rate those registers describe by
|
||||
// the time a program reads them. Everything derived from the clock was wrong
|
||||
// by that factor, which is how a performance change came back looking three
|
||||
// times slower than the code it replaced.
|
||||
//
|
||||
// The display frame is the one rate on this machine that cannot drift: it
|
||||
// comes from the CRT mode. Sixteen frames is about 290 ms of init.
|
||||
static void calibrateTickHz(void) {
|
||||
uint8_t last;
|
||||
uint16_t frames = 0u;
|
||||
uint32_t start;
|
||||
uint32_t ticks;
|
||||
uint32_t guard;
|
||||
|
||||
// Every spin is bounded. A machine where V-DISP never toggles would
|
||||
// otherwise hang here in jlInit, which is far worse than a clock that is
|
||||
// merely wrong: the guard gives up and keeps the register-derived rate.
|
||||
last = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u);
|
||||
guard = X68K_TICK_CAL_GUARD;
|
||||
while ((uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u) == last) {
|
||||
if (--guard == 0ul) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
last = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u);
|
||||
start = gX68kTicks;
|
||||
guard = X68K_TICK_CAL_GUARD;
|
||||
while (frames < X68K_TICK_CAL_FRAMES) {
|
||||
uint8_t now = (uint8_t)((*X68K_MFP_GPIP & X68K_GPIP_VDISP) != 0u);
|
||||
|
||||
if (now != last) {
|
||||
last = now;
|
||||
if (now != 0u) {
|
||||
frames++;
|
||||
}
|
||||
}
|
||||
if (--guard == 0ul) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
ticks = gX68kTicks - start;
|
||||
if (ticks == 0ul) {
|
||||
return; // ISR never fired: keep the guess
|
||||
}
|
||||
gTickHz = (ticks * (uint32_t)jlpFrameHz()) / (uint32_t)X68K_TICK_CAL_FRAMES;
|
||||
if (gTickHz == 0ul) {
|
||||
gTickHz = tickHzFromMfp();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -332,9 +398,21 @@ static void uploadGfxPalette(const jlSurfaceT *src) {
|
|||
// 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
|
||||
// One table entry, addressed as a byte displacement from a plane base.
|
||||
#define SPREAD_AT(base, off) (*(const uint32_t *)((base) + (off)))
|
||||
|
||||
static uint32_t gSpreadHi[X68K_BITPLANES][X68K_SPREAD_NIBBLES];
|
||||
static uint32_t gSpreadLo[X68K_BITPLANES][X68K_SPREAD_NIBBLES];
|
||||
// One table, laid out so the expansion loop can reach it with the 68000's
|
||||
// (d8,An,Xn) mode and nothing else: [plane][nibble] holds the pair of output
|
||||
// longs (pixels 0-1, then 2-3) that plane contributes, so ONE index serves
|
||||
// two stores. A plane is 16 nibbles x 8 bytes = 128 bytes, which is exactly
|
||||
// the reach of the signed 8-bit displacement, so two base pointers parked on
|
||||
// planes 1 and 3 address all four planes at -128 and 0 with no arithmetic.
|
||||
//
|
||||
// Plane 0's entries carry the row's palette base as well. Folding it in there
|
||||
// costs 16 stores when the SCB changes and saves an OR on every output long:
|
||||
// the inner loop just ORs four table reads and stores.
|
||||
static uint32_t gSpreadPair[X68K_BITPLANES][X68K_SPREAD_NIBBLES][2];
|
||||
static uint8_t gSpreadPalBase = 0u;
|
||||
static bool gSpreadBuilt = false;
|
||||
|
||||
|
||||
|
|
@ -348,16 +426,39 @@ static void buildSpreadTables(void) {
|
|||
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)
|
||||
gSpreadPair[plane][nib][0] = (((nib >> 3) & 1u) ? (bit << 16) : 0ul)
|
||||
| (((nib >> 2) & 1u) ? bit : 0ul);
|
||||
gSpreadLo[plane][nib] = (((nib >> 1) & 1u) ? (bit << 16) : 0ul)
|
||||
gSpreadPair[plane][nib][1] = (((nib >> 1) & 1u) ? (bit << 16) : 0ul)
|
||||
| (((nib >> 0) & 1u) ? bit : 0ul);
|
||||
}
|
||||
}
|
||||
gSpreadPalBase = 0u;
|
||||
gSpreadBuilt = true;
|
||||
}
|
||||
|
||||
|
||||
// Re-fold the row's palette base into plane 0's entries. Sixteen pairs, and
|
||||
// only when the SCB actually changes from one row to the next -- which for a
|
||||
// screen with one palette means once per present.
|
||||
static void spreadSetPalBase(uint8_t palBase) {
|
||||
uint32_t pal2;
|
||||
uint16_t nib;
|
||||
|
||||
if (palBase == gSpreadPalBase) {
|
||||
return;
|
||||
}
|
||||
pal2 = ((uint32_t)palBase << 16) | (uint32_t)palBase;
|
||||
for (nib = 0u; nib < X68K_SPREAD_NIBBLES; nib++) {
|
||||
uint32_t bare0 = (((nib >> 3) & 1u) ? 0x10000ul : 0ul) | (((nib >> 2) & 1u) ? 1ul : 0ul);
|
||||
uint32_t bare1 = (((nib >> 1) & 1u) ? 0x10000ul : 0ul) | (((nib >> 0) & 1u) ? 1ul : 0ul);
|
||||
|
||||
gSpreadPair[0][nib][0] = bare0 | pal2;
|
||||
gSpreadPair[0][nib][1] = bare1 | pal2;
|
||||
}
|
||||
gSpreadPalBase = palBase;
|
||||
}
|
||||
|
||||
|
||||
// ----- Present --------------------------------------------------------------
|
||||
|
||||
// Copy the surface's four planes into the TEXT PLANE at $E00000.
|
||||
|
|
@ -381,10 +482,8 @@ void jlpPresent(const jlSurfaceT *src) {
|
|||
uint16_t y;
|
||||
uint16_t firstByte;
|
||||
uint16_t rowBytes;
|
||||
uint32_t pal2;
|
||||
const uint8_t *sp;
|
||||
uint16_t b;
|
||||
uint16_t hi0, hi1, hi2, hi3;
|
||||
uint16_t lo0, lo1, lo2, lo3;
|
||||
uint8_t palBase;
|
||||
uint16_t blitRows = 0;
|
||||
|
||||
|
|
@ -406,6 +505,7 @@ void jlpPresent(const jlSurfaceT *src) {
|
|||
if (pd == NULL) {
|
||||
return;
|
||||
}
|
||||
sp = (const uint8_t *)&gSpreadPair[0][0][0];
|
||||
x68kSerialPump(); // the palette upload + spread-table build above are already line time
|
||||
for (y = 0; y < SURFACE_HEIGHT; y++) {
|
||||
if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) {
|
||||
|
|
@ -425,8 +525,10 @@ void jlpPresent(const jlSurfaceT *src) {
|
|||
rowBytes = (uint16_t)(X68K_BYTES_PER_ROW - firstByte);
|
||||
}
|
||||
// This row's band selects the high nibble of every pixel byte -- the
|
||||
// whole SCB mechanism, exactly src/dos/hal.c:83.
|
||||
// whole SCB mechanism, exactly src/dos/hal.c:83. It rides in plane 0's
|
||||
// table rather than in an OR per output long.
|
||||
palBase = (uint8_t)(src->scb[y] << 4);
|
||||
spreadSetPalBase(palBase);
|
||||
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;
|
||||
|
|
@ -438,25 +540,38 @@ void jlpPresent(const jlSurfaceT *src) {
|
|||
+ (((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);
|
||||
// Four long stores per eight pixels; each long is two GVRAM words.
|
||||
//
|
||||
// The index is a BYTE offset into the table, never an array
|
||||
// subscript: (v >> 1) & 0x78 is the high nibble times the 8-byte
|
||||
// entry size and (v << 3) & 0x78 is the low one, so the compiler has
|
||||
// nothing left to scale. The plane is ORed into the same index --
|
||||
// 0x78 leaves bit 7 upwards free -- which keeps ONE table base live
|
||||
// in an address register. Handing the compiler a base per plane
|
||||
// instead made it worse: it folded each into an absolute and then
|
||||
// reloaded it with an LEA before every single lookup.
|
||||
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;
|
||||
uint8_t v0 = *p0++;
|
||||
uint8_t v1 = *p1++;
|
||||
uint8_t v2 = *p2++;
|
||||
uint8_t v3 = *p3++;
|
||||
uint16_t o0 = (uint16_t)((v0 >> 1) & 0x78u);
|
||||
uint16_t o1 = (uint16_t)((v1 >> 1) & 0x78u);
|
||||
uint16_t o2 = (uint16_t)((v2 >> 1) & 0x78u);
|
||||
uint16_t o3 = (uint16_t)((v3 >> 1) & 0x78u);
|
||||
|
||||
*wordDst++ = SPREAD_AT(sp, o0) | SPREAD_AT(sp, o1 | 128)
|
||||
| SPREAD_AT(sp, o2 | 256) | SPREAD_AT(sp, o3 | 384);
|
||||
*wordDst++ = SPREAD_AT(sp, o0 + 4) | SPREAD_AT(sp, (o1 | 128) + 4)
|
||||
| SPREAD_AT(sp, (o2 | 256) + 4) | SPREAD_AT(sp, (o3 | 384) + 4);
|
||||
o0 = (uint16_t)((v0 << 3) & 0x78u);
|
||||
o1 = (uint16_t)((v1 << 3) & 0x78u);
|
||||
o2 = (uint16_t)((v2 << 3) & 0x78u);
|
||||
o3 = (uint16_t)((v3 << 3) & 0x78u);
|
||||
*wordDst++ = SPREAD_AT(sp, o0) | SPREAD_AT(sp, o1 | 128)
|
||||
| SPREAD_AT(sp, o2 | 256) | SPREAD_AT(sp, o3 | 384);
|
||||
*wordDst++ = SPREAD_AT(sp, o0 + 4) | SPREAD_AT(sp, (o1 | 128) + 4)
|
||||
| SPREAD_AT(sp, (o2 | 256) + 4) | SPREAD_AT(sp, (o3 | 384) + 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,6 @@
|
|||
#include "spriteInternal.h"
|
||||
#include "x68kPlanar.h"
|
||||
|
||||
// Blitter first/last-word mask "all bits" -- the CPU fill takes the same value.
|
||||
#define X68K_MASK_ALL 0xFFFFu
|
||||
|
||||
// The Amiga picks between blitter and CPU walk at 16 per-plane words because
|
||||
// the blitter's fixed setup loses below that. Here BOTH paths are the CPU, and
|
||||
// the memcpy path wins at essentially any size (it is one call per row instead
|
||||
|
|
@ -29,97 +26,27 @@
|
|||
#define X68K_COPYRECT_MEMCPY_MIN_WORDS 0u
|
||||
|
||||
|
||||
|
||||
// ----- C stand-ins for the shared 68k asm -----
|
||||
// ----- Hand-written assembly -----
|
||||
//
|
||||
// src/m68k/surface68k.s and src/amiga/circle.s export these, but their symbols
|
||||
// carry the m68k-amigaos/mint leading underscore (_surface68kFillSpan4Planes)
|
||||
// and elf2x68k is ELF, which has no such prefix. Rather than fork the asm for
|
||||
// an ABI difference, the port uses C for now; adopting the asm later is a
|
||||
// symbol-aliasing change, not a rewrite. These are the obvious optimisation
|
||||
// targets once correctness is banked.
|
||||
// The area fills live in src/m68k/planarFill.s, shared with the Amiga: both
|
||||
// ports keep four separate plane buffers of 40 bytes per row, so a span of
|
||||
// pixels is the same bytes in the same places. (It carries both symbol
|
||||
// spellings because gcc-amigaos prepends an underscore to C names and
|
||||
// elf2x68k does not.) The circle OUTLINE is still per-port asm, one file
|
||||
// each. The pixel walkers are shared too, in src/m68k/planarPlot.s.
|
||||
|
||||
// Fill one horizontal span across all four planes: a partial left byte, a run
|
||||
// of whole middle bytes, then a partial right byte. fbN is the fill byte for
|
||||
// plane N (0x00 or 0xFF), so a colour is applied by writing each plane's bit.
|
||||
static void surface68kFillSpan4Planes(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3,
|
||||
uint16_t numMid, uint8_t leftMask, uint8_t rightMask,
|
||||
uint8_t fb0, uint8_t fb1, uint8_t fb2, uint8_t fb3) {
|
||||
uint8_t *pl[X68K_BITPLANES];
|
||||
uint8_t fb[X68K_BITPLANES];
|
||||
uint16_t i;
|
||||
uint16_t m;
|
||||
|
||||
pl[0] = p0; pl[1] = p1; pl[2] = p2; pl[3] = p3;
|
||||
fb[0] = fb0; fb[1] = fb1; fb[2] = fb2; fb[3] = fb3;
|
||||
for (i = 0u; i < X68K_BITPLANES; i++) {
|
||||
uint8_t *d = pl[i];
|
||||
if (leftMask != 0u) {
|
||||
*d = (uint8_t)((*d & (uint8_t)~leftMask) | (fb[i] & leftMask));
|
||||
}
|
||||
d++;
|
||||
for (m = 0u; m < numMid; m++) {
|
||||
*d++ = fb[i];
|
||||
}
|
||||
if (rightMask != 0u) {
|
||||
*d = (uint8_t)((*d & (uint8_t)~rightMask) | (fb[i] & rightMask));
|
||||
}
|
||||
}
|
||||
}
|
||||
extern void surface68kFillCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color);
|
||||
extern void surface68kFillRect(uint8_t *const *planes, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color);
|
||||
|
||||
|
||||
// Bresenham midpoint circle outline, plotted straight into the planes. Caller
|
||||
// has verified the whole bbox is on-surface, so there is no per-pixel clip.
|
||||
static void x68kPlotPlanes(uint8_t *pl[X68K_BITPLANES], uint16_t x, uint16_t y, uint8_t color) {
|
||||
uint32_t off;
|
||||
uint8_t bit;
|
||||
uint16_t i;
|
||||
|
||||
off = ((uint32_t)y * X68K_BYTES_PER_ROW) + (uint32_t)(x >> 3);
|
||||
bit = (uint8_t)(0x80u >> (x & 7u));
|
||||
for (i = 0u; i < X68K_BITPLANES; i++) {
|
||||
if ((color >> i) & 1u) {
|
||||
pl[i][off] |= bit;
|
||||
} else {
|
||||
pl[i][off] = (uint8_t)(pl[i][off] & (uint8_t)~bit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void surface68kAmigaCircleOutline(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3,
|
||||
uint16_t cx, uint16_t cy, uint16_t r, uint8_t color) {
|
||||
uint8_t *pl[X68K_BITPLANES];
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t d;
|
||||
|
||||
pl[0] = p0; pl[1] = p1; pl[2] = p2; pl[3] = p3;
|
||||
x = 0;
|
||||
y = (int16_t)r;
|
||||
d = (int16_t)(1 - (int16_t)r);
|
||||
while (x <= y) {
|
||||
x68kPlotPlanes(pl, (uint16_t)(cx + x), (uint16_t)(cy + y), color);
|
||||
x68kPlotPlanes(pl, (uint16_t)(cx - x), (uint16_t)(cy + y), color);
|
||||
x68kPlotPlanes(pl, (uint16_t)(cx + x), (uint16_t)(cy - y), color);
|
||||
x68kPlotPlanes(pl, (uint16_t)(cx - x), (uint16_t)(cy - y), color);
|
||||
x68kPlotPlanes(pl, (uint16_t)(cx + y), (uint16_t)(cy + x), color);
|
||||
x68kPlotPlanes(pl, (uint16_t)(cx - y), (uint16_t)(cy + x), color);
|
||||
x68kPlotPlanes(pl, (uint16_t)(cx + y), (uint16_t)(cy - x), color);
|
||||
x68kPlotPlanes(pl, (uint16_t)(cx - y), (uint16_t)(cy - x), color);
|
||||
// The tie d == 0 must take the NO-SHRINK branch. src/core/draw.c:428
|
||||
// uses `err <= 0` and src/amiga/circle.s:149 uses tst.w/bgt (shrink
|
||||
// only when err > 0); `d < 0` shrinks the major axis on the tie and
|
||||
// plots a different pixel set for 114 of the 199 reachable radii.
|
||||
if (d <= 0) {
|
||||
d = (int16_t)(d + (2 * x) + 3);
|
||||
} else {
|
||||
d = (int16_t)(d + (2 * (x - y)) + 5);
|
||||
y--;
|
||||
}
|
||||
x++;
|
||||
}
|
||||
}
|
||||
// circle.s
|
||||
// src/m68k/planarPlot.s, shared with the Amiga: Bresenham walkers with 16-way
|
||||
// colour dispatch, so every plane write is one memory read-modify-write with
|
||||
// the set-or-clear choice already made. Both are bit-identical to draw.c's own
|
||||
// walkers. Straight lines never reach jlpDrawLine -- draw.c diverts those to
|
||||
// jlFillRect -- so the line body walks the general diagonal case only.
|
||||
extern void surface68kPlanarCircle(uint8_t *const *planes, uint16_t cx, uint16_t cy, uint16_t r, uint16_t color);
|
||||
extern void surface68kPlanarLine(uint8_t *const *planes, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color);
|
||||
|
||||
|
||||
// ----- CPU stand-ins for the Amiga blitter helpers -----
|
||||
|
|
@ -129,56 +56,37 @@ static void surface68kAmigaCircleOutline(uint8_t *p0, uint8_t *p1, uint8_t *p2,
|
|||
// after each row (0 for a full-stride operation).
|
||||
|
||||
// DEST-only block fill: `height` rows of `widthWords` words set or cleared.
|
||||
static void x68kCpuFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height,
|
||||
uint16_t dstModBytes, bool setBits) {
|
||||
uint16_t row;
|
||||
uint16_t w;
|
||||
uint16_t *p;
|
||||
uint16_t val;
|
||||
// Fill a run of 16-bit words with one value, in longs where it can. Marked
|
||||
// always_inline on purpose: the masked fill calls it once PER ROW, and at
|
||||
// 16x16 a row's middle is a word or two, where a real call costs more than
|
||||
// the stores it saves. Measured: routing that middle through a function made
|
||||
// jlFillRect 16x16 run at 0.45x of the word loop it replaced.
|
||||
static inline __attribute__((always_inline))
|
||||
void x68kFillWordRun(uint8_t *d, uint16_t words, uint32_t val32, uint16_t val16) {
|
||||
uint32_t *lp;
|
||||
uint16_t longs;
|
||||
|
||||
val = setBits ? 0xFFFFu : 0x0000u;
|
||||
for (row = 0u; row < height; row++) {
|
||||
p = (uint16_t *)dst;
|
||||
for (w = 0u; w < widthWords; w++) {
|
||||
*p++ = val;
|
||||
if (words != 0u && ((uintptr_t)d & 2u) != 0u) {
|
||||
*(uint16_t *)d = val16;
|
||||
d += 2;
|
||||
words--;
|
||||
}
|
||||
dst += (widthWords * 2u) + dstModBytes;
|
||||
lp = (uint32_t *)d;
|
||||
longs = (uint16_t)(words >> 1);
|
||||
while (longs >= 4u) {
|
||||
lp[0] = val32;
|
||||
lp[1] = val32;
|
||||
lp[2] = val32;
|
||||
lp[3] = val32;
|
||||
lp += 4;
|
||||
longs = (uint16_t)(longs - 4u);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Masked fill: as above, but the first and last words are merged through
|
||||
// `fwm`/`lwm` so a rect can start and end mid-word. The Amiga does this with
|
||||
// the blitter's first/last-word masks; here it is an explicit read-modify-write
|
||||
// on just those two words, with the interior written whole.
|
||||
static void x68kCpuMaskedFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height,
|
||||
uint16_t dstModBytes, uint16_t fwm, uint16_t lwm,
|
||||
bool setBits) {
|
||||
uint16_t row;
|
||||
uint16_t w;
|
||||
uint16_t *p;
|
||||
uint16_t val;
|
||||
uint16_t mask;
|
||||
|
||||
val = setBits ? 0xFFFFu : 0x0000u;
|
||||
for (row = 0u; row < height; row++) {
|
||||
p = (uint16_t *)dst;
|
||||
for (w = 0u; w < widthWords; w++) {
|
||||
mask = 0xFFFFu;
|
||||
if (w == 0u) {
|
||||
mask &= fwm;
|
||||
while (longs != 0u) {
|
||||
*lp++ = val32;
|
||||
longs--;
|
||||
}
|
||||
if (w == (uint16_t)(widthWords - 1u)) {
|
||||
mask &= lwm;
|
||||
}
|
||||
if (mask == 0xFFFFu) {
|
||||
*p = val;
|
||||
} else {
|
||||
*p = (uint16_t)((*p & (uint16_t)~mask) | (val & mask));
|
||||
}
|
||||
p++;
|
||||
}
|
||||
dst += (widthWords * 2u) + dstModBytes;
|
||||
if ((words & 1u) != 0u) {
|
||||
*(uint16_t *)lp = val16;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,7 +138,10 @@ void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
|
|||
|
||||
for (i = 0; i < X68K_BITPLANES; i++) {
|
||||
setBits = ((colorIndex >> i) & 1u) != 0u;
|
||||
x68kCpuFillOne(pd->planes[i], (X68K_BYTES_PER_ROW / 2u), SURFACE_HEIGHT, 0u, setBits);
|
||||
// A whole plane is 200 contiguous rows of 20 words: one flat run.
|
||||
x68kFillWordRun(pd->planes[i], (uint16_t)((X68K_BYTES_PER_ROW / 2u) * SURFACE_HEIGHT),
|
||||
setBits ? 0xFFFFFFFFul : 0x00000000ul,
|
||||
setBits ? 0xFFFFu : 0x0000u);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -251,80 +162,39 @@ void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
|
|||
// bits AND its complement (see the two masked-fill minterms).
|
||||
void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
|
||||
X68kPlanarT *pd;
|
||||
uint16_t wordFirst;
|
||||
uint16_t wordLast;
|
||||
uint16_t widthWords;
|
||||
uint16_t fwm;
|
||||
uint16_t lwm;
|
||||
uint16_t dstMod;
|
||||
uint16_t plane;
|
||||
uint8_t bitVal;
|
||||
uint8_t *dst;
|
||||
|
||||
// Callers guarantee s != NULL and positive w/h: the jlFillRect
|
||||
// wrapper validates + clips before fillRectOnSurface, and the
|
||||
// fill-circle span loop emits spanWidth = 2x+1 >= 1 with h = 1.
|
||||
// Callers guarantee s != NULL and positive w/h: the jlFillRect wrapper
|
||||
// validates and clips before fillRectOnSurface.
|
||||
pd = (X68kPlanarT *)s->portData;
|
||||
if (pd == NULL) {
|
||||
jlpGenericFillRect(s, x, y, w, h, colorIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// Word-granular rect geometry. wordFirst/wordLast index the 16-pixel
|
||||
// plane words the rect touches; fwm/lwm are the blitter first/last-
|
||||
// word masks that cut the partial-pixel edges (pixel 0 is bit 15 of
|
||||
// its word, so the left mask drops the high bits of the first word
|
||||
// and the right mask drops the low bits of the last). One blit per
|
||||
// plane then fills any width, so there is no byte-aligned or
|
||||
// per-pixel-edge special case to follow.
|
||||
wordFirst = (uint16_t)((uint16_t)x >> 4);
|
||||
wordLast = (uint16_t)(((uint16_t)x + w - 1u) >> 4);
|
||||
widthWords = (uint16_t)(wordLast - wordFirst + 1u);
|
||||
fwm = (uint16_t)(X68K_MASK_ALL >> ((uint16_t)x & 15u));
|
||||
lwm = (uint16_t)(X68K_MASK_ALL << (15u - (((uint16_t)x + w - 1u) & 15u)));
|
||||
dstMod = (uint16_t)(X68K_BYTES_PER_ROW - (widthWords << 1));
|
||||
// Full-width rows are contiguous within a plane, so a full-width rect is
|
||||
// ONE flat run of words per plane instead of h separately masked ones --
|
||||
// about three cycles a byte against the general path's six. Every
|
||||
// full-screen wipe is this shape, so it keeps its own case.
|
||||
if (x == 0 && w == (int16_t)SURFACE_WIDTH) {
|
||||
uint16_t plane;
|
||||
|
||||
/* Full-width fast path: the rect spans every plane word with both
|
||||
* edge masks all-ones, so each plane row is a whole 20-word store
|
||||
* with no destination read. jlFillRect 320x200 (full-screen wipes)
|
||||
* is the dominant case and lands here. DEST-only block fill --
|
||||
* (h << 6) | 20 words, BLTDMOD = 0 because consecutive full-width
|
||||
* rows are contiguous; set bits write $FFFF (minterm $FF), clear
|
||||
* bits $0000 (minterm $00). One OwnBlitter for all four planes;
|
||||
* CPU fill completes synchronously, so the planes are ready before
|
||||
* the present copy or any later CPU draw reads them. */
|
||||
if (wordFirst == 0u && wordLast == (uint16_t)((X68K_BYTES_PER_ROW / 2u) - 1u)
|
||||
&& fwm == X68K_MASK_ALL && lwm == X68K_MASK_ALL) {
|
||||
uint8_t *rowBase;
|
||||
for (plane = 0u; plane < X68K_BITPLANES; plane++) {
|
||||
uint8_t bitVal = (uint8_t)((colorIndex >> plane) & 1u);
|
||||
uint8_t *rowBase = pd->planes[plane] + (uint16_t)y * X68K_BYTES_PER_ROW;
|
||||
|
||||
for (plane = 0; plane < X68K_BITPLANES; plane++) {
|
||||
bitVal = (uint8_t)((colorIndex >> plane) & 1u);
|
||||
rowBase = pd->planes[plane] + (uint16_t)y * X68K_BYTES_PER_ROW;
|
||||
x68kCpuFillOne(rowBase, (X68K_BYTES_PER_ROW / 2u), (uint16_t)h, 0u, bitVal != 0u);
|
||||
x68kFillWordRun(rowBase, (uint16_t)((X68K_BYTES_PER_ROW / 2u) * (uint16_t)h),
|
||||
bitVal ? 0xFFFFFFFFul : 0x00000000ul,
|
||||
bitVal ? 0xFFFFu : 0x0000u);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/* General masked fill: A is the constant $FFFF cut to the rect by
|
||||
* the fwm/lwm word masks, C reads and D writes the destination so
|
||||
* bits outside the mask survive. One blit per plane covers any
|
||||
* width, partial pixel edges included -- the blitter does the edge
|
||||
* masking in hardware, replacing the old per-pixel-edge CPU walkers.
|
||||
* UBER jlFillRect 16x16 / 80x80 lands here. (jlFillCircle spans
|
||||
* left this path in hunt-2 rank 14: the exact-disk walk feeds
|
||||
* x68kFillSpanInline directly, CPU-side.) widthWords == 1 needs no
|
||||
* special case: the blitter ANDs both edge masks onto the single
|
||||
* word. */
|
||||
|
||||
for (plane = 0; plane < X68K_BITPLANES; plane++) {
|
||||
bitVal = (uint8_t)((colorIndex >> plane) & 1u);
|
||||
dst = pd->planes[plane]
|
||||
+ (uint16_t)y * X68K_BYTES_PER_ROW
|
||||
+ (uint16_t)(wordFirst << 1);
|
||||
x68kCpuMaskedFillOne(dst, widthWords, (uint16_t)h, dstMod, fwm, lwm, bitVal != 0u);
|
||||
}
|
||||
|
||||
// Everything else goes to planarFill.s, which shares its span body with
|
||||
// the filled circle: masked end bytes, long stores between them, and the
|
||||
// set-or-clear decision made once per colour instead of per plane per row.
|
||||
// That includes one-pixel-wide rects, which is what draw.c turns a
|
||||
// vertical line into -- the asm's single-byte case covers them.
|
||||
surface68kFillRect(pd->planes, (uint16_t)x, (uint16_t)y, (uint16_t)w, (uint16_t)h, colorIndex);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -345,6 +215,93 @@ void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint
|
|||
// pixel's 4bpp color is assembled from / written to the 4 plane bits
|
||||
// at byte x>>3, bit 0x80 >> (x & 7).
|
||||
|
||||
// Chunky pixel PAIR -> one 2-bit slice of each plane, packed so four table
|
||||
// reads build all four plane bytes for eight pixels at once.
|
||||
//
|
||||
// Byte lane p of the entry holds the two bits plane p takes from the pair:
|
||||
// the first pixel in bit 1, the second in bit 0. Accumulating with
|
||||
// `acc = (acc << 2) | table[b]` four times fills every lane with its eight
|
||||
// bits, and the lanes never bleed into each other because each starts empty
|
||||
// and only ever takes two bits at the bottom.
|
||||
//
|
||||
// This replaces eight read-modify-writes PER PIXEL PAIR in the unaligned
|
||||
// sprite walker with four table reads and two masked writes per plane per
|
||||
// EIGHT pixels, which is what the walker cost: Space Taxi's sprites are
|
||||
// almost never byte-aligned, so the walker is the path the game actually
|
||||
// takes, and it measured 13% of the whole frame in restore alone.
|
||||
// One plane's eight pixels starting at a sub-byte offset, spread to nibbles.
|
||||
// Reading two bytes and shifting inside a 16-bit window is what makes the
|
||||
// unaligned case cost the same as the aligned one.
|
||||
#define X68K_PLANE_SLICE(_p, _off, _shift) \
|
||||
(gPlaneToChunky[(uint8_t)(((((uint16_t)(_p)[(_off)] << 8) \
|
||||
| (uint16_t)(_p)[(_off) + 1u]) >> (8u - (_shift))) & 0xFFu)])
|
||||
|
||||
// Merge one plane's eight pixels into the two destination bytes they can
|
||||
// straddle. `bits` is the plane byte with the first pixel in bit 7; shifting
|
||||
// it into a 16-bit window and masking keeps neighbouring pixels intact, which
|
||||
// whole-byte stores would not.
|
||||
#define X68K_MERGE_PLANE(_p, _off, _bits, _shift, _mHi, _mLo) \
|
||||
do { \
|
||||
uint16_t w16_ = (uint16_t)(((uint16_t)(_bits) << 8) >> (_shift)); \
|
||||
(_p)[(_off)] = (uint8_t)(((_p)[(_off)] & (uint8_t)~(_mHi)) \
|
||||
| (uint8_t)((w16_ >> 8) & (_mHi))); \
|
||||
if ((_mLo) != 0u) { \
|
||||
(_p)[(_off) + 1u] = (uint8_t)(((_p)[(_off) + 1u] & (uint8_t)~(_mLo)) \
|
||||
| (uint8_t)(w16_ & (_mLo))); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
|
||||
static uint32_t gPairToPlanes[256];
|
||||
// The same two pixels' transparency: bit 1 the first pixel, bit 0 the second,
|
||||
// set when the pixel is not colour 0. Accumulated alongside gPairToPlanes so
|
||||
// the sprite draw gets its write mask from the same four table reads.
|
||||
static uint8_t gPairToMask[256];
|
||||
// The other direction, for the sprite SAVE: one plane byte (eight pixels)
|
||||
// spread into eight nibbles' bit 0, packed as four bytes. Plane p's
|
||||
// contribution is the same table shifted left by p, because each nibble
|
||||
// carries a single bit and cannot carry into its neighbour -- so four table
|
||||
// reads and three shifts rebuild eight chunky pixels from four planes.
|
||||
static uint32_t gPlaneToChunky[256];
|
||||
static bool gPairTableBuilt = false;
|
||||
|
||||
|
||||
static void buildPairTable(void) {
|
||||
uint16_t b;
|
||||
|
||||
if (gPairTableBuilt) {
|
||||
return;
|
||||
}
|
||||
for (b = 0u; b < 256u; b++) {
|
||||
uint8_t first = (uint8_t)(b >> 4);
|
||||
uint8_t second = (uint8_t)(b & 0x0Fu);
|
||||
uint32_t entry = 0ul;
|
||||
uint16_t plane;
|
||||
|
||||
for (plane = 0u; plane < X68K_BITPLANES; plane++) {
|
||||
uint32_t bits = (uint32_t)((((first >> plane) & 1u) << 1) | ((second >> plane) & 1u));
|
||||
|
||||
entry |= bits << (plane * 8u);
|
||||
}
|
||||
gPairToPlanes[b] = entry;
|
||||
gPairToMask[b] = (uint8_t)(((first != 0u) ? 2u : 0u) | ((second != 0u) ? 1u : 0u));
|
||||
}
|
||||
for (b = 0u; b < 256u; b++) {
|
||||
uint32_t spread = 0ul;
|
||||
uint16_t px;
|
||||
|
||||
// Pixel 0 is bit 7 and lands in the top nibble of the first byte.
|
||||
for (px = 0u; px < 8u; px++) {
|
||||
if ((b & (0x80u >> px)) != 0u) {
|
||||
spread |= 1ul << ((3u - (px >> 1)) * 8u + ((px & 1u) ? 0u : 4u));
|
||||
}
|
||||
}
|
||||
gPlaneToChunky[b] = spread;
|
||||
}
|
||||
gPairTableBuilt = true;
|
||||
}
|
||||
|
||||
|
||||
void jlpSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) {
|
||||
const X68kPlanarT *pd;
|
||||
uint16_t pairs;
|
||||
|
|
@ -399,30 +356,43 @@ void jlpSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w,
|
|||
return;
|
||||
}
|
||||
|
||||
// Unaligned span, eight pixels at a time: take each plane's eight-bit
|
||||
// slice with one 16-bit shift, then rebuild the chunky pairs from four
|
||||
// table reads (see gPlaneToChunky). The old loop tested four masked
|
||||
// plane bytes per PIXEL and recomputed the mask and offset each time.
|
||||
buildPairTable();
|
||||
pairs = (uint16_t)(w >> 1);
|
||||
{
|
||||
uint16_t shift = (uint16_t)((uint16_t)x & 7u);
|
||||
uint16_t firstOff = (uint16_t)((uint16_t)x >> 3);
|
||||
uint16_t rowOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW + firstOff);
|
||||
|
||||
for (row = 0; row < (uint16_t)h; row++) {
|
||||
const uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW;
|
||||
const uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW;
|
||||
const uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW;
|
||||
const uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW;
|
||||
const uint8_t *p0 = pd->planes[0] + rowOff;
|
||||
const uint8_t *p1 = pd->planes[1] + rowOff;
|
||||
const uint8_t *p2 = pd->planes[2] + rowOff;
|
||||
const uint8_t *p3 = pd->planes[3] + rowOff;
|
||||
uint16_t srcByte = 0u;
|
||||
|
||||
dst = dstPlaneBytes + (uint16_t)(row * pairs);
|
||||
for (pair = 0; pair < pairs; pair++) {
|
||||
uint16_t px = (uint16_t)(x + (int16_t)(pair << 1));
|
||||
uint16_t byteOff = (uint16_t)(px >> 3);
|
||||
uint8_t maskHi = (uint8_t)(0x80u >> (px & 7u));
|
||||
uint8_t maskLo = (uint8_t)(0x80u >> ((px + 1u) & 7u));
|
||||
uint16_t byteOffLo = (uint16_t)((px + 1u) >> 3);
|
||||
uint8_t hi = 0u;
|
||||
uint8_t lo = 0u;
|
||||
if (p0[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x01u); }
|
||||
if (p1[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x02u); }
|
||||
if (p2[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x04u); }
|
||||
if (p3[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x08u); }
|
||||
if (p0[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x01u); }
|
||||
if (p1[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x02u); }
|
||||
if (p2[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x04u); }
|
||||
if (p3[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x08u); }
|
||||
dst[pair] = (uint8_t)((hi << 4) | lo);
|
||||
for (pair = 0u; pair < pairs; pair += 4u) {
|
||||
uint16_t have = (uint16_t)(pairs - pair);
|
||||
uint32_t chunky;
|
||||
uint16_t k;
|
||||
|
||||
if (have > 4u) {
|
||||
have = 4u;
|
||||
}
|
||||
chunky = X68K_PLANE_SLICE(p0, srcByte, shift)
|
||||
| (X68K_PLANE_SLICE(p1, srcByte, shift) << 1)
|
||||
| (X68K_PLANE_SLICE(p2, srcByte, shift) << 2)
|
||||
| (X68K_PLANE_SLICE(p3, srcByte, shift) << 3);
|
||||
for (k = 0u; k < have; k++) {
|
||||
dst[pair + k] = (uint8_t)((chunky >> ((3u - k) * 8u)) & 0xFFu);
|
||||
}
|
||||
srcByte++;
|
||||
}
|
||||
rowOff = (uint16_t)(rowOff + X68K_BYTES_PER_ROW);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -477,32 +447,57 @@ void jlpSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uin
|
|||
return;
|
||||
}
|
||||
|
||||
// Unaligned span. Eight pixels at a time, not one: four table reads build
|
||||
// all four plane bytes at once (see gPairToPlanes), then each plane is
|
||||
// shifted into place and merged with two masked writes. The old walker
|
||||
// did eight read-modify-writes per PIXEL PAIR, thirty-two per eight
|
||||
// pixels, and recomputed a mask and a byte offset for every one of them.
|
||||
buildPairTable();
|
||||
pairs = (uint16_t)(w >> 1);
|
||||
{
|
||||
uint16_t shift = (uint16_t)((uint16_t)x & 7u);
|
||||
uint16_t firstOff = (uint16_t)((uint16_t)x >> 3);
|
||||
uint16_t rowOff = (uint16_t)((uint16_t)y * X68K_BYTES_PER_ROW + firstOff);
|
||||
|
||||
for (row = 0; row < (uint16_t)h; row++) {
|
||||
uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW;
|
||||
uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW;
|
||||
uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW;
|
||||
uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * X68K_BYTES_PER_ROW;
|
||||
uint8_t *p0 = pd->planes[0] + rowOff;
|
||||
uint8_t *p1 = pd->planes[1] + rowOff;
|
||||
uint8_t *p2 = pd->planes[2] + rowOff;
|
||||
uint8_t *p3 = pd->planes[3] + rowOff;
|
||||
uint16_t dstByte = 0u;
|
||||
|
||||
src = srcPlaneBytes + (uint16_t)(row * pairs);
|
||||
for (pair = 0; pair < pairs; pair++) {
|
||||
uint8_t b = src[pair];
|
||||
uint8_t hi = (uint8_t)(b >> 4);
|
||||
uint8_t lo = (uint8_t)(b & 0x0Fu);
|
||||
uint16_t px = (uint16_t)(x + (int16_t)(pair << 1));
|
||||
uint16_t byteOff = (uint16_t)(px >> 3);
|
||||
uint8_t maskHi = (uint8_t)(0x80u >> (px & 7u));
|
||||
uint8_t notHi = (uint8_t)(~maskHi);
|
||||
uint16_t byteOffLo = (uint16_t)((px + 1u) >> 3);
|
||||
uint8_t maskLo = (uint8_t)(0x80u >> ((px + 1u) & 7u));
|
||||
uint8_t notLo = (uint8_t)(~maskLo);
|
||||
p0[byteOff] = (uint8_t)((p0[byteOff] & notHi) | ((hi & 0x01u) ? maskHi : 0u));
|
||||
p1[byteOff] = (uint8_t)((p1[byteOff] & notHi) | ((hi & 0x02u) ? maskHi : 0u));
|
||||
p2[byteOff] = (uint8_t)((p2[byteOff] & notHi) | ((hi & 0x04u) ? maskHi : 0u));
|
||||
p3[byteOff] = (uint8_t)((p3[byteOff] & notHi) | ((hi & 0x08u) ? maskHi : 0u));
|
||||
p0[byteOffLo] = (uint8_t)((p0[byteOffLo] & notLo) | ((lo & 0x01u) ? maskLo : 0u));
|
||||
p1[byteOffLo] = (uint8_t)((p1[byteOffLo] & notLo) | ((lo & 0x02u) ? maskLo : 0u));
|
||||
p2[byteOffLo] = (uint8_t)((p2[byteOffLo] & notLo) | ((lo & 0x04u) ? maskLo : 0u));
|
||||
p3[byteOffLo] = (uint8_t)((p3[byteOffLo] & notLo) | ((lo & 0x08u) ? maskLo : 0u));
|
||||
for (pair = 0u; pair < pairs; pair += 4u) {
|
||||
uint16_t have = (uint16_t)(pairs - pair);
|
||||
uint32_t acc = 0ul;
|
||||
uint16_t k;
|
||||
uint16_t valid;
|
||||
uint16_t maskHi;
|
||||
uint16_t maskLo;
|
||||
|
||||
if (have > 4u) {
|
||||
have = 4u;
|
||||
}
|
||||
for (k = 0u; k < have; k++) {
|
||||
acc = (acc << 2) | gPairToPlanes[src[pair + k]];
|
||||
}
|
||||
// A short last group leaves its pixels at the bottom of each
|
||||
// lane; push them up so the first pixel is always bit 7.
|
||||
if (have < 4u) {
|
||||
acc <<= (2u * (4u - have));
|
||||
}
|
||||
valid = (uint16_t)(0xFFu << (8u - (have * 2u)));
|
||||
valid &= 0xFFu;
|
||||
maskHi = (uint16_t)((valid << 8) >> shift) >> 8;
|
||||
maskLo = (uint16_t)((valid << 8) >> shift) & 0xFFu;
|
||||
|
||||
X68K_MERGE_PLANE(p0, dstByte, (uint8_t)(acc & 0xFFu), shift, maskHi, maskLo);
|
||||
X68K_MERGE_PLANE(p1, dstByte, (uint8_t)((acc >> 8) & 0xFFu), shift, maskHi, maskLo);
|
||||
X68K_MERGE_PLANE(p2, dstByte, (uint8_t)((acc >> 16) & 0xFFu), shift, maskHi, maskLo);
|
||||
X68K_MERGE_PLANE(p3, dstByte, (uint8_t)((acc >> 24) & 0xFFu), shift, maskHi, maskLo);
|
||||
dstByte++;
|
||||
}
|
||||
rowOff = (uint16_t)(rowOff + X68K_BYTES_PER_ROW);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -562,7 +557,6 @@ void jlpSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
|
|||
int16_t dstByteIdx;
|
||||
uint16_t wTiles;
|
||||
uint8_t shift;
|
||||
uint8_t bit;
|
||||
uint8_t p0, p1, p2, p3, mask;
|
||||
uint8_t leftMask, notMask;
|
||||
uint8_t rshift, rightMask;
|
||||
|
|
@ -574,6 +568,7 @@ void jlpSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
|
|||
if (pd == NULL) {
|
||||
return;
|
||||
}
|
||||
buildPairTable();
|
||||
|
||||
wTiles = sp->widthTiles;
|
||||
w = (int16_t)(wTiles * 8u);
|
||||
|
|
@ -608,18 +603,26 @@ void jlpSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
|
|||
tile = sp->tileData + (uint32_t)((tileY * wTiles + byteCol) * 32u);
|
||||
chunky = tile + inTileY * 4u;
|
||||
|
||||
/* c2p: 8 pixels (4 chunky bytes) -> 4 plane bytes + mask. */
|
||||
p0 = 0u; p1 = 0u; p2 = 0u; p3 = 0u; mask = 0u;
|
||||
for (bit = 0; bit < 8u; bit++) {
|
||||
uint8_t nibble = (bit & 1u)
|
||||
? (uint8_t)(chunky[bit >> 1] & 0x0Fu)
|
||||
: (uint8_t)(chunky[bit >> 1] >> 4);
|
||||
uint8_t bm = (uint8_t)(0x80u >> bit);
|
||||
if (nibble != 0u) { mask |= bm; }
|
||||
if (nibble & 1u) { p0 |= bm; }
|
||||
if (nibble & 2u) { p1 |= bm; }
|
||||
if (nibble & 4u) { p2 |= bm; }
|
||||
if (nibble & 8u) { p3 |= bm; }
|
||||
/* c2p: 8 pixels (4 chunky bytes) -> 4 plane bytes + mask, four
|
||||
* table reads instead of eight passes of five conditional ORs.
|
||||
* See gPairToPlanes: each read carries two bits for every plane,
|
||||
* so one accumulator fills all four plane bytes at once. */
|
||||
{
|
||||
uint32_t acc = 0ul;
|
||||
uint16_t m = 0u;
|
||||
uint16_t k;
|
||||
|
||||
for (k = 0u; k < 4u; k++) {
|
||||
uint8_t cb = chunky[k];
|
||||
|
||||
acc = (acc << 2) | gPairToPlanes[cb];
|
||||
m = (uint16_t)((m << 2) | gPairToMask[cb]);
|
||||
}
|
||||
p0 = (uint8_t)(acc & 0xFFu);
|
||||
p1 = (uint8_t)((acc >> 8) & 0xFFu);
|
||||
p2 = (uint8_t)((acc >> 16) & 0xFFu);
|
||||
p3 = (uint8_t)((acc >> 24) & 0xFFu);
|
||||
mask = (uint8_t)m;
|
||||
}
|
||||
if (mask == 0u) {
|
||||
continue;
|
||||
|
|
@ -737,32 +740,6 @@ void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint
|
|||
// the four shadow planes. Caller (the per-primitive walker below) has
|
||||
// already validated (x, y) is on-surface. byteOff is computed once
|
||||
// and reused across all four planes since stride is the same in each.
|
||||
/* Per-pixel plane RMW with the color-bit classification hoisted by
|
||||
* the caller. set0..set3 are 0xFF if that plane's color bit is 1
|
||||
* (OR-in bitMask), 0 if 0 (AND-out bitMask). Each plane updates with
|
||||
* `*pn = (*pn & ~bitMask) | (setN & bitMask)` -- branch-free, 4
|
||||
* RMWs per pixel. Inlined into shape walkers so the outer loop pays
|
||||
* no function-call overhead. */
|
||||
/* Bit-mask LUT for the per-pixel macro -- replaces a runtime
|
||||
* `0x80u >> (x & 7)` (8 cyc per bit shifted on 68000) with a single
|
||||
* byte load. Saves ~25 cyc per pixel inside circle / line walkers. */
|
||||
static const uint8_t kAmigaPlanePutPixelBitLut[8] = {
|
||||
0x80u, 0x40u, 0x20u, 0x10u, 0x08u, 0x04u, 0x02u, 0x01u
|
||||
};
|
||||
|
||||
#define AMIGA_PLANE_PUT_PIXEL(pd_, x_, y_, set0_, set1_, set2_, set3_) do { \
|
||||
uint16_t byteOff_ = (uint16_t)((uint16_t)(y_) * X68K_BYTES_PER_ROW + ((uint16_t)(x_) >> 3)); \
|
||||
uint8_t bitMask_ = kAmigaPlanePutPixelBitLut[(uint16_t)(x_) & 7u]; \
|
||||
uint8_t notMask_ = (uint8_t)~bitMask_; \
|
||||
uint8_t *p0_ = &(pd_)->planes[0][byteOff_]; \
|
||||
uint8_t *p1_ = &(pd_)->planes[1][byteOff_]; \
|
||||
uint8_t *p2_ = &(pd_)->planes[2][byteOff_]; \
|
||||
uint8_t *p3_ = &(pd_)->planes[3][byteOff_]; \
|
||||
*p0_ = (uint8_t)((*p0_ & notMask_) | ((set0_) & bitMask_)); \
|
||||
*p1_ = (uint8_t)((*p1_ & notMask_) | ((set1_) & bitMask_)); \
|
||||
*p2_ = (uint8_t)((*p2_ & notMask_) | ((set2_) & bitMask_)); \
|
||||
*p3_ = (uint8_t)((*p3_ & notMask_) | ((set3_) & bitMask_)); \
|
||||
} while (0)
|
||||
|
||||
|
||||
static void x68kPlanarSetPixel(X68kPlanarT *pd, int16_t x, int16_t y, uint8_t color) {
|
||||
|
|
@ -806,56 +783,13 @@ void jlpDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
|
|||
}
|
||||
|
||||
|
||||
// Bresenham's diagonal line, planar-only walk. Same algorithm as
|
||||
// cross-platform jlDrawLine's fallback so the pixel set matches it
|
||||
// bit-for-bit.
|
||||
static void x68kPlanarLine(X68kPlanarT *pd, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color) {
|
||||
int16_t dx;
|
||||
int16_t dy;
|
||||
int16_t sx;
|
||||
int16_t sy;
|
||||
int16_t err;
|
||||
int16_t e2;
|
||||
uint8_t set0;
|
||||
uint8_t set1;
|
||||
uint8_t set2;
|
||||
uint8_t set3;
|
||||
|
||||
set0 = (color & 0x01u) ? 0xFFu : 0x00u;
|
||||
set1 = (color & 0x02u) ? 0xFFu : 0x00u;
|
||||
set2 = (color & 0x04u) ? 0xFFu : 0x00u;
|
||||
set3 = (color & 0x08u) ? 0xFFu : 0x00u;
|
||||
|
||||
dx = (int16_t)((x1 > x0) ? (x1 - x0) : (x0 - x1));
|
||||
dy = (int16_t)(-((y1 > y0) ? (y1 - y0) : (y0 - y1)));
|
||||
sx = (int16_t)((x0 < x1) ? 1 : -1);
|
||||
sy = (int16_t)((y0 < y1) ? 1 : -1);
|
||||
err = (int16_t)(dx + dy);
|
||||
while (1) {
|
||||
AMIGA_PLANE_PUT_PIXEL(pd, x0, y0, set0, set1, set2, set3);
|
||||
if (x0 == x1 && y0 == y1) {
|
||||
break;
|
||||
}
|
||||
e2 = (int16_t)(2 * err);
|
||||
if (e2 >= dy) {
|
||||
err = (int16_t)(err + dy);
|
||||
x0 = (int16_t)(x0 + sx);
|
||||
}
|
||||
if (e2 <= dx) {
|
||||
err = (int16_t)(err + dx);
|
||||
y0 = (int16_t)(y0 + sy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void jlpDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
|
||||
X68kPlanarT *pd;
|
||||
if (s->portData == NULL) {
|
||||
return;
|
||||
}
|
||||
pd = (X68kPlanarT *)s->portData;
|
||||
x68kPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
|
||||
surface68kPlanarLine(pd->planes, x0, y0, x1, y1, (uint16_t)(colorIndex & 0x0Fu));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -865,112 +799,33 @@ void jlpDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t co
|
|||
return;
|
||||
}
|
||||
pd = (X68kPlanarT *)s->portData;
|
||||
surface68kAmigaCircleOutline(pd->planes[0], pd->planes[1],
|
||||
pd->planes[2], pd->planes[3],
|
||||
(uint16_t)cx, (uint16_t)cy, r,
|
||||
(uint8_t)(colorIndex & 0x0Fu));
|
||||
}
|
||||
|
||||
|
||||
/* Single-row 4-plane span fill via shared68k asm. Caller pre-computes
|
||||
* the left/right partial-byte masks; fillByte per plane is just
|
||||
* 0xFF/0x00 based on colorIndex bit. The asm body avoids per-byte
|
||||
* function-call dispatch and the C compiler's per-iter overhead --
|
||||
* critical for jlFillCircle r=40 which pre-asm was paying ~50 ms/call
|
||||
* for 80 spans. */
|
||||
static inline __attribute__((always_inline))
|
||||
void x68kFillSpanInline(X68kPlanarT *pd, int16_t spanX, int16_t spanY,
|
||||
uint16_t spanW, uint8_t colorIndex) {
|
||||
static const uint8_t kLM[8] = {0xFFu,0x7Fu,0x3Fu,0x1Fu,0x0Fu,0x07u,0x03u,0x01u};
|
||||
static const uint8_t kRM[8] = {0x80u,0xC0u,0xE0u,0xF0u,0xF8u,0xFCu,0xFEu,0xFFu};
|
||||
|
||||
uint16_t byteFirst = (uint16_t)((uint16_t)spanX >> 3);
|
||||
uint16_t lastBit = (uint16_t)(spanX + spanW - 1);
|
||||
uint16_t byteLast = (uint16_t)(lastBit >> 3);
|
||||
uint8_t leftMask = kLM[(uint16_t)spanX & 7u];
|
||||
uint8_t rightMask = kRM[lastBit & 7u];
|
||||
uint16_t rowOff = (uint16_t)((uint16_t)spanY * X68K_BYTES_PER_ROW + byteFirst);
|
||||
uint8_t fb0 = ((colorIndex >> 0) & 1u) ? 0xFFu : 0x00u;
|
||||
uint8_t fb1 = ((colorIndex >> 1) & 1u) ? 0xFFu : 0x00u;
|
||||
uint8_t fb2 = ((colorIndex >> 2) & 1u) ? 0xFFu : 0x00u;
|
||||
uint8_t fb3 = ((colorIndex >> 3) & 1u) ? 0xFFu : 0x00u;
|
||||
uint8_t *p0 = pd->planes[0] + rowOff;
|
||||
uint8_t *p1 = pd->planes[1] + rowOff;
|
||||
uint8_t *p2 = pd->planes[2] + rowOff;
|
||||
uint8_t *p3 = pd->planes[3] + rowOff;
|
||||
|
||||
if (byteFirst == byteLast) {
|
||||
/* Single-byte case kept in C: the asm path post-increments
|
||||
* the pointer between leading and trailing RMW, which would
|
||||
* read the wrong byte if both edges land on the same byte.
|
||||
* One-byte spans are rare anyway (~1 of 80 in jlFillCircle r=40)
|
||||
* so the C overhead is fine here. */
|
||||
uint8_t mask = (uint8_t)(leftMask & rightMask);
|
||||
uint8_t notMask = (uint8_t)~mask;
|
||||
uint8_t *pp[X68K_BITPLANES];
|
||||
uint8_t fb[X68K_BITPLANES];
|
||||
uint8_t i;
|
||||
pp[0] = p0; pp[1] = p1; pp[2] = p2; pp[3] = p3;
|
||||
fb[0] = fb0; fb[1] = fb1; fb[2] = fb2; fb[3] = fb3;
|
||||
for (i = 0; i < X68K_BITPLANES; i++) {
|
||||
*pp[i] = (uint8_t)((*pp[i] & notMask) | (fb[i] & mask));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
uint16_t numMid = (uint16_t)(byteLast - byteFirst - 1u);
|
||||
surface68kFillSpan4Planes(p0, p1, p2, p3, numMid, leftMask, rightMask,
|
||||
fb0, fb1, fb2, fb3);
|
||||
}
|
||||
surface68kPlanarCircle(pd->planes, (uint16_t)cx, (uint16_t)cy, r, (uint16_t)(colorIndex & 0x0Fu));
|
||||
}
|
||||
|
||||
|
||||
// src/m68k/planarFill.s does the whole exact-disk walk: for each y in [0, r] the
|
||||
// largest x with x*x + y*y <= r*r, then one span at cy+y and one at cy-y, each
|
||||
// row filled exactly once. Row coverage is identical to draw.c's own
|
||||
// jlFillCircle, so the golden hashes hold by construction.
|
||||
//
|
||||
// The C this replaces called an assembly span filler from a C walk. That was
|
||||
// already 1.5x the pixel-at-a-time version, but the call and the per-span
|
||||
// geometry cost more than the pixels did: the masks are now computed once for
|
||||
// a row's two spans, and the walk's 32-bit arithmetic is 16-bit throughout.
|
||||
//
|
||||
// The asm keeps r*r in a word, so it needs r <= 181. draw.c only calls this
|
||||
// when the bounding box is on-surface and every planar surface on this port is
|
||||
// SURFACE_HEIGHT rows tall, which caps r at half of that; the check below
|
||||
// fails the build rather than let a taller surface wrap silently.
|
||||
bool jlpFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
|
||||
X68kPlanarT *pd;
|
||||
uint32_t r2;
|
||||
uint32_t xx;
|
||||
uint32_t yy;
|
||||
int32_t x;
|
||||
uint16_t y;
|
||||
enum { kMaxWordRadius = 181 };
|
||||
|
||||
_Static_assert((SURFACE_HEIGHT / 2) <= kMaxWordRadius, "r*r must fit a word in src/m68k/planarFill.s");
|
||||
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
pd = (X68kPlanarT *)s->portData;
|
||||
// Exact-disk span walk (perf-hunt-2 rank 14, the recorded "true
|
||||
// lever"; algorithm copied from the ST stPlanarCircleFill): for
|
||||
// each y offset track the largest x with x^2 + y^2 <= r^2
|
||||
// incrementally, and fill each row exactly ONCE. The old midpoint
|
||||
// loop emitted 4 octant spans per iteration whose horizontal spans
|
||||
// overlapped (~14-16% redundant bytes at r=40) and re-dispatched
|
||||
// x68kFillSpanInline for each. Row coverage is identical to
|
||||
// draw.c's jlFillCircle and the ST walker, so the golden hashes
|
||||
// are unchanged by construction. Spans stay unclipped -- draw.c
|
||||
// routes every off-surface circle through its own clipped loop
|
||||
// before jlpFillCircle is called (same contract as before).
|
||||
r2 = (uint32_t)r * (uint32_t)r;
|
||||
xx = r2;
|
||||
yy = 0u;
|
||||
x = (int32_t)r;
|
||||
for (y = 0u; y <= r; y++) {
|
||||
uint32_t xxLimit;
|
||||
int16_t spanX;
|
||||
uint16_t spanW;
|
||||
|
||||
xxLimit = r2 - yy;
|
||||
while (xx > xxLimit) {
|
||||
xx = xx - ((uint32_t)x + (uint32_t)x - 1u);
|
||||
x--;
|
||||
}
|
||||
spanX = (int16_t)(cx - (int16_t)x);
|
||||
spanW = (uint16_t)((int16_t)x + (int16_t)x + 1);
|
||||
x68kFillSpanInline(pd, spanX, (int16_t)(cy + (int16_t)y), spanW, colorIndex);
|
||||
if (y > 0u) {
|
||||
x68kFillSpanInline(pd, spanX, (int16_t)(cy - (int16_t)y), spanW, colorIndex);
|
||||
}
|
||||
yy = yy + ((uint32_t)y + (uint32_t)y + 1u);
|
||||
}
|
||||
surface68kFillCircle(((X68kPlanarT *)s->portData)->planes, (uint16_t)cx, (uint16_t)cy, r, colorIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1307,7 +1162,11 @@ void *jlpSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
|
|||
* Amiga these are plain heap: no blitter or DMA reads them, only the
|
||||
* CPU, so there is no Chip RAM constraint to satisfy. */
|
||||
for (i = 0; i < X68K_BITPLANES; i++) {
|
||||
pd->planes[i] = (uint8_t *)calloc(1u, (size_t)X68K_PLANE_SIZE);
|
||||
// One byte of slack past the end. The shifted plane slice the sprite
|
||||
// save takes reads the byte AFTER the one it needs whenever the span
|
||||
// is not byte-aligned, which on the last row would otherwise be one
|
||||
// byte past the buffer. Padding is cheaper than branching per group.
|
||||
pd->planes[i] = (uint8_t *)calloc(1u, (size_t)X68K_PLANE_SIZE + 1u);
|
||||
jlLogF("x68k: shadow plane %u on heap (CPU only, addr=$%08lX)",
|
||||
(unsigned)i, (unsigned long)pd->planes[i]);
|
||||
if (pd->planes[i] == NULL) {
|
||||
|
|
|
|||
|
|
@ -98,6 +98,10 @@ static void sccIsrInstall(void);
|
|||
static void sccIsrUninstall(void);
|
||||
|
||||
static bool gSccIsrInstalled = false;
|
||||
// Has the app opened the port at all? Without this the polled pump traps into
|
||||
// IOCS on every call, and jlpPresent calls it once every two blitted rows --
|
||||
// about fifty _ISNS232C traps per frame for a game that never touched serial.
|
||||
static bool gSerialOpened = false;
|
||||
static uint8_t gSccRxVec = 0u;
|
||||
static uint32_t gSccOldRx = 0ul;
|
||||
static uint32_t gSccOldSp = 0ul;
|
||||
|
|
@ -165,7 +169,7 @@ static void sccIsrUninstall(void) {
|
|||
// fallback that keeps the port working, with the caller deaf-tolerant up to the SOFT ring instead
|
||||
// of RSDRV's ~64 bytes.
|
||||
void x68kSerialPump(void) {
|
||||
if (gSccIsrInstalled) {
|
||||
if (gSccIsrInstalled || !gSerialOpened) {
|
||||
return;
|
||||
}
|
||||
while (_iocs_isns232c() != 0) {
|
||||
|
|
@ -190,6 +194,7 @@ uint16_t jlpSerialAvailable(void) {
|
|||
|
||||
void jlpSerialClose(void) {
|
||||
sccIsrUninstall(); // hand channel A RX back to RSDRV before the vectors dangle
|
||||
gSerialOpened = false;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -212,6 +217,7 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
|
|||
// idiom is a _CRTMOD contract, undocumented for _SET232C -- XEiJ reads the
|
||||
// RSDRV work area instead -- so the write could not be trusted.)
|
||||
(void)config;
|
||||
gSerialOpened = true;
|
||||
sccIsrInstall(); // own the RX interrupt; falls back to the polled pump if the vectors look wrong
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ int main(void) {
|
|||
memset(&gGame, 0, sizeof(gGame));
|
||||
memset(&gSim, 0, sizeof(gSim));
|
||||
stRenderInit(stage);
|
||||
stAudioInit();
|
||||
(void)stAudioInit();
|
||||
stSimNewGame(&gSim, 2u, false);
|
||||
|
||||
// ---- ranking ($4D92) ----
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue