X68k, ST, and Amiga optimizations.
This commit is contained in:
parent
5adf55e80f
commit
f6c4ad3747
8 changed files with 385 additions and 56 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
|
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__":
|
||||||
|
|
|
||||||
98
spacetaxi.c
98
spacetaxi.c
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
spacetaxi.h
18
spacetaxi.h
|
|
@ -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
142
stAudio.c
|
|
@ -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);
|
||||||
|
|
|
||||||
25
stCels.c
25
stCels.c
|
|
@ -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;
|
||||||
|
|
|
||||||
4
stCels.h
4
stCels.h
|
|
@ -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.
|
||||||
|
|
|
||||||
125
stRender.c
125
stRender.c
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) ----
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue