X68k, ST, and Amiga optimizations.

This commit is contained in:
Scott Duensing 2026-09-24 22:01:56 -05:00
parent 5adf55e80f
commit f6c4ad3747
8 changed files with 385 additions and 56 deletions

View file

@ -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 first word ends on the low level plays its second word inverted. Both
versions are stored and the player picks by the running parity. versions are stored and the player picks by the running parity.
speech.bin (little-endian): 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 format (0 = signed +/-100, 1 = 0x80-biased for the IIgs raw stream)
u8 pad[3] u32 dataBytes u8 polarities (1 or 2) u8 pad[2] u32 dataBytes
entries[pitchCount * count * 2]: u8 code, u8 parity, u16 samples, entries[pitchCount * count * polarities]: u8 code, u8 parity,
u32 offset -- polarity is the innermost index (0 = as decoded, u16 samples, u32 offset -- polarity is the innermost index
1 = inverted). parity = 1 when the utterance ends on the (0 = as decoded, 1 = inverted). parity = 1 when the utterance
opposite level from the one it started on. ends on the opposite level from the one it started on.
data: sample bytes 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 re
import struct import struct
@ -77,6 +80,8 @@ def decode(rle, perOn):
def main(): def main():
args = [a for a in sys.argv[1:] if not a.startswith("--")] args = [a for a in sys.argv[1:] if not a.startswith("--")]
biased = "--biased" in sys.argv[1:] biased = "--biased" in sys.argv[1:]
single = "--single" in sys.argv[1:]
polarities = 1 if single else 2
src, dst = args[0], args[1] src, dst = args[0], args[1]
entries, data = parseSource(src) entries, data = parseSource(src)
records = [] records = []
@ -86,7 +91,7 @@ def main():
for code, off, length in entries: for code, off, length in entries:
samples, level = decode(data[off:off + length], perOn) samples, level = decode(data[off:off + length], perOn)
parity = 1 if level < 0 else 0 parity = 1 if level < 0 else 0
for pol in range(2): for pol in range(polarities):
pcm = bytearray() pcm = bytearray()
for s in samples: for s in samples:
v = -s if pol else s v = -s if pol else s
@ -94,12 +99,14 @@ def main():
records.append((code, parity, len(samples), len(blob))) records.append((code, parity, len(samples), len(blob)))
blob += pcm blob += pcm
with open(dst, "wb") as f: 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: for code, parity, n, off in records:
f.write(struct.pack("<BBHI", code, parity, n, off)) f.write(struct.pack("<BBHI", code, parity, n, off))
f.write(blob) f.write(blob)
total = sum(n for _, _, n, _ in records) 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__": if __name__ == "__main__":

View file

@ -40,6 +40,11 @@
#define ST_GAME_OVER_TICKS 60u #define ST_GAME_OVER_TICKS 60u
// Menu joystick repeat delay ($4101 with X = $82). // Menu joystick repeat delay ($4101 with X = $82).
#define ST_MENU_REPEAT_TICKS 8u #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). // The immortal cabbies ($4A89 image, $4E35 screen).
#define ST_HS_ENTRIES 10u #define ST_HS_ENTRIES 10u
@ -87,6 +92,7 @@ typedef struct {
uint8_t hsCursor; // $4F4D the blinking cursor's current glyph uint8_t hsCursor; // $4F4D the blinking cursor's current glyph
uint8_t hsBlinkTicks; // ticks toward the next cursor flip uint8_t hsBlinkTicks; // ticks toward the next cursor flip
bool paused; // $4FCB: SHIFT is holding the game bool paused; // $4FCB: SHIFT is holding the game
bool fatal; // an asset on the disk would not load
} StGameT; } StGameT;
// The immortal cabbies ($4A89): ten entries of a right-aligned "dddd.dd" // 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", (const uint8_t *)"ALSO TRY RESCUE SQUAD BY JOHN KUTCHER",
}; };
static const uint8_t kTextReturn[] = "PRESS FIRE TO RETURN"; 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 StGameT gGame;
static StSimT gSim; static StSimT gSim;
@ -163,6 +177,8 @@ static StLevelT gLevel;
static void blankScreen(StSimT *sim, uint8_t border, uint8_t bg); static void blankScreen(StSimT *sim, uint8_t border, uint8_t bg);
static void enterTitle(void); static void enterTitle(void);
static void fatalHold(void);
static void fatalStop(const char *line1, const char *line2);
static void gameOverEnter(void); static void gameOverEnter(void);
static void gameOverFinish(void); static void gameOverFinish(void);
static void highScoreDraw(void); static void highScoreDraw(void);
@ -207,9 +223,10 @@ static void blankScreen(StSimT *sim, uint8_t border, uint8_t bg) {
static void enterTitle(void) { static void enterTitle(void) {
gSim.demoMode = 0u; gSim.demoMode = 0u;
if (!stLevelLoad(&gLevel, "levels/title.dat")) { 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); stTitleEnter(&gSim, &gLevel);
stRenderSceneChanged(&gSim); stRenderSceneChanged(&gSim);
stRenderTitleLogo(&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. // $5FBB -- "GAME OVER!" over the play field, sprites hidden.
static void gameOverEnter(void) { static void gameOverEnter(void) {
stSimDrawText(&gSim, 14u, 10u, kTextBlank12, 1u); stSimDrawText(&gSim, 14u, 10u, kTextBlank12, 1u);
@ -505,8 +561,7 @@ static bool loadLevel(uint8_t index) {
ST_LOG("spacetaxi: ! cannot load %s", path); ST_LOG("spacetaxi: ! cannot load %s", path);
return false; return false;
} }
stRenderLevelCels(gStage, &gLevel); return stRenderLevelCels(gStage, &gLevel);
return true;
} }
@ -842,12 +897,25 @@ static void startDemoScreen(void) {
(void)stSimAdvancePlayer(&gSim); (void)stSimAdvancePlayer(&gSim);
gSim.levelState++; gSim.levelState++;
if (!loadLevel(e->levelIndex)) { if (!loadLevel(e->levelIndex)) {
enterTitle(); fatalStop(kTextFatalLoad, kTextFatalLevel);
return; 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, 8u, kTextGetReady, 5u);
stSimDrawText(&gSim, 10u, 10u, kTextOnTheTaxi, 5u); stSimDrawText(&gSim, 10u, 10u, kTextOnTheTaxi, 5u);
gSim.frame.enableMask &= 0x07u; gSim.frame.enableMask &= 0x07u;
stRenderSceneChanged(&gSim);
gGame.waitTicks = ST_GET_READY_TICKS; gGame.waitTicks = ST_GET_READY_TICKS;
gGame.state = ST_STATE_GET_READY; gGame.state = ST_STATE_GET_READY;
} }
@ -881,7 +949,7 @@ static void startNewScreen(void) {
} }
gGame.pendingLevel = next; gGame.pendingLevel = next;
if (!loadLevel(next)) { if (!loadLevel(next)) {
enterTitle(); fatalStop(kTextFatalLoad, kTextFatalLevel);
return; return;
} }
gSim.levelState++; gSim.levelState++;
@ -931,21 +999,31 @@ int main(void) {
memset(&gSim, 0, sizeof(gSim)); memset(&gSim, 0, sizeof(gSim));
stRenderInit(stage); stRenderInit(stage);
stRenderLoading(stage); stRenderLoading(stage);
stAudioInit(); if (!stAudioInit()) {
fatalStop(kTextFatalMemory, kTextFatalSpeech);
}
// Boot: the playback buffer as the C64 leaves it, and the state a // Boot: the playback buffer as the C64 leaves it, and the state a
// fresh machine has when the first title intro starts ($4092 + the // fresh machine has when the first title intro starts ($4092 + the
// title's own sparkle/walk history from the dump). // title's own sparkle/walk history from the dump).
stSimNewGame(&gSim, 1u, false); stSimNewGame(&gSim, 1u, false);
stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES); stSimSeedDemoBuffer(&gSim, kDemoBufferInit, ST_DEMO_BUFFER_BYTES);
gSim.waveIdx = 3u; gSim.waveIdx = 3u;
stRenderPrewarm(stage, &gSim); if (!gGame.fatal && !stRenderPrewarm(stage, &gSim)) {
fatalStop(kTextFatalMemory, kTextFatalSprites);
}
highScoreLoad(); // $23E5 highScoreLoad(); // $23E5
enterTitle(); if (!gGame.fatal) {
enterTitle();
}
gGame.lastFrame = (uint8_t)jlFrameCount(); gGame.lastFrame = (uint8_t)jlFrameCount();
for (;;) { for (;;) {
uint8_t ticks; uint8_t ticks;
if (gGame.fatal) {
fatalHold();
break;
}
jlInputPoll(); jlInputPoll();
// $4FCB runStopWatcher, on the game tick, the intro and the menus: // $4FCB runStopWatcher, on the game tick, the intro and the menus:
// SHIFT held freezes everything with the sound gated off and any // SHIFT held freezes everything with the sound gated off and any
@ -1056,5 +1134,5 @@ int main(void) {
stAudioShutdown(); stAudioShutdown();
stRenderShutdown(); stRenderShutdown();
jlShutdown(); jlShutdown();
return 0; return gGame.fatal ? 1 : 0;
} }

View file

@ -25,9 +25,11 @@
#if defined(__W65816__) || defined(JOEYLIB_PLATFORM_X68000) #if defined(__W65816__) || defined(JOEYLIB_PLATFORM_X68000)
#define ST_LOG_RESET() ((void)0) #define ST_LOG_RESET() ((void)0)
#define ST_LOG(...) ((void)0) #define ST_LOG(...) ((void)0)
#define ST_LOG_FLUSH() ((void)0)
#else #else
#define ST_LOG_RESET() jlLogReset() #define ST_LOG_RESET() jlLogReset()
#define ST_LOG(...) jlLogF(__VA_ARGS__) #define ST_LOG(...) jlLogF(__VA_ARGS__)
#define ST_LOG_FLUSH() jlLogFlush()
#endif #endif
// Level loading through the JoeyLib data path (DATA/levels/...). // 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 // animates without a single pixel moving. Call after
// stRenderSceneChanged, which turns it back off. // stRenderSceneChanged, which turns it back off.
void stRenderIntroStars(void); 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). // 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). // 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 stAudioShutdown(void);
void stAudioTick(void); void stAudioTick(void);

142
stAudio.c
View file

@ -29,6 +29,13 @@
#define ST_ATTEN_LOUD 4u #define ST_ATTEN_LOUD 4u
#define ST_ATTEN_SOFT 8u #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 #define ST_ATTEN_OFF 15u
// NTSC SID: Hz = word * 1022727 / 16777216 = word * 0.06096. // 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 // the ride half a second on every "HEY TAXI"; the 1-bit intermediate
// format that followed still needed a per-byte expansion loop. // format that followed still needed a per-byte expansion loop.
#define ST_SPEECH_FILE "speech.bin" #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_HEADER_BYTES 16u
#define ST_SPEECH_ENTRY_BYTES 8u #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_SIGNED 0u
#define ST_SPEECH_FORMAT_BIASED 1u #define ST_SPEECH_FORMAT_BIASED 1u
#define ST_SPEECH_LOAD_CHUNK 16384u #define ST_SPEECH_LOAD_CHUNK 16384u
@ -88,6 +101,9 @@ typedef struct {
static StSfxSlotT gSfx; // SID voice 1 (programs + sweep) static StSfxSlotT gSfx; // SID voice 1 (programs + sweep)
static StSfxSlotT gTone; // SID voice 2 static StSfxSlotT gTone; // SID voice 2
static bool gNoiseOn; 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 // Speech playback state. The fill callback walks a queue of utterances so
// a phrase like "HT" ("Hey" + "Taxi") runs as one continuous stream // 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 gSpeechSample; // next sample index in it
static uint16_t gSpeechSamples; // its length static uint16_t gSpeechSamples; // its length
static uint8_t gSpeechParity; // it ends on the opposite level 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 // 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 // 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. // 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 uint16_t sidHz(uint16_t word);
static void speechLoad(void); static bool speechLoad(void);
static bool speechNext(void); static bool speechNext(void);
static void speechArm(void); static void speechArm(void);
static uint16_t speechDecode(uint8_t *dst, uint16_t count); 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. // is empty, which ends the stream.
// DATA/speech.bin into one jlAlloc block; without it the game is mute // DATA/speech.bin into one jlAlloc block; without it the game is mute
// but otherwise fine. // but otherwise fine.
static void speechLoad(void) { static bool speechLoad(void) {
FILE *fp = jlDataOpen(ST_SPEECH_FILE, "rb"); FILE *fp = jlDataOpen(ST_SPEECH_FILE, "rb");
uint8_t head[ST_SPEECH_HEADER_BYTES]; uint8_t head[ST_SPEECH_HEADER_BYTES];
uint8_t format; uint8_t format;
@ -155,22 +173,30 @@ static void speechLoad(void) {
gSpeechBlob = 0; gSpeechBlob = 0;
if (fp == 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; 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); fclose(fp);
return; return false;
} }
gSpeechPolarities = head[9];
gSpeechPitchBase = head[5]; gSpeechPitchBase = head[5];
gSpeechPitchCount = head[6]; gSpeechPitchCount = head[6];
gSpeechCount = head[7]; gSpeechCount = head[7];
dataBytes = (uint32_t)head[12] | ((uint32_t)head[13] << 8) | ((uint32_t)head[14] << 16) | ((uint32_t)head[15] << 24); 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); gSpeechBlob = (uint8_t *)jlAlloc(total);
if (gSpeechBlob == 0) { 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); fclose(fp);
return; return false;
} }
memcpy(gSpeechBlob, head, sizeof(head)); memcpy(gSpeechBlob, head, sizeof(head));
// The block spans banks on the IIgs; read it in bank-safe pieces. // 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; want = ST_SPEECH_LOAD_CHUNK;
} }
if (fread(gSpeechBlob + done, 1u, (size_t)want, fp) != (size_t)want) { 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); jlFree(gSpeechBlob);
gSpeechBlob = 0; gSpeechBlob = 0;
break; break;
@ -189,6 +216,11 @@ static void speechLoad(void) {
done += want; done += want;
} }
fclose(fp); 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) { while (gSpeechHead != gSpeechTail) {
uint8_t code = gSpeechQueue[gSpeechHead]; 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; uint8_t k;
gSpeechHead = (uint8_t)((gSpeechHead + 1u) % ST_SPEECH_QUEUE); 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) { 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); 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]; gSpeechParity = v[1];
gSpeechSamples = (uint16_t)(v[2] | ((uint16_t)v[3] << 8)); gSpeechSamples = (uint16_t)(v[2] | ((uint16_t)v[3] << 8));
gSpeechSample = 0u; 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; return true;
} }
} }
@ -252,7 +289,17 @@ static uint16_t speechDecode(uint8_t *dst, uint16_t count) {
if (take > left) { if (take > left) {
take = left; take = left;
} }
jlMemCopy(dst + n, gSpeechPcm + gSpeechSample, take); 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); gSpeechSample = (uint16_t)(gSpeechSample + take);
n = (uint16_t)(n + take); n = (uint16_t)(n + take);
} }
@ -368,14 +415,27 @@ void stAudioTick(void) {
jlAudioVoice(gTone.voice, 0u, ST_ATTEN_OFF); 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(); (void)jlAudioInit();
gSpeechStage = (uint8_t *)jlAlloc(ST_SPEECH_STAGE_BYTES); gSpeechStage = (uint8_t *)jlAlloc(ST_SPEECH_STAGE_BYTES);
speechLoad(); ok = (gSpeechStage != 0) && speechLoad();
gSfx.voice = ST_VOICE_SFX; gSfx.voice = ST_VOICE_SFX;
gNoiseTicks = 0u;
gSfx.ticksLeft = 0u; gSfx.ticksLeft = 0u;
gTone.voice = ST_VOICE_TONE; gTone.voice = ST_VOICE_TONE;
gTone.ticksLeft = 0u; gTone.ticksLeft = 0u;
@ -385,6 +445,8 @@ void stAudioInit(void) {
gSpeechActive = false; gSpeechActive = false;
gSpeechPitch = ST_SPEECH_PITCH_BASE; gSpeechPitch = ST_SPEECH_PITCH_BASE;
gSpeechPol = 0u; gSpeechPol = 0u;
gSpeechInvert = false;
return ok;
} }
@ -401,16 +463,38 @@ void stAudioShutdown(void) {
// $6D6A / $6A3B -- voice 3 jet noise gate. // $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) { 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; return;
} }
gNoiseOn = on;
if (on) { if (on) {
jlAudioNoise(31u, ST_ATTEN_SOFT); gNoiseOn = true;
} else { jlAudioNoise(ST_NOISE_JET_PITCH, ST_ATTEN_SOFT);
jlAudioNoise(0u, ST_ATTEN_OFF); 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; StSfxSlotT *slot = (program9[8] == 1u) ? &gTone : &gSfx;
if ((ctrl & 0x80u) != 0u) { if ((ctrl & 0x80u) != 0u) {
// Noise waveform: a burst on the noise generator. // Noise waveform: a burst on the noise generator, held for the
jlAudioNoise(31u, ST_ATTEN_LOUD); // program's release time exactly as the tone slots below hold
gNoiseOn = true; // 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; return;
} }
jlAudioVoice(slot->voice, sidHz(word), ST_ATTEN_LOUD); jlAudioVoice(slot->voice, sidHz(word), ST_ATTEN_LOUD);

View file

@ -44,6 +44,31 @@ const StCelDefT kStCels[] = {
const uint16_t kStCelCount = (uint16_t)(sizeof(kStCels) / sizeof(kStCels[0])); 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) { 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 pal[4];
uint8_t tx; uint8_t tx;

View file

@ -30,6 +30,10 @@ typedef struct {
extern const StCelDefT kStCels[]; extern const StCelDefT kStCels[];
extern const uint16_t kStCelCount; 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` // 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. // into `out`, no duplicates. 0 for a level without hook sprites.

View file

@ -155,6 +155,7 @@ static StSpriteCacheT *cacheFreeSlot(void);
static void levelCelsEvict(void); static void levelCelsEvict(void);
static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr); static const uint8_t *levelSpriteBitmap(const StLevelT *level, uint8_t ptr);
static void loadingBar(jlSurfaceT *stage, uint8_t done, uint8_t total); 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 cellCol(uint16_t cell, uint8_t row);
static uint8_t cellRow(uint16_t cell); static uint8_t cellRow(uint16_t cell);
static void collectGlyphs(StSimT *sim); static void collectGlyphs(StSimT *sim);
@ -166,10 +167,11 @@ static bool loadPrecompiledCels(void);
static bool logoBuildTile(const StSimT *sim); static bool logoBuildTile(const StSimT *sim);
static uint8_t logoFrameIndex(const StSimT *sim); static uint8_t logoFrameIndex(const StSimT *sim);
static void logoPaletteApply(const StSimT *sim, uint8_t frame); static void logoPaletteApply(const StSimT *sim, uint8_t frame);
static void starPaletteApply(void);
static void paintCells(jlSurfaceT *stage, StSimT *sim); 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 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 bool spriteOnScreen(int16_t px, int16_t py);
static void starPaletteApply(void);
static void tileFromChunky(jlTileT *out, const uint8_t *chunky); 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) { if (idx <= 2u && gPrewarming) {
cacheCompile(slot); 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); ST_LOG("spacetaxi: ! cel $%02X colour %u (level %u) built mid-ride -- add it to stLevelCelList", ptr, color, level);
} }
slot->lastUse = gRender.cacheStamp; 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, // 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 // because the two-shift form of the multiply came back as a __mulhi3
// library call on the 65816. // 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 uint8_t cellCol(uint16_t cell, uint8_t row) {
static const uint16_t kRowBase[ST_SCREEN_ROWS] = { static const uint16_t kRowBase[ST_SCREEN_ROWS] = {
0, 40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480, 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 // here behind the LOADING bar. Either way nothing compiles once the
// level is running. Keyed exactly as cachedSprite keys them, so every // level is running. Keyed exactly as cachedSprite keys them, so every
// lookup during the ride is a hit. // 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]; static StCelDefT list[ST_LEVEL_CELS_MAX];
jlSpriteT *cels[ST_LEVEL_CELS_MAX]; jlSpriteT *cels[ST_LEVEL_CELS_MAX];
char path[24]; char path[24];
uint8_t n; uint8_t n;
uint8_t got; uint8_t got;
uint8_t ready = 0u;
uint8_t k; uint8_t k;
levelCelsEvict(); levelCelsEvict();
if (level == 0) { if (level == 0) {
return; return true;
} }
n = stLevelCelList(level, list, ST_LEVEL_CELS_MAX); n = stLevelCelList(level, list, ST_LEVEL_CELS_MAX);
if (n == 0u) { if (n == 0u) {
return; return true;
} }
memcpy(path, "sprites/level", 13u); memcpy(path, "sprites/level", 13u);
path[13] = (char)('0' + (level->levelIndex + 1u) / 10u); path[13] = (char)('0' + (level->levelIndex + 1u) / 10u);
@ -938,11 +961,17 @@ void stRenderLevelCels(jlSurfaceT *stage, const StLevelT *level) {
slot->used = true; slot->used = true;
slot->pinned = true; slot->pinned = true;
slot->lastUse = gRender.cacheStamp; slot->lastUse = gRender.cacheStamp;
ready++;
} }
gPrewarming = false; gPrewarming = false;
if (got != n) { if (got != n) {
jlFillRect(stage, 60, 110, 200u, 6u, 0u); 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->used = true;
e->pinned = true; e->pinned = true;
e->lastUse = 0u; 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; 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 // "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 // game's own, indexed by ASCII (the C64 font's letters sit at their
@ -999,12 +1071,13 @@ void stRenderLoading(jlSurfaceT *stage) {
jlStagePresent(); 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 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 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 }; 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 total = (uint8_t)(sizeof(kCabPtrs) + sizeof(kPassPtrs) + sizeof(kFlamePtrs) + 1u);
uint8_t done = 0u; uint8_t done = 0u;
bool ok;
uint8_t k; uint8_t k;
StFrameT saved; StFrameT saved;
@ -1012,7 +1085,7 @@ void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
// runtime build below on any port without a matching .spc. // runtime build below on any port without a matching .spc.
if (loadPrecompiledCels()) { if (loadPrecompiledCels()) {
ST_LOG("spacetaxi: cels from sprites/staxicels.spc"); ST_LOG("spacetaxi: cels from sprites/staxicels.spc");
return; return prewarmReport(kStCelCount);
} }
ST_LOG("spacetaxi: ! no usable sprites/staxicels.spc -- JIT prewarm"); ST_LOG("spacetaxi: ! no usable sprites/staxicels.spc -- JIT prewarm");
saved = sim->frame; saved = sim->frame;
@ -1050,7 +1123,45 @@ void stRenderPrewarm(jlSurfaceT *stage, StSimT *sim) {
(void)cachedSprite(sim, 2u); (void)cachedSprite(sim, 2u);
sim->frame = saved; sim->frame = saved;
gPrewarming = false; gPrewarming = false;
ok = prewarmReport(total);
jlFillRect(stage, 60, 110, 200u, 6u, 0u); 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;
} }

View file

@ -104,7 +104,7 @@ int main(void) {
memset(&gGame, 0, sizeof(gGame)); memset(&gGame, 0, sizeof(gGame));
memset(&gSim, 0, sizeof(gSim)); memset(&gSim, 0, sizeof(gSim));
stRenderInit(stage); stRenderInit(stage);
stAudioInit(); (void)stAudioInit();
stSimNewGame(&gSim, 2u, false); stSimNewGame(&gSim, 2u, false);
// ---- ranking ($4D92) ---- // ---- ranking ($4D92) ----