More fixes and optimizations.
This commit is contained in:
parent
1a7c7ec05a
commit
b2ba0c33e9
12 changed files with 1528 additions and 220 deletions
|
|
@ -31,6 +31,10 @@ MAME="$repo/toolchains/cache/mame-mame0264/x68k"
|
||||||
TEMPLATE="$SP/x68mame/HUMAN302.XDF"
|
TEMPLATE="$SP/x68mame/HUMAN302.XDF"
|
||||||
FRAMES="${X68K_BENCH_FRAMES:-120000}"
|
FRAMES="${X68K_BENCH_FRAMES:-120000}"
|
||||||
WALL="${X68K_BENCH_WALL:-7200}"
|
WALL="${X68K_BENCH_WALL:-7200}"
|
||||||
|
# Which plane the built port presents into: must match X68K_PRESENT_TEXT in
|
||||||
|
# src/x68000/hal.c (1 -> text, 0 -> gfx). Only the completion DETECTOR cares;
|
||||||
|
# the hashes are of the surface in RAM and are the same either way.
|
||||||
|
PRESENT_PLANE="${X68K_PRESENT_PLANE:-text}"
|
||||||
|
|
||||||
export PATH="$repo/toolchains/x68000/m68k-xelf/bin:$PATH"
|
export PATH="$repo/toolchains/x68000/m68k-xelf/bin:$PATH"
|
||||||
work=$(mktemp -d -t joey-x68bench.XXXXXX)
|
work=$(mktemp -d -t joey-x68bench.XXXXXX)
|
||||||
|
|
@ -92,10 +96,29 @@ local FRAMES = $FRAMES
|
||||||
local GREEN_STABLE = 600 -- ~11 s of emulated time held steady
|
local GREEN_STABLE = 600 -- ~11 s of emulated time held steady
|
||||||
local EXIT_SETTLE = 900 -- keypress -> fclose -> back at the Human68k prompt
|
local EXIT_SETTLE = 900 -- keypress -> fclose -> back at the Human68k prompt
|
||||||
|
|
||||||
-- crtmod 13, 256-colour graphics plane: one 16-bit word per pixel at \$C00000,
|
-- Where the present puts pixels, which depends on the port's
|
||||||
-- 512 words per row, the 320x200 stage centred at origin (96, 156).
|
-- X68K_PRESENT_TEXT switch:
|
||||||
|
-- text (default) -- crtmod 12, the four 1bpp TEXT planes at \$E00000, 128
|
||||||
|
-- bytes per row, planes 128 KB apart. A pixel's colour is
|
||||||
|
-- one bit from each plane at the same offset.
|
||||||
|
-- gfx -- crtmod 13, 256-colour GRAPHICS plane at \$C00000, one
|
||||||
|
-- 16-bit word per pixel, 512 words per row.
|
||||||
|
-- The 320x200 stage is centred at origin (96, 156) either way.
|
||||||
|
local presentPlane = "$PRESENT_PLANE"
|
||||||
local function stagePixel(px, py)
|
local function stagePixel(px, py)
|
||||||
return mem:read_u8(0xC00000 + ((156 + py) * 512 + 96 + px) * 2 + 1)
|
if presentPlane == "gfx" then
|
||||||
|
return mem:read_u8(0xC00000 + ((156 + py) * 512 + 96 + px) * 2 + 1)
|
||||||
|
end
|
||||||
|
local x = 96 + px
|
||||||
|
local byteOff = (156 + py) * 128 + (x >> 3)
|
||||||
|
local bit = 0x80 >> (x & 7)
|
||||||
|
local v = 0
|
||||||
|
for plane = 0, 3 do
|
||||||
|
if (mem:read_u8(0xE00000 + plane * 0x20000 + byteOff) & bit) ~= 0 then
|
||||||
|
v = v | (1 << plane)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return v
|
||||||
end
|
end
|
||||||
|
|
||||||
local kProbes = { {4,4}, {160,4}, {315,4}, {4,100}, {160,100}, {315,100},
|
local kProbes = { {4,4}, {160,4}, {315,4}, {4,100}, {160,100}, {315,100},
|
||||||
|
|
|
||||||
181
scripts/verify-amiga-audio.sh
Executable file
181
scripts/verify-amiga-audio.sh
Executable file
|
|
@ -0,0 +1,181 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# verify-amiga-audio.sh - Amiga audio gate, graded on recorded sound.
|
||||||
|
#
|
||||||
|
# Paula cannot be tapped register-by-register here: fs-uae has no trace
|
||||||
|
# facility and no debugger this harness can drive, so unlike the IIgs and
|
||||||
|
# ST gates (which read the DOC and the YM2149 directly) this one records
|
||||||
|
# what actually came out of the emulator and grades that.
|
||||||
|
#
|
||||||
|
# Capture path: fs-uae plays through OpenAL, OpenAL is pointed at
|
||||||
|
# PulseAudio with ALSOFT_DRIVERS=pulse, and PULSE_SINK sends this ONE
|
||||||
|
# process to a null sink created for the run -- the host's default sink is
|
||||||
|
# never touched, so the gate is safe to run on a desktop that is playing
|
||||||
|
# something else. parec then records that sink's monitor.
|
||||||
|
#
|
||||||
|
# What is graded: the AUDIO example is driven through four phases whose
|
||||||
|
# expected loudness differs by a wide margin.
|
||||||
|
#
|
||||||
|
# music the baked tracker song on entry, loud
|
||||||
|
# paused P pressed, near silence
|
||||||
|
# sfx SPACE during the pause, so ANY sound must be the digital SFX
|
||||||
|
# resumed P again, loud again
|
||||||
|
#
|
||||||
|
# Firing the SFX into a paused song is the point: mixed under the music an
|
||||||
|
# 8-bit one-shot barely moves the total and "did the SFX play" becomes a
|
||||||
|
# judgement call, while against silence it is unambiguous. Coexistence is
|
||||||
|
# still covered, because the music has to come back afterwards on the same
|
||||||
|
# hardware. Measured margins: music ~5700, paused ~240, sfx ~1170.
|
||||||
|
#
|
||||||
|
# Keys are HELD (keydown, pause, keyup), not tapped: the demo tests
|
||||||
|
# jlKeyPressed, an edge predicate polled once a frame, and a make/break
|
||||||
|
# delivered between two polls is never seen.
|
||||||
|
#
|
||||||
|
# --fullscreen=1 and --initial_input_grab=1 are REQUIRED under a WM-less
|
||||||
|
# Xvfb: SDL only accepts keyboard input once its window has focus, no
|
||||||
|
# window manager is there to grant it, and xdotool's XSendEvent route is
|
||||||
|
# ignored by SDL. A fullscreen SDL window takes focus itself, after which
|
||||||
|
# plain XTEST typing lands. Same reasoning as verify-amiga-input.sh.
|
||||||
|
#
|
||||||
|
# Usage: scripts/verify-amiga-audio.sh [example]
|
||||||
|
# example binary name in build/amiga/bin, default Audio
|
||||||
|
# Env: AMIGA_AUDIO_KEEP set to keep the work dir and the WAV
|
||||||
|
# Requires: fs-uae, Xvfb, xdotool, parec/pactl (PulseAudio or PipeWire),
|
||||||
|
# python3, toolchains/emulators/support/kickstart.rom,
|
||||||
|
# build/amiga/bin/<example>
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
prog=${1:-Audio}
|
||||||
|
binDir=$repo/build/amiga/bin
|
||||||
|
kickstart=$repo/toolchains/emulators/support/kickstart.rom
|
||||||
|
grader=$repo/tools/gradeAudioPhases.py
|
||||||
|
label=verify-amiga-audio
|
||||||
|
|
||||||
|
[ -f "$kickstart" ] || { echo "$label: missing $kickstart" >&2; exit 2; }
|
||||||
|
[ -f "$grader" ] || { echo "$label: missing $grader" >&2; exit 2; }
|
||||||
|
for tool in fs-uae Xvfb xdotool parec pactl python3; do
|
||||||
|
command -v "$tool" >/dev/null || { echo "$label: $tool not installed" >&2; exit 2; }
|
||||||
|
done
|
||||||
|
pactl info >/dev/null 2>&1 || { echo "$label: no PulseAudio/PipeWire server to record from" >&2; exit 2; }
|
||||||
|
|
||||||
|
if [ -d "$binDir/$prog" ]; then
|
||||||
|
progDir=$binDir/$prog
|
||||||
|
elif [ -f "$binDir/$prog" ]; then
|
||||||
|
progDir=""
|
||||||
|
else
|
||||||
|
echo "$label: $binDir/$prog not built (run 'make amiga')" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
work=$(mktemp -d -t joeylib-amaudio.XXXXXX)
|
||||||
|
sinkModule=""
|
||||||
|
display=":$((90 + RANDOM % 9))"
|
||||||
|
xvfbPid=""
|
||||||
|
fsPid=""
|
||||||
|
parecPid=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
[ -n "$parecPid" ] && kill "$parecPid" 2>/dev/null
|
||||||
|
[ -n "$fsPid" ] && kill "$fsPid" 2>/dev/null
|
||||||
|
[ -n "$xvfbPid" ] && kill "$xvfbPid" 2>/dev/null
|
||||||
|
# Always unload the sink: leaving null sinks behind on the user's
|
||||||
|
# audio server is the one side effect this gate could have.
|
||||||
|
[ -n "$sinkModule" ] && pactl unload-module "$sinkModule" 2>/dev/null
|
||||||
|
if [ -n "${AMIGA_AUDIO_KEEP:-}" ]; then
|
||||||
|
echo "$label: kept $work" >&2
|
||||||
|
else
|
||||||
|
rm -rf "$work"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Stage the Amiga volume exactly as run-amiga-headless.sh does.
|
||||||
|
mkdir -p "$work/hd/s"
|
||||||
|
if [ -n "$progDir" ]; then
|
||||||
|
cp -r "$progDir/." "$work/hd/"
|
||||||
|
else
|
||||||
|
find "$binDir" -maxdepth 1 -type f -executable -exec cp -t "$work/hd/" {} +
|
||||||
|
[ -d "$binDir/DATA" ] && cp -r "$binDir/DATA" "$work/hd/"
|
||||||
|
fi
|
||||||
|
printf 'stack 32768\n:%s\n' "$prog" > "$work/hd/s/startup-sequence"
|
||||||
|
|
||||||
|
sinkName="joeyAudioGate$$"
|
||||||
|
sinkModule=$(pactl load-module module-null-sink \
|
||||||
|
sink_name="$sinkName" \
|
||||||
|
sink_properties=device.description="$sinkName" 2>/dev/null) || sinkModule=""
|
||||||
|
[ -n "$sinkModule" ] || { echo "$label: could not create a null sink to record from" >&2; exit 2; }
|
||||||
|
|
||||||
|
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
|
||||||
|
xvfbPid=$!
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# --fast_memory=0 --slow_memory=0 keeps this a bare A500, the port's floor.
|
||||||
|
DISPLAY="$display" PULSE_SINK="$sinkName" ALSOFT_DRIVERS=pulse SDL_AUDIODRIVER=pulse \
|
||||||
|
fs-uae \
|
||||||
|
--amiga_model=A500 \
|
||||||
|
--hard_drive_0="$work/hd" \
|
||||||
|
--hard_drive_0_label=JOEYLIB \
|
||||||
|
--kickstart_file="$kickstart" \
|
||||||
|
--fullscreen=1 \
|
||||||
|
--initial_input_grab=1 \
|
||||||
|
--fade_out_duration=0 \
|
||||||
|
--fast_memory=0 \
|
||||||
|
--slow_memory=0 \
|
||||||
|
> "$work/fsuae.log" 2>&1 &
|
||||||
|
fsPid=$!
|
||||||
|
|
||||||
|
# Let Kickstart boot and the startup-sequence reach the program.
|
||||||
|
sleep 12
|
||||||
|
if ! kill -0 "$fsPid" 2>/dev/null; then
|
||||||
|
echo "$label: FAIL - fs-uae exited before the program started" >&2
|
||||||
|
tail -5 "$work/fsuae.log" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
parec --device="$sinkName.monitor" --format=s16le --rate=44100 --channels=2 \
|
||||||
|
--file-format=wav "$work/out.wav" > "$work/parec.log" 2>&1 &
|
||||||
|
parecPid=$!
|
||||||
|
|
||||||
|
win=$(DISPLAY="$display" xdotool search --name -- "FS-UAE" | head -1)
|
||||||
|
if [ -z "$win" ]; then
|
||||||
|
echo "$label: FAIL - no FS-UAE window to send keys to" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
DISPLAY="$display" xdotool windowactivate --sync "$win" 2>/dev/null
|
||||||
|
|
||||||
|
hold() { # $1 = key name
|
||||||
|
DISPLAY="$display" xdotool keydown --window "$win" "$1"
|
||||||
|
sleep 0.4
|
||||||
|
DISPLAY="$display" xdotool keyup --window "$win" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The phase boundaries below are the timestamps handed to the grader, so
|
||||||
|
# the sleeps and the spec must stay in step.
|
||||||
|
sleep 6 # 0-6s music
|
||||||
|
hold p # -> paused
|
||||||
|
sleep 8 # 6-14s paused
|
||||||
|
for _ in 1 2 3; do # 14-24s sfx into silence
|
||||||
|
hold space
|
||||||
|
sleep 1.5
|
||||||
|
done
|
||||||
|
sleep 4
|
||||||
|
hold p # -> resumed
|
||||||
|
sleep 9 # 24-33s resumed
|
||||||
|
|
||||||
|
kill "$parecPid" 2>/dev/null
|
||||||
|
parecPid=""
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
[ -s "$work/out.wav" ] || { echo "$label: FAIL - recorded no audio at all" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Thresholds sit far from the measured values (music ~5700, paused ~240,
|
||||||
|
# sfx ~1170) so ordinary mixing jitter cannot flip the verdict.
|
||||||
|
python3 "$grader" "$label" "$work/out.wav" \
|
||||||
|
"music:1:5:2000:" \
|
||||||
|
"paused:8:13::800" \
|
||||||
|
"sfx:16:22:800:" \
|
||||||
|
"resumed:27:32:2000:"
|
||||||
|
status=$?
|
||||||
|
[ -n "${AMIGA_AUDIO_KEEP:-}" ] && cp "$work/out.wav" "$repo/build/amiga/headless-audio.wav" 2>/dev/null
|
||||||
|
exit $status
|
||||||
279
scripts/verify-atarist-audio.sh
Executable file
279
scripts/verify-atarist-audio.sh
Executable file
|
|
@ -0,0 +1,279 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# verify-atarist-audio.sh - Atari ST audio gate, read off the YM2149.
|
||||||
|
#
|
||||||
|
# The ST has ONE sound chip and two software layers competing for it, so
|
||||||
|
# this gate traces every PSG register write and grades the competition
|
||||||
|
# rather than listening to the output. Four things are checked:
|
||||||
|
#
|
||||||
|
# 1. The chip-tracker is alive. jlMusicPlay drives the three PSG voices
|
||||||
|
# through reg 0-5 (12-bit tone period) and reg 8/9/A (4-bit volume),
|
||||||
|
# gated by reg 7. Writes must come from the APPLICATION, not TOS --
|
||||||
|
# the trace carries the pc of every write and TOS lives at $Exxxxx,
|
||||||
|
# which is the discriminator used throughout.
|
||||||
|
#
|
||||||
|
# 2. The digital pump runs at its REAL rate. Sampled sound on the ST is
|
||||||
|
# PWM through channel A's volume nibble: MFP Timer A at 2.4576 MHz /
|
||||||
|
# 200 = 12288 Hz, and src/atarist/audio.c writes reg 8 on every one
|
||||||
|
# of those ticks. Per frame that is 12288/60 = 205 (measured) or
|
||||||
|
# 12288/50 = 246, against the tracker's ONE. The two populations
|
||||||
|
# cannot be confused, so a per-frame count answers both "did the
|
||||||
|
# pump start" and "is the divider right" -- a halved divider would
|
||||||
|
# show ~103 and still sound like something.
|
||||||
|
#
|
||||||
|
# 3. Reg 8 has exactly ONE writer while the pump runs. This is the bug
|
||||||
|
# the gate exists for: the tracker's voice 0 and the PWM pump both
|
||||||
|
# own reg 8, and whoever wrote last wins, so a tracker that keeps
|
||||||
|
# writing it chops the sample into silence. The pc in the trace
|
||||||
|
# makes the writers countable. Each pump run's first and last frame
|
||||||
|
# are excluded: pumpStart and uninstallTimerA legitimately touch
|
||||||
|
# reg 8 from outside the ISR (the latter replays the latched voice).
|
||||||
|
#
|
||||||
|
# 4. They actually OVERLAP. "No collision" is worthless if it was
|
||||||
|
# bought by silencing the music, so the tracker must still be
|
||||||
|
# driving its own channel B inside the pump window.
|
||||||
|
#
|
||||||
|
# Subject: the AUDIO example, which starts the baked tracker song on
|
||||||
|
# entry and fires a digital SFX on SPACE -- precisely the overlap. Note
|
||||||
|
# SPACE is HELD (keydown, wait, keyup) rather than tapped: the demo tests
|
||||||
|
# jlKeyPressed, an edge predicate polled once a frame, and a make/break
|
||||||
|
# delivered between two polls is never seen. A bare `keypress` produces a
|
||||||
|
# run with the tracker alone and no pump at all.
|
||||||
|
#
|
||||||
|
# STAXI is a poor subject here despite exercising both layers: it STOPS
|
||||||
|
# the music while speech plays, so check 4 can never pass on it.
|
||||||
|
#
|
||||||
|
# Usage: scripts/verify-atarist-audio.sh [example]
|
||||||
|
# example PRG base name in build/atarist/bin, default AUDIO
|
||||||
|
# Env: ST_AUDIO_TAPS how many held SPACE presses (default 6)
|
||||||
|
# ST_AUDIO_KEEP set to keep the work dir for inspection
|
||||||
|
# Requires: hatari, toolchains/emulators/support/emutos-512k.img,
|
||||||
|
# build/atarist/bin/<example>.PRG
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
prog=${1:-AUDIO}
|
||||||
|
taps=${ST_AUDIO_TAPS:-6}
|
||||||
|
tos=$repo/toolchains/emulators/support/emutos-512k.img
|
||||||
|
binDir=$repo/build/atarist/bin
|
||||||
|
|
||||||
|
# Space Taxi ships as its own folder; everything else is a bare PRG.
|
||||||
|
if [ -d "$binDir/$prog" ]; then
|
||||||
|
driveDir=$binDir/$prog
|
||||||
|
else
|
||||||
|
driveDir=$binDir
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -f "$driveDir/$prog.PRG" ] || { echo "verify-atarist-audio: missing $driveDir/$prog.PRG (run 'make atarist')" >&2; exit 2; }
|
||||||
|
[ -f "$tos" ] || { echo "verify-atarist-audio: missing $tos" >&2; exit 2; }
|
||||||
|
|
||||||
|
work=$(mktemp -d -t joeylib-staudio.XXXXXX)
|
||||||
|
hatariPid=""
|
||||||
|
cleanup() {
|
||||||
|
# SIGKILL, by PID. Three things are deliberate here:
|
||||||
|
# * SIGKILL, because Hatari IGNORES SIGTERM -- measured: a plain
|
||||||
|
# kill, and `pkill -x hatari`, both leave it running forever.
|
||||||
|
# * by PID, not by name, so a Hatari the user has open survives.
|
||||||
|
# * no `timeout -s KILL` wrapper, because that would make $! the
|
||||||
|
# timeout and killing THAT orphans Hatari. The trap is the only
|
||||||
|
# thing that needs to work, and it covers EXIT/INT/TERM.
|
||||||
|
# The launch uses process substitution so $! is Hatari itself rather
|
||||||
|
# than the filter at the end of a pipe.
|
||||||
|
[ -n "$hatariPid" ] && { kill -KILL "$hatariPid"; wait "$hatariPid"; } 2>/dev/null
|
||||||
|
[ -n "${ST_AUDIO_KEEP:-}" ] || rm -rf "$work"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
[ -n "${ST_AUDIO_KEEP:-}" ] && echo "verify-atarist-audio: work dir $work" >&2
|
||||||
|
|
||||||
|
# 4 MB, not the stock 1 MB: STAXI's loader calls 1 MB a fatal asset
|
||||||
|
# failure and this gate is about sound, not about memory.
|
||||||
|
#
|
||||||
|
# The psg_write trace runs ~800 KB/s, so it is filtered as it is produced
|
||||||
|
# -- keeping the raw stream would put hundreds of MB in the work dir.
|
||||||
|
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy hatari \
|
||||||
|
--tos "$tos" \
|
||||||
|
--harddrive "$driveDir" \
|
||||||
|
--gemdos-drive C \
|
||||||
|
--drive-a off --drive-b off \
|
||||||
|
--sound 44100 \
|
||||||
|
--memsize 4 \
|
||||||
|
--fast-forward on \
|
||||||
|
--cmd-fifo "$work/cmd.fifo" \
|
||||||
|
--trace psg_write \
|
||||||
|
--auto "C:\\$prog.PRG" \
|
||||||
|
> >(grep --line-buffered -E '^ym write data' > "$work/ym.log") 2>&1 &
|
||||||
|
hatariPid=$!
|
||||||
|
|
||||||
|
# Hatari CREATES the fifo; a write to one with no reader blocks forever,
|
||||||
|
# so every command goes through a timeout.
|
||||||
|
for _ in $(seq 1 25); do
|
||||||
|
[ -p "$work/cmd.fifo" ] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
if [ ! -p "$work/cmd.fifo" ]; then
|
||||||
|
echo "verify-atarist-audio: FAIL - hatari never created the command fifo" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
send() { timeout 5 bash -c "echo \"\$1\" > \"$work/cmd.fifo\"" _ "$1"; }
|
||||||
|
|
||||||
|
# Wait for the APPLICATION to write the PSG. TOS writes it too (bell,
|
||||||
|
# floppy seek), and every TOS pc starts with 'e'.
|
||||||
|
waited=0
|
||||||
|
until grep -qE 'pc=[^e]' "$work/ym.log" 2>/dev/null; do
|
||||||
|
sleep 2
|
||||||
|
waited=$((waited + 2))
|
||||||
|
if [ "$waited" -ge 40 ]; then
|
||||||
|
echo "verify-atarist-audio: FAIL - $prog never wrote the PSG in ${waited}s (the tracker never started)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! kill -0 "$hatariPid" 2>/dev/null; then
|
||||||
|
echo "verify-atarist-audio: FAIL - Hatari exited early" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Let the tracker settle, then snapshot a pump-free baseline.
|
||||||
|
sleep 8
|
||||||
|
cp "$work/ym.log" "$work/baseline.log"
|
||||||
|
|
||||||
|
# HOLD space so the once-a-frame jlKeyPressed poll can see the edge.
|
||||||
|
i=0
|
||||||
|
while [ "$i" -lt "$taps" ]; do
|
||||||
|
send 'hatari-event keydown 57' || true # $39 = SPACE
|
||||||
|
sleep 1
|
||||||
|
send 'hatari-event keyup 57' || true
|
||||||
|
sleep 1
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
sleep 3
|
||||||
|
# The braces + wait suppress bash's "Killed" job notice on stderr, which
|
||||||
|
# would otherwise look like a gate error in the output.
|
||||||
|
{ kill -KILL "$hatariPid"; wait "$hatariPid"; } 2>/dev/null
|
||||||
|
hatariPid=""
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
[ -s "$work/ym.log" ] || { echo "verify-atarist-audio: FAIL - empty PSG trace" >&2; exit 1; }
|
||||||
|
|
||||||
|
python3 - "$work/ym.log" "$work/baseline.log" "$prog" "$taps" <<'PY'
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# ym write data reg=0x8 val=0xc video_cyc=8696 60@17 pc=e1197e instr_cycle 16
|
||||||
|
LINE = re.compile(r'^ym write data reg=0x([0-9a-f]+) val=0x([0-9a-f]+) '
|
||||||
|
r'video_cyc=(\d+).*pc=([0-9a-f]+)')
|
||||||
|
|
||||||
|
PUMP_PER_FRAME_MIN = 100 # tracker writes <= 1; the pump writes 205-246
|
||||||
|
PUMP_RATE_LO = 150 # 12288/60 = 205, 12288/50 = 246. Below this the
|
||||||
|
PUMP_RATE_HI = 280 # Timer A divider is wrong, not the sound.
|
||||||
|
STRAY_FRAME_PCT_MAX = 5.0 # start/stop transitions only; a fighting
|
||||||
|
# tracker touches reg 8 EVERY frame.
|
||||||
|
REG_VOL_A = 8
|
||||||
|
REG_MIXER = 7
|
||||||
|
TONE_B_REGS = (2, 3) # channel B period == the tracker's own voice
|
||||||
|
|
||||||
|
|
||||||
|
def perFrameWrites(path):
|
||||||
|
"""{frame: {reg: [pc, ...]}} for APPLICATION writes only."""
|
||||||
|
out = {}
|
||||||
|
frame = 0
|
||||||
|
prev = -1
|
||||||
|
for line in open(path, errors='ignore'):
|
||||||
|
m = LINE.match(line)
|
||||||
|
if m is None:
|
||||||
|
continue
|
||||||
|
reg = int(m.group(1), 16)
|
||||||
|
cyc = int(m.group(3))
|
||||||
|
pc = m.group(4)
|
||||||
|
if cyc < prev: # video_cyc restarts every frame
|
||||||
|
frame += 1
|
||||||
|
prev = cyc
|
||||||
|
if pc.startswith('e'): # TOS, not us
|
||||||
|
continue
|
||||||
|
out.setdefault(frame, {}).setdefault(reg, []).append(pc)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def countReg(frames, table, regs):
|
||||||
|
return sum(len(table[f].get(r, ())) for f in frames for r in regs)
|
||||||
|
|
||||||
|
|
||||||
|
full = perFrameWrites(sys.argv[1])
|
||||||
|
base = perFrameWrites(sys.argv[2])
|
||||||
|
prog = sys.argv[3]
|
||||||
|
taps = int(sys.argv[4])
|
||||||
|
if not full:
|
||||||
|
print("verify-atarist-audio: FAIL - no application PSG writes at all", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
frames = sorted(full)
|
||||||
|
fails = []
|
||||||
|
|
||||||
|
# 1. The tracker ran before any SFX overlapped it.
|
||||||
|
baseFrames = sorted(base)
|
||||||
|
baseTone = countReg(baseFrames, base, TONE_B_REGS)
|
||||||
|
baseMixer = countReg(baseFrames, base, (REG_MIXER,))
|
||||||
|
if baseTone == 0 or baseMixer == 0:
|
||||||
|
fails.append(f"the tracker never played: {baseTone} channel-B period writes and "
|
||||||
|
f"{baseMixer} mixer writes before the SFX. jlMusicPlay did not drive the PSG")
|
||||||
|
|
||||||
|
# 2. The pump ran, at the right rate. Group pump frames into contiguous runs
|
||||||
|
# so each SFX can be told from the next.
|
||||||
|
runs = []
|
||||||
|
current = []
|
||||||
|
for f in frames:
|
||||||
|
if len(full[f].get(REG_VOL_A, ())) >= PUMP_PER_FRAME_MIN:
|
||||||
|
current.append(f)
|
||||||
|
elif current:
|
||||||
|
runs.append(current)
|
||||||
|
current = []
|
||||||
|
if current:
|
||||||
|
runs.append(current)
|
||||||
|
|
||||||
|
peak = max((len(full[f].get(REG_VOL_A, ())) for f in frames), default=0)
|
||||||
|
if not runs:
|
||||||
|
fails.append(f"the PWM pump never ran: peak {peak} reg-8 writes in a frame, want "
|
||||||
|
f">= {PUMP_PER_FRAME_MIN}. jlAudioPlaySfx did not start Timer A, so "
|
||||||
|
f"every digital sound on this port is silent")
|
||||||
|
elif not (PUMP_RATE_LO <= peak <= PUMP_RATE_HI):
|
||||||
|
fails.append(f"the pump ran at {peak} reg-8 writes/frame, want {PUMP_RATE_LO}-{PUMP_RATE_HI} "
|
||||||
|
f"(12288 Hz / 50-60 fps). The MFP Timer A divider is wrong, so sampled "
|
||||||
|
f"sound plays at the wrong pitch")
|
||||||
|
|
||||||
|
summary = f"{prog} tracker baseline {baseTone} toneB/{baseMixer} mixer"
|
||||||
|
if runs:
|
||||||
|
# 3 + 4. Inside each run, minus its first and last frame.
|
||||||
|
interior = [f for r in runs for f in (r[1:-1] if len(r) > 2 else [])]
|
||||||
|
writers = {}
|
||||||
|
for f in interior:
|
||||||
|
for pc in full[f].get(REG_VOL_A, ()):
|
||||||
|
writers[pc] = writers.get(pc, 0) + 1
|
||||||
|
ranked = sorted(writers.items(), key=lambda kv: -kv[1])
|
||||||
|
isr = ranked[0][0] if ranked else None
|
||||||
|
strayFrames = sum(1 for f in interior
|
||||||
|
if any(pc != isr for pc in full[f].get(REG_VOL_A, ())))
|
||||||
|
strayPct = (100.0 * strayFrames / len(interior)) if interior else 0.0
|
||||||
|
if strayPct > STRAY_FRAME_PCT_MAX:
|
||||||
|
others = ", ".join(f"pc={pc} x{n}" for pc, n in ranked[1:4])
|
||||||
|
fails.append(f"reg 8 (volume A) has {len(ranked)} writers in {strayPct:.1f}% of the "
|
||||||
|
f"pump's frames: ISR pc={isr} plus {others}. The tracker is overwriting "
|
||||||
|
f"the PWM sample - digital and chiptune are colliding on channel A")
|
||||||
|
|
||||||
|
toneDuring = countReg(interior, full, TONE_B_REGS)
|
||||||
|
if toneDuring == 0:
|
||||||
|
fails.append("the tracker stopped writing its own channel B while the pump ran, so "
|
||||||
|
"the SFX silenced the music instead of mixing with it")
|
||||||
|
|
||||||
|
summary += (f"; pump {len(runs)} runs (expected {taps}) peak {peak} reg8/frame, "
|
||||||
|
f"{len(writers)} reg8 writer(s), stray frames {strayPct:.1f}%, "
|
||||||
|
f"tracker toneB during pump {toneDuring}")
|
||||||
|
else:
|
||||||
|
summary += f"; pump never ran (peak {peak} reg8/frame)"
|
||||||
|
|
||||||
|
print(f"verify-atarist-audio: {summary}")
|
||||||
|
for f in fails:
|
||||||
|
print(f"verify-atarist-audio: {f}", file=sys.stderr)
|
||||||
|
if fails:
|
||||||
|
print("verify-atarist-audio: FAIL", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print("verify-atarist-audio: PASS")
|
||||||
|
PY
|
||||||
176
scripts/verify-dos-audio.sh
Executable file
176
scripts/verify-dos-audio.sh
Executable file
|
|
@ -0,0 +1,176 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# verify-dos-audio.sh - DOS audio gate, graded on recorded sound.
|
||||||
|
#
|
||||||
|
# The DOS HAL drives a Sound Blaster Pro/SB16 with 8237 auto-init DMA fed
|
||||||
|
# by libxmp-lite (src/dos/audio.c). DOSBox 0.74 exposes none of that to a
|
||||||
|
# script -- no register trace, no debugger this harness can drive -- so
|
||||||
|
# unlike the IIgs and ST gates (which read the DOC and the YM2149) this
|
||||||
|
# one records what actually came out and grades it. The Amiga gate works
|
||||||
|
# the same way for the same reason, and both share tools/gradeAudioPhases.py.
|
||||||
|
#
|
||||||
|
# Capture path: DOSBox plays through SDL, SDL is pointed at PulseAudio,
|
||||||
|
# and PULSE_SINK sends this ONE process to a null sink created for the
|
||||||
|
# run, so the host's default sink is never touched and the gate is safe on
|
||||||
|
# a desktop that is playing something else. parec records that sink's
|
||||||
|
# monitor. Note the WAV header is only finalised when parec exits, so it
|
||||||
|
# is stopped and waited for before the file is read.
|
||||||
|
#
|
||||||
|
# What is graded: the AUDIO example is driven through four phases whose
|
||||||
|
# expected loudness differs by a wide margin.
|
||||||
|
#
|
||||||
|
# music the baked tracker song on entry, loud
|
||||||
|
# paused P pressed, near silence
|
||||||
|
# sfx SPACE during the pause, so ANY sound must be the digital SFX
|
||||||
|
# resumed P again, loud again
|
||||||
|
#
|
||||||
|
# Firing the SFX into a paused song is the point: mixed under the music an
|
||||||
|
# 8-bit one-shot barely moves the total, while against silence it is
|
||||||
|
# unambiguous. This also covers the SMIX overlay specifically -- SFX on
|
||||||
|
# DOS mix through libxmp-lite's smix rather than a separate channel, so
|
||||||
|
# "SFX audible while the module player is loaded but paused" is exactly
|
||||||
|
# the path that can break. Measured: music ~3290, paused ~113, sfx ~1341.
|
||||||
|
#
|
||||||
|
# Keys are HELD (keydown, pause, keyup), not tapped: the demo tests
|
||||||
|
# jlKeyPressed, an edge predicate polled once a frame, and a make/break
|
||||||
|
# delivered between two polls is never seen.
|
||||||
|
#
|
||||||
|
# Usage: scripts/verify-dos-audio.sh [example]
|
||||||
|
# example EXE base name in build/dos/bin, default AUDIO
|
||||||
|
# Env: DOS_AUDIO_KEEP set to keep the work dir and the WAV
|
||||||
|
# CYCLES override the conf's 386SX-16 cpu cap
|
||||||
|
# Requires: dosbox, Xvfb, xdotool, parec/pactl (PulseAudio or PipeWire),
|
||||||
|
# python3, build/dos/bin/<example>.EXE
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
prog=${1:-AUDIO}
|
||||||
|
binDir=$repo/build/dos/bin
|
||||||
|
conf=$repo/scripts/dosbox-386sx16.conf
|
||||||
|
grader=$repo/tools/gradeAudioPhases.py
|
||||||
|
label=verify-dos-audio
|
||||||
|
|
||||||
|
[ -f "$grader" ] || { echo "$label: missing $grader" >&2; exit 2; }
|
||||||
|
[ -f "$conf" ] || { echo "$label: missing $conf" >&2; exit 2; }
|
||||||
|
for tool in dosbox Xvfb xdotool parec pactl python3; do
|
||||||
|
command -v "$tool" >/dev/null || { echo "$label: $tool not installed" >&2; exit 2; }
|
||||||
|
done
|
||||||
|
pactl info >/dev/null 2>&1 || { echo "$label: no PulseAudio/PipeWire server to record from" >&2; exit 2; }
|
||||||
|
|
||||||
|
# Space Taxi ships as its own folder; everything else is a bare EXE.
|
||||||
|
if [ -f "$binDir/$prog/$prog.EXE" ]; then
|
||||||
|
mountDir=$binDir/$prog
|
||||||
|
elif [ -f "$binDir/$prog.EXE" ]; then
|
||||||
|
mountDir=$binDir
|
||||||
|
else
|
||||||
|
echo "$label: $binDir/$prog.EXE not built (run 'make dos')" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
work=$(mktemp -d -t joeylib-dosaudio.XXXXXX)
|
||||||
|
sinkModule=""
|
||||||
|
display=":$((90 + RANDOM % 9))"
|
||||||
|
xvfbPid=""
|
||||||
|
dosPid=""
|
||||||
|
parecPid=""
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
[ -n "$parecPid" ] && kill "$parecPid" 2>/dev/null
|
||||||
|
[ -n "$dosPid" ] && kill "$dosPid" 2>/dev/null
|
||||||
|
[ -n "$xvfbPid" ] && kill "$xvfbPid" 2>/dev/null
|
||||||
|
# Always unload the sink: a leaked null sink on the user's audio
|
||||||
|
# server is the one side effect this gate could have.
|
||||||
|
[ -n "$sinkModule" ] && pactl unload-module "$sinkModule" 2>/dev/null
|
||||||
|
if [ -n "${DOS_AUDIO_KEEP:-}" ]; then
|
||||||
|
echo "$label: kept $work" >&2
|
||||||
|
else
|
||||||
|
rm -rf "$work"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
sinkName="joeyDosGate$$"
|
||||||
|
sinkModule=$(pactl load-module module-null-sink \
|
||||||
|
sink_name="$sinkName" \
|
||||||
|
sink_properties=device.description="$sinkName" 2>/dev/null) || sinkModule=""
|
||||||
|
[ -n "$sinkModule" ] || { echo "$label: could not create a null sink to record from" >&2; exit 2; }
|
||||||
|
|
||||||
|
Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp >/dev/null 2>&1 &
|
||||||
|
xvfbPid=$!
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
dosArgs=(-conf "$conf")
|
||||||
|
[ -n "${CYCLES:-}" ] && dosArgs+=(-set "cpu cycles=$CYCLES")
|
||||||
|
dosArgs+=(
|
||||||
|
-c "MOUNT C $mountDir"
|
||||||
|
-c "C:"
|
||||||
|
-c "$prog.EXE"
|
||||||
|
)
|
||||||
|
|
||||||
|
DISPLAY="$display" PULSE_SINK="$sinkName" SDL_AUDIODRIVER=pulse \
|
||||||
|
dosbox "${dosArgs[@]}" > "$work/dosbox.log" 2>&1 &
|
||||||
|
dosPid=$!
|
||||||
|
|
||||||
|
sleep 12
|
||||||
|
if ! kill -0 "$dosPid" 2>/dev/null; then
|
||||||
|
echo "$label: FAIL - DOSBox exited before the program started" >&2
|
||||||
|
tail -5 "$work/dosbox.log" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
win=$(DISPLAY="$display" xdotool search --name -- "DOSBox" | head -1)
|
||||||
|
if [ -z "$win" ]; then
|
||||||
|
echo "$label: FAIL - no DOSBox window to send keys to" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# The title carries the running program, which is a free check that the
|
||||||
|
# EXE actually launched rather than dropping to a DOS prompt.
|
||||||
|
title=$(DISPLAY="$display" xdotool getwindowname "$win" 2>/dev/null)
|
||||||
|
case "$title" in
|
||||||
|
*"$prog"*) ;;
|
||||||
|
*) echo "$label: FAIL - DOSBox is not running $prog (window title: $title)" >&2
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
parec --device="$sinkName.monitor" --format=s16le --rate=44100 --channels=2 \
|
||||||
|
--file-format=wav "$work/out.wav" > "$work/parec.log" 2>&1 &
|
||||||
|
parecPid=$!
|
||||||
|
|
||||||
|
DISPLAY="$display" xdotool windowactivate --sync "$win" 2>/dev/null
|
||||||
|
|
||||||
|
hold() { # $1 = key name
|
||||||
|
DISPLAY="$display" xdotool keydown --window "$win" "$1"
|
||||||
|
sleep 0.4
|
||||||
|
DISPLAY="$display" xdotool keyup --window "$win" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The phase boundaries below are the timestamps handed to the grader, so
|
||||||
|
# the sleeps and the spec must stay in step.
|
||||||
|
sleep 6 # 0-6s music
|
||||||
|
hold p # -> paused
|
||||||
|
sleep 8 # 6-14s paused
|
||||||
|
for _ in 1 2 3; do # 14-24s sfx into silence
|
||||||
|
hold space
|
||||||
|
sleep 1.5
|
||||||
|
done
|
||||||
|
sleep 4
|
||||||
|
hold p # -> resumed
|
||||||
|
sleep 9 # 24-33s resumed
|
||||||
|
|
||||||
|
# parec writes the WAV header lazily; it must exit before the file parses.
|
||||||
|
kill "$parecPid" 2>/dev/null
|
||||||
|
wait "$parecPid" 2>/dev/null
|
||||||
|
parecPid=""
|
||||||
|
|
||||||
|
[ -s "$work/out.wav" ] || { echo "$label: FAIL - recorded no audio at all" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Thresholds sit far from the measured values (music ~3290, paused ~113,
|
||||||
|
# sfx ~1341) so ordinary mixing jitter cannot flip the verdict.
|
||||||
|
python3 "$grader" "$label" "$work/out.wav" \
|
||||||
|
"music:1:5:1500:" \
|
||||||
|
"paused:8:13::600" \
|
||||||
|
"sfx:16:22:700:" \
|
||||||
|
"resumed:27:32:1500:"
|
||||||
|
status=$?
|
||||||
|
[ -n "${DOS_AUDIO_KEEP:-}" ] && cp "$work/out.wav" "$repo/build/dos/headless-audio.wav" 2>/dev/null
|
||||||
|
exit $status
|
||||||
|
|
@ -25,6 +25,10 @@ TEMPLATE="$SP/x68mame/HUMAN302.XDF"
|
||||||
GOLDEN="${X68K_GOLDEN:-$repo/tests/goldens/uber/iigs.txt}"
|
GOLDEN="${X68K_GOLDEN:-$repo/tests/goldens/uber/iigs.txt}"
|
||||||
FRAMES="${X68K_GOLDEN_FRAMES:-60000}"
|
FRAMES="${X68K_GOLDEN_FRAMES:-60000}"
|
||||||
WALL="${X68K_GOLDEN_WALL:-7200}"
|
WALL="${X68K_GOLDEN_WALL:-7200}"
|
||||||
|
# Which plane the built port presents into: must match X68K_PRESENT_TEXT in
|
||||||
|
# src/x68000/hal.c (1 -> text, 0 -> gfx). Only the completion DETECTOR cares;
|
||||||
|
# the hashes are of the surface in RAM and are the same either way.
|
||||||
|
PRESENT_PLANE="${X68K_PRESENT_PLANE:-text}"
|
||||||
|
|
||||||
export PATH="$repo/toolchains/x68000/m68k-xelf/bin:$PATH"
|
export PATH="$repo/toolchains/x68000/m68k-xelf/bin:$PATH"
|
||||||
work=$(mktemp -d -t joey-x68gold.XXXXXX)
|
work=$(mktemp -d -t joey-x68gold.XXXXXX)
|
||||||
|
|
@ -86,10 +90,29 @@ local FRAMES = $FRAMES
|
||||||
local GREEN_STABLE = 600 -- ~11 s of emulated time held steady
|
local GREEN_STABLE = 600 -- ~11 s of emulated time held steady
|
||||||
local EXIT_SETTLE = 900 -- keypress -> fclose -> back at the Human68k prompt
|
local EXIT_SETTLE = 900 -- keypress -> fclose -> back at the Human68k prompt
|
||||||
|
|
||||||
-- crtmod 13, 256-colour graphics plane: one 16-bit word per pixel at \$C00000,
|
-- Where the present puts pixels, which depends on the port's
|
||||||
-- 512 words per row, the 320x200 stage centred at origin (96, 156).
|
-- X68K_PRESENT_TEXT switch:
|
||||||
|
-- text (default) -- crtmod 12, the four 1bpp TEXT planes at \$E00000, 128
|
||||||
|
-- bytes per row, planes 128 KB apart. A pixel's colour is
|
||||||
|
-- one bit from each plane at the same offset.
|
||||||
|
-- gfx -- crtmod 13, 256-colour GRAPHICS plane at \$C00000, one
|
||||||
|
-- 16-bit word per pixel, 512 words per row.
|
||||||
|
-- The 320x200 stage is centred at origin (96, 156) either way.
|
||||||
|
local presentPlane = "$PRESENT_PLANE"
|
||||||
local function stagePixel(px, py)
|
local function stagePixel(px, py)
|
||||||
return mem:read_u8(0xC00000 + ((156 + py) * 512 + 96 + px) * 2 + 1)
|
if presentPlane == "gfx" then
|
||||||
|
return mem:read_u8(0xC00000 + ((156 + py) * 512 + 96 + px) * 2 + 1)
|
||||||
|
end
|
||||||
|
local x = 96 + px
|
||||||
|
local byteOff = (156 + py) * 128 + (x >> 3)
|
||||||
|
local bit = 0x80 >> (x & 7)
|
||||||
|
local v = 0
|
||||||
|
for plane = 0, 3 do
|
||||||
|
if (mem:read_u8(0xE00000 + plane * 0x20000 + byteOff) & bit) ~= 0 then
|
||||||
|
v = v | (1 << plane)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return v
|
||||||
end
|
end
|
||||||
|
|
||||||
local kProbes = { {4,4}, {160,4}, {315,4}, {4,100}, {160,100}, {315,100},
|
local kProbes = { {4,4}, {160,4}, {315,4}, {4,100}, {160,100}, {315,100},
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,12 @@
|
||||||
// whose 4-bit volume reg 0x0A is SHARED with tone voice 2 -- see the
|
// whose 4-bit volume reg 0x0A is SHARED with tone voice 2 -- see the
|
||||||
// guards in applyYmVoice/applyYmNoise.
|
// guards in applyYmVoice/applyYmNoise.
|
||||||
#define YM_REG_NOISE_PERIOD 6u
|
#define YM_REG_NOISE_PERIOD 6u
|
||||||
|
#define YM_REG_VOL_A 8u
|
||||||
|
#define YM_REG_VOL_B 9u
|
||||||
#define YM_REG_VOL_C 10u
|
#define YM_REG_VOL_C 10u
|
||||||
|
#define YM_MIX_TONE_A 0x01u
|
||||||
#define YM_MIX_TONE_C 0x04u
|
#define YM_MIX_TONE_C 0x04u
|
||||||
|
#define YM_MIX_NOISE_A 0x08u
|
||||||
#define YM_MIX_NOISE_C 0x20u
|
#define YM_MIX_NOISE_C 0x20u
|
||||||
|
|
||||||
#define MFP_TA_BIT 0x20
|
#define MFP_TA_BIT 0x20
|
||||||
|
|
@ -114,17 +118,55 @@ static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS];
|
||||||
// can't be read back, so this shadow IS the authoritative state.
|
// can't be read back, so this shadow IS the authoritative state.
|
||||||
static volatile uint8_t gYmToneMask = 0x3Fu;
|
static volatile uint8_t gYmToneMask = 0x3Fu;
|
||||||
|
|
||||||
|
// Supexec'd workers take no parameters of their own, so each request is
|
||||||
|
// stashed here first. Declared up with the module state rather than beside
|
||||||
|
// applyYmVoice because uninstallTimerA replays a voice through them.
|
||||||
|
static volatile uint8_t gYmVoice;
|
||||||
|
static volatile uint16_t gYmFreqHz;
|
||||||
|
static volatile uint8_t gYmAtten;
|
||||||
|
static volatile uint8_t gYmNoisePitch;
|
||||||
|
static volatile uint8_t gYmNoiseAtten;
|
||||||
|
|
||||||
|
// Voice 0's last requested state, latched whether or not it could be applied.
|
||||||
|
// While the PWM pump owns channel A the voice cannot sound, and this is what
|
||||||
|
// gets replayed to bring it back when the pump stops. Starts silent.
|
||||||
|
static volatile uint16_t gVoiceAFreqHz;
|
||||||
|
static volatile uint8_t gVoiceAAtten = 15u;
|
||||||
|
|
||||||
// ----- Internal helpers -----
|
// ----- Internal helpers -----
|
||||||
|
|
||||||
// Supexec workers defined further down (with the PSG voice cluster)
|
// Supexec workers defined further down (with the PSG voice cluster)
|
||||||
// but needed by the early lifecycle functions.
|
// but needed by the early lifecycle functions.
|
||||||
static long ymPsgAllOff(void);
|
static long ymPsgAllOff(void);
|
||||||
|
static long applyYmVoice(void);
|
||||||
|
|
||||||
|
// Writing a YM register is a SELECT store then a DATA store, and the PWM ISR
|
||||||
|
// writes register 8 on every one of its 12288 ticks a second. If it lands
|
||||||
|
// between the two, the foreground's data byte goes to register 8 -- a click in
|
||||||
|
// the digital output -- and the register it was aimed at never receives it,
|
||||||
|
// which is a wrong or missing note. Nothing arbitrated that, so digital plus
|
||||||
|
// ANY PSG activity corrupted both, on all three channels and not just the one
|
||||||
|
// they share.
|
||||||
|
//
|
||||||
|
// So a foreground write masks interrupts across the pair. It costs a few
|
||||||
|
// cycles on the handful of writes a frame rather than anything per sample,
|
||||||
|
// which matters because the ISR already eats about half the CPU. The ISR
|
||||||
|
// itself needs no such guard: it is the only other writer.
|
||||||
|
static void ymWrite(uint8_t reg, uint8_t val) {
|
||||||
|
uint16_t saved;
|
||||||
|
|
||||||
|
__asm__ volatile ("move.w %%sr,%0\n\tori.w #0x0700,%%sr"
|
||||||
|
: "=d" (saved) : : "memory");
|
||||||
|
*ST_YM_SELECT = reg;
|
||||||
|
*ST_YM_DATA = val;
|
||||||
|
__asm__ volatile ("move.w %0,%%sr" : : "d" (saved) : "memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Write YM mixer reg 7 from the gYmToneMask shadow, preserving the
|
// Write YM mixer reg 7 from the gYmToneMask shadow, preserving the
|
||||||
// TOS I/O-port-direction high bits. Supervisor context required.
|
// TOS I/O-port-direction high bits. Supervisor context required.
|
||||||
static void ymWriteMixer(void) {
|
static void ymWriteMixer(void) {
|
||||||
*ST_YM_SELECT = YM_REG_MIXER;
|
ymWrite(YM_REG_MIXER, (uint8_t)(YM_MIXER_IO_DIR | gYmToneMask));
|
||||||
*ST_YM_DATA = (uint8_t)(YM_MIXER_IO_DIR | gYmToneMask);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -148,6 +190,12 @@ static long installTimerA(void) {
|
||||||
// by TOS for floppy / keyboard / printer).
|
// by TOS for floppy / keyboard / printer).
|
||||||
// reg 8 (channel A volume): start at 0 to avoid a pop at start.
|
// reg 8 (channel A volume): start at 0 to avoid a pop at start.
|
||||||
//
|
//
|
||||||
|
// Channel A ONLY. This used to gate every tone and noise off and zero all
|
||||||
|
// three volumes, so starting a digital sound silenced a playing chiptune
|
||||||
|
// outright -- including channels B and C, which the DAC never touches.
|
||||||
|
// Now B and C keep whatever the music left there and play straight
|
||||||
|
// through, and only voice 0 stands down for the duration.
|
||||||
|
//
|
||||||
// Without the mixer setup, whatever state TOS left noise A in
|
// Without the mixer setup, whatever state TOS left noise A in
|
||||||
// gets gated by our 12 kHz volume writes -- if noise A was on,
|
// gets gated by our 12 kHz volume writes -- if noise A was on,
|
||||||
// a constant volume = constant hiss. Standard PWM-DAC trick is
|
// a constant volume = constant hiss. Standard PWM-DAC trick is
|
||||||
|
|
@ -159,14 +207,9 @@ static long installTimerA(void) {
|
||||||
// disable bits over an assumed-safe TOS-default mask. Bit 6 set
|
// disable bits over an assumed-safe TOS-default mask. Bit 6 set
|
||||||
// (port A output) matches stock TOS; bit 7 set (port B output)
|
// (port A output) matches stock TOS; bit 7 set (port B output)
|
||||||
// matches the centronics-printer direction TOS configures.
|
// matches the centronics-printer direction TOS configures.
|
||||||
gYmToneMask = 0x3Fu; // all tones + noise disabled (PWM-DAC default)
|
gYmToneMask = (uint8_t)(gYmToneMask | YM_MIX_TONE_A | YM_MIX_NOISE_A);
|
||||||
ymWriteMixer(); // -> 0xFF: tones+noise off, I/O ports A+B output
|
ymWriteMixer(); // channel A gated off: its volume reg is now the DAC
|
||||||
*ST_YM_SELECT = 8;
|
ymWrite(YM_REG_VOL_A, 0); // channel A volume = 0 to avoid a pop at start
|
||||||
*ST_YM_DATA = 0; // channel A volume = 0 to avoid a pop at start
|
|
||||||
*ST_YM_SELECT = 9;
|
|
||||||
*ST_YM_DATA = 0; // channel B volume = 0
|
|
||||||
*ST_YM_SELECT = 10;
|
|
||||||
*ST_YM_DATA = 0; // channel C volume = 0
|
|
||||||
|
|
||||||
// MFP Timer A: stop, install our vector, set prescaler 200 + data
|
// MFP Timer A: stop, install our vector, set prescaler 200 + data
|
||||||
// 1 (= 2.4576 MHz / 200 = 12288 Hz), then start.
|
// 1 (= 2.4576 MHz / 200 = 12288 Hz), then start.
|
||||||
|
|
@ -186,11 +229,16 @@ static long uninstallTimerA(void) {
|
||||||
(void)Setexc(VEC_MFP_TA, (long)gOldTimerAVec);
|
(void)Setexc(VEC_MFP_TA, (long)gOldTimerAVec);
|
||||||
gOldTimerAVec = NULL;
|
gOldTimerAVec = NULL;
|
||||||
}
|
}
|
||||||
/* Silence channel A volume so handoff back to TOS is clean (no
|
// Channel A belongs to the PSG again. Replay voice 0's latched state,
|
||||||
* residual DC level on the speaker). */
|
// which both clears the DAC's residual level (so the handoff back to TOS
|
||||||
*ST_YM_SELECT = 8;
|
// leaves no DC on the speaker) and restores a note the music player set
|
||||||
*ST_YM_DATA = 0;
|
// while the pump owned the channel. Without the replay voice 0 would stay
|
||||||
return 0;
|
// dead until its next note EVENT, because src/core/music.c only reprograms
|
||||||
|
// a voice when one arrives -- a held note would never come back.
|
||||||
|
gYmVoice = 0u;
|
||||||
|
gYmFreqHz = gVoiceAFreqHz;
|
||||||
|
gYmAtten = gVoiceAAtten;
|
||||||
|
return applyYmVoice();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -228,8 +276,13 @@ static void pumpStop(void) {
|
||||||
if (!gPumpRunning) {
|
if (!gPumpRunning) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Supexec(uninstallTimerA);
|
// Clear the flag BEFORE the Supexec, not after: uninstallTimerA replays
|
||||||
|
// voice 0 through applyYmVoice, and applyYmVoice stands that voice down
|
||||||
|
// for as long as the flag says the DAC owns channel A. Stopping the timer
|
||||||
|
// is the first thing uninstallTimerA does, so the ISR cannot fire in
|
||||||
|
// between and the early clear costs nothing.
|
||||||
gPumpRunning = false;
|
gPumpRunning = false;
|
||||||
|
Supexec(uninstallTimerA);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -249,15 +302,19 @@ bool jlpAudioInit(void) {
|
||||||
|
|
||||||
|
|
||||||
void jlpAudioShutdown(void) {
|
void jlpAudioShutdown(void) {
|
||||||
// PSG teardown first, independent of gReady: the tone/noise voice
|
// Pump first, then the PSG: stopping the pump hands channel A back and
|
||||||
// layer is usable without jlpAudioInit, and the YM latches -- a
|
// replays voice 0, so the all-off has to come after it or a replayed note
|
||||||
// live voice keeps sounding after exit unless gated off here.
|
// would outlive the program. (The all-off still runs independent of
|
||||||
|
// gReady: the tone/noise voice layer is usable without jlpAudioInit, and
|
||||||
|
// the YM latches -- a live voice keeps sounding after exit.)
|
||||||
|
if (gReady) {
|
||||||
|
pumpStop();
|
||||||
|
}
|
||||||
Supexec(ymPsgAllOff);
|
Supexec(ymPsgAllOff);
|
||||||
|
|
||||||
if (!gReady) {
|
if (!gReady) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pumpStop();
|
|
||||||
silenceMixBuffer();
|
silenceMixBuffer();
|
||||||
|
|
||||||
if (gXmpCtx != NULL) {
|
if (gXmpCtx != NULL) {
|
||||||
|
|
@ -351,12 +408,6 @@ void jlpAudioTone(uint16_t freqHz) {
|
||||||
// YM regs are supervisor-only on the ST, so this is a Supexec block.
|
// YM regs are supervisor-only on the ST, so this is a Supexec block.
|
||||||
// We stash the request in static globals (Supexec'd functions have
|
// We stash the request in static globals (Supexec'd functions have
|
||||||
// no parameters of their own).
|
// no parameters of their own).
|
||||||
static volatile uint8_t gYmVoice;
|
|
||||||
static volatile uint16_t gYmFreqHz;
|
|
||||||
static volatile uint8_t gYmAtten;
|
|
||||||
static volatile uint8_t gYmNoisePitch;
|
|
||||||
static volatile uint8_t gYmNoiseAtten;
|
|
||||||
// gYmToneMask is the shared YM mixer shadow, defined in module state.
|
|
||||||
|
|
||||||
|
|
||||||
static long applyYmVoice(void) {
|
static long applyYmVoice(void) {
|
||||||
|
|
@ -368,6 +419,15 @@ static long applyYmVoice(void) {
|
||||||
toneReg = (uint8_t)(gYmVoice * 2u); // reg 0/2/4
|
toneReg = (uint8_t)(gYmVoice * 2u); // reg 0/2/4
|
||||||
volReg = (uint8_t)(8u + gYmVoice); // reg 8/9/A
|
volReg = (uint8_t)(8u + gYmVoice); // reg 8/9/A
|
||||||
|
|
||||||
|
// Voice 0 and the PWM DAC are the same register (reg 8) and the same mixer
|
||||||
|
// gate (bit 0), and they cannot be shared: enabling tone A chops the DAC's
|
||||||
|
// output with a square wave, and the DAC's 12 kHz writes trample the
|
||||||
|
// voice's level. While the pump runs the DAC wins and the voice waits --
|
||||||
|
// jlpAudioVoice has latched its state, and uninstallTimerA replays it.
|
||||||
|
if (gPumpRunning && gYmVoice == 0u) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (gYmFreqHz == 0u || gYmAtten >= 15u) {
|
if (gYmFreqHz == 0u || gYmAtten >= 15u) {
|
||||||
// Silence: disable tone on this channel (mixer bit 0/1/2 set),
|
// Silence: disable tone on this channel (mixer bit 0/1/2 set),
|
||||||
// zero volume. Don't touch other channels' bits. Channel C's
|
// zero volume. Don't touch other channels' bits. Channel C's
|
||||||
|
|
@ -377,8 +437,7 @@ static long applyYmVoice(void) {
|
||||||
gYmToneMask = (uint8_t)(gYmToneMask | ((uint8_t)1u << gYmVoice));
|
gYmToneMask = (uint8_t)(gYmToneMask | ((uint8_t)1u << gYmVoice));
|
||||||
ymWriteMixer();
|
ymWriteMixer();
|
||||||
if (gYmVoice != 2u || (gYmToneMask & YM_MIX_NOISE_C) != 0u) {
|
if (gYmVoice != 2u || (gYmToneMask & YM_MIX_NOISE_C) != 0u) {
|
||||||
*ST_YM_SELECT = volReg;
|
ymWrite(volReg, 0);
|
||||||
*ST_YM_DATA = 0;
|
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -391,14 +450,11 @@ static long applyYmVoice(void) {
|
||||||
period = 0x0FFFu;
|
period = 0x0FFFu;
|
||||||
}
|
}
|
||||||
|
|
||||||
*ST_YM_SELECT = toneReg;
|
ymWrite(toneReg, (uint8_t)(period & 0xFFu));
|
||||||
*ST_YM_DATA = (uint8_t)(period & 0xFFu);
|
ymWrite((uint8_t)(toneReg + 1u), (uint8_t)((period >> 8) & 0x0Fu));
|
||||||
*ST_YM_SELECT = (uint8_t)(toneReg + 1u);
|
|
||||||
*ST_YM_DATA = (uint8_t)((period >> 8) & 0x0Fu);
|
|
||||||
|
|
||||||
vol = (uint8_t)(15u - gYmAtten);
|
vol = (uint8_t)(15u - gYmAtten);
|
||||||
*ST_YM_SELECT = volReg;
|
ymWrite(volReg, (uint8_t)(vol & 0x0Fu));
|
||||||
*ST_YM_DATA = (uint8_t)(vol & 0x0Fu);
|
|
||||||
|
|
||||||
// Enable tone on this channel (clear its bit in mixer).
|
// Enable tone on this channel (clear its bit in mixer).
|
||||||
gYmToneMask = (uint8_t)(gYmToneMask & ~((uint8_t)1u << gYmVoice));
|
gYmToneMask = (uint8_t)(gYmToneMask & ~((uint8_t)1u << gYmVoice));
|
||||||
|
|
@ -418,17 +474,14 @@ static long applyYmNoise(void) {
|
||||||
gYmToneMask = (uint8_t)(gYmToneMask | YM_MIX_NOISE_C);
|
gYmToneMask = (uint8_t)(gYmToneMask | YM_MIX_NOISE_C);
|
||||||
ymWriteMixer();
|
ymWriteMixer();
|
||||||
if ((gYmToneMask & YM_MIX_TONE_C) != 0u) {
|
if ((gYmToneMask & YM_MIX_TONE_C) != 0u) {
|
||||||
*ST_YM_SELECT = YM_REG_VOL_C;
|
ymWrite(YM_REG_VOL_C, 0);
|
||||||
*ST_YM_DATA = 0;
|
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
// Program period + volume before opening the mixer gate so the
|
// Program period + volume before opening the mixer gate so the
|
||||||
// gate never opens onto stale settings.
|
// gate never opens onto stale settings.
|
||||||
*ST_YM_SELECT = YM_REG_NOISE_PERIOD;
|
ymWrite(YM_REG_NOISE_PERIOD, (uint8_t)(gYmNoisePitch & 0x1Fu));
|
||||||
*ST_YM_DATA = (uint8_t)(gYmNoisePitch & 0x1Fu);
|
ymWrite(YM_REG_VOL_C, (uint8_t)((uint8_t)(15u - gYmNoiseAtten) & 0x0Fu));
|
||||||
*ST_YM_SELECT = YM_REG_VOL_C;
|
|
||||||
*ST_YM_DATA = (uint8_t)((uint8_t)(15u - gYmNoiseAtten) & 0x0Fu);
|
|
||||||
gYmToneMask = (uint8_t)(gYmToneMask & (uint8_t)~YM_MIX_NOISE_C);
|
gYmToneMask = (uint8_t)(gYmToneMask & (uint8_t)~YM_MIX_NOISE_C);
|
||||||
ymWriteMixer();
|
ymWriteMixer();
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -440,14 +493,13 @@ static long applyYmNoise(void) {
|
||||||
// (the chip free-runs with zero CPU) cannot keep sounding after the
|
// (the chip free-runs with zero CPU) cannot keep sounding after the
|
||||||
// program exits.
|
// program exits.
|
||||||
static long ymPsgAllOff(void) {
|
static long ymPsgAllOff(void) {
|
||||||
gYmToneMask = 0x3Fu;
|
gYmToneMask = 0x3Fu;
|
||||||
ymWriteMixer();
|
ymWriteMixer();
|
||||||
*ST_YM_SELECT = 8;
|
ymWrite(YM_REG_VOL_A, 0);
|
||||||
*ST_YM_DATA = 0;
|
ymWrite(YM_REG_VOL_B, 0);
|
||||||
*ST_YM_SELECT = 9;
|
ymWrite(YM_REG_VOL_C, 0);
|
||||||
*ST_YM_DATA = 0;
|
// Drop the latched voice-0 note too, or a later pump stop would replay it.
|
||||||
*ST_YM_SELECT = YM_REG_VOL_C;
|
gVoiceAAtten = 15u;
|
||||||
*ST_YM_DATA = 0;
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -489,12 +541,15 @@ void jlpAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
|
||||||
if (voice >= JOEY_AUDIO_VOICES) {
|
if (voice >= JOEY_AUDIO_VOICES) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The PWM-via-volume engine in jlpAudioInit hammers YM reg 8
|
// Digital output and chip music now coexist. The PWM engine holds channel
|
||||||
// (channel A volume) at 12 kHz for MOD output. If that IRQ is
|
// A (reg 8) as its DAC for as long as it runs, so voice 0 is latched here
|
||||||
// running, it will fight the per-voice writes here. AGI does not
|
// and applied when the pump stops; voices 1 and 2 play throughout. Writes
|
||||||
// call jlAudioInit, so Timer A is not installed and the YM is
|
// are atomic against the ISR (see ymWrite), which is what makes the two
|
||||||
// ours -- but a future caller mixing modes would hear collisions
|
// safe to run together at all.
|
||||||
// on channel A.
|
if (voice == 0u) {
|
||||||
|
gVoiceAFreqHz = freqHz;
|
||||||
|
gVoiceAAtten = atten;
|
||||||
|
}
|
||||||
gYmVoice = voice;
|
gYmVoice = voice;
|
||||||
gYmFreqHz = freqHz;
|
gYmFreqHz = freqHz;
|
||||||
gYmAtten = atten;
|
gYmAtten = atten;
|
||||||
|
|
|
||||||
|
|
@ -449,19 +449,11 @@ void stageDirtyClearAll(void) {
|
||||||
memset(gStageMinWord, STAGE_DIRTY_CLEAN_MIN, SURFACE_HEIGHT);
|
memset(gStageMinWord, STAGE_DIRTY_CLEAN_MIN, SURFACE_HEIGHT);
|
||||||
memset(gStageMaxWord, STAGE_DIRTY_CLEAN_MAX, SURFACE_HEIGHT);
|
memset(gStageMaxWord, STAGE_DIRTY_CLEAN_MAX, SURFACE_HEIGHT);
|
||||||
#endif
|
#endif
|
||||||
#if defined(JOEYLIB_PLATFORM_IIGS)
|
// The IIgs group bytes are NOT cleared here any more:
|
||||||
// NOT memset: this runs after every present, and llvm-mos lowers a
|
// iigsStageBandsClearAll clears each one as it clears that group's rows,
|
||||||
// memset to a far-called byte loop (~30 cyc/byte) -- the same trap
|
// because it has to read them anyway to decide whether those rows need
|
||||||
// the band fill above avoids. Open-coding the 25 stores was worth
|
// clearing at all. Folding it in there saves this loop (~800 cycles of
|
||||||
// ~800 cycles a present, which was the whole gain of the group map.
|
// every present) outright.
|
||||||
{
|
|
||||||
uint8_t grp;
|
|
||||||
|
|
||||||
for (grp = 0u; grp < STAGE_DIRTY_GROUPS; grp++) {
|
|
||||||
gStageDirtyGroup[grp] = 0u;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3231,14 +3231,15 @@ isvsRet:
|
||||||
; ([dp],y) for an operand that fits the direct page, so we cannot use a
|
; ([dp],y) for an operand that fits the direct page, so we cannot use a
|
||||||
; 16-bit bss symbol as the pointer slot. Instead, faithful to the spec's
|
; 16-bit bss symbol as the pointer slot. Instead, faithful to the spec's
|
||||||
; "pointer arg0 that IS used" recipe, we repoint D at a bank-0 scratch
|
; "pointer arg0 that IS used" recipe, we repoint D at a bank-0 scratch
|
||||||
; struct (dpxlScratch): store pixels (A:X) at dpxlScratch+0..2 and use
|
; struct. That struct is the HARDWARE STACK, not a bss slot -- see the
|
||||||
; [pix],y with pix=0. Because D no longer overlays the stack frame, the
|
; prologue note. We push pixels (A:X) and point D at the pushed copy, then
|
||||||
|
; use [pix],y with pix=0. Because D no longer overlays the stack frame, the
|
||||||
; x,y,nibble args are read stack-relative.
|
; x,y,nibble args are read stack-relative.
|
||||||
; No PHB/PLB: DBR is never written here (PERF-AUDIT #53), so the frame
|
; No PHB/PLB: DBR is never written here (PERF-AUDIT #53), so the frame is
|
||||||
; is php(+1)+phd(+2)=+3. On JSL entry x@4,s y@6,s nib@8,s; after the 3
|
; php(+1)+phd(+2)+phx(+2)+pha(+2)=+7. On JSL entry x@4,s y@6,s nib@8,s;
|
||||||
; push bytes: x@7,s y@9,s nib@11,s.
|
; after the 7 push bytes: x@11,s y@13,s nib@15,s.
|
||||||
; pix=0 (D-relative, = dpxlScratch+0)
|
; pix=0 (D-relative, = the pushed far pointer)
|
||||||
; xx=7,s yy=9,s nib=11,s
|
; xx=11,s yy=13,s nib=15,s
|
||||||
; pix is a 3-byte far pointer (A low16 + X bank byte); we store A then X
|
; pix is a 3-byte far pointer (A low16 + X bank byte); we store A then X
|
||||||
; as words -> a harmless 4th filler byte in the 4-byte scratch.
|
; as words -> a harmless 4th filler byte in the 4-byte scratch.
|
||||||
; ====================================================================
|
; ====================================================================
|
||||||
|
|
@ -3246,7 +3247,7 @@ isvsRet:
|
||||||
.section .text.iigsDrawPixelInner,"ax"
|
.section .text.iigsDrawPixelInner,"ax"
|
||||||
.globl iigsDrawPixelInner
|
.globl iigsDrawPixelInner
|
||||||
iigsDrawPixelInner:
|
iigsDrawPixelInner:
|
||||||
pix = 0 ; pixels far ptr (D+0..2 = dpxlScratch)
|
pix = 0 ; pixels far ptr (D+0..2 = stacked copy)
|
||||||
|
|
||||||
php
|
php
|
||||||
phd
|
phd
|
||||||
|
|
@ -3254,27 +3255,27 @@ pix = 0 ; pixels far ptr (D+0..2 = dpxlScra
|
||||||
.a16
|
.a16
|
||||||
.i16
|
.i16
|
||||||
|
|
||||||
; Stash the pixels far pointer into the DP scratch via D-RELATIVE stores
|
; STACK D-frame, not `lda #dpxlScratch; tcd`. The direct page is ALWAYS bank
|
||||||
; (AFTER tcd): a plain `sta dpxlScratch` writes the LOAD bank (DBR) while
|
; 0, but dpxlScratch is a BSS symbol whose address is an offset in the LOAD
|
||||||
; [pix],y reads through D, which is ALWAYS bank 0 -- the two disagreed and the
|
; bank, so D landed on bank 0 at that offset -- memory this program does not
|
||||||
; plot pointer was garbage. phx/pha preserve pix (A:X survive php/phd)
|
; own. Stores and the [pix],y read agreed with each other, so it looked right
|
||||||
; across the `lda #dpxlScratch` that clobbers A; pulled back so the N,s arg
|
; until the link layout moved that offset somewhere bank 0 would not hold it.
|
||||||
; offsets below keep their values.
|
; The hardware stack IS bank 0. Same fix as the tile routines and
|
||||||
phx ; save pix bank
|
; iigsDrawLineInner. The 4 pushed bytes stay for the life of the call, so the
|
||||||
pha ; save pix offset
|
; N,s offsets below are 4 higher than the JSL-entry figures and the exit drops
|
||||||
lda #dpxlScratch
|
; them before PLD.
|
||||||
tcd ; D = dpxlScratch (bank 0); [pix],y == [0],y
|
phx ; pix high16 (bank) -> 3,s
|
||||||
pla ; pix offset
|
pha ; pix low16 (offset) -> 1,s
|
||||||
sta pix+0 ; D-relative -> bank0:dpxlScratch+0
|
tsc
|
||||||
pla ; pix bank
|
inc a ; A = SP+1
|
||||||
sta pix+2 ; D-relative -> bank0:dpxlScratch+2
|
tcd ; D -> stacked pix; [pix],y == [0],y
|
||||||
|
|
||||||
; Compute byte offset = y*160 + (x>>1) into A. Fold the LUT row directly
|
; Compute byte offset = y*160 + (x>>1) into A. Fold the LUT row directly
|
||||||
; into the add (adc gRowOffsetLut,x) -- no scratch round-trip (#53).
|
; into the add (adc gRowOffsetLut,x) -- no scratch round-trip (#53).
|
||||||
lda 9,s ; y
|
lda 13,s ; y
|
||||||
asl a ; A = y*2 (LUT byte offset)
|
asl a ; A = y*2 (LUT byte offset)
|
||||||
tax
|
tax
|
||||||
lda 7,s ; x
|
lda 11,s ; x
|
||||||
lsr a ; A = x >> 1
|
lsr a ; A = x >> 1
|
||||||
clc
|
clc
|
||||||
adc gRowOffsetLut,x ; A = x>>1 + y*160
|
adc gRowOffsetLut,x ; A = x>>1 + y*160
|
||||||
|
|
@ -3283,12 +3284,12 @@ pix = 0 ; pixels far ptr (D+0..2 = dpxlScra
|
||||||
sep #0x20 ; M=8
|
sep #0x20 ; M=8
|
||||||
.a8
|
.a8
|
||||||
|
|
||||||
lda 7,s ; A = x low byte (parity in LSB)
|
lda 11,s ; A = x low byte (parity in LSB)
|
||||||
and #1
|
and #1
|
||||||
bne dpxlOdd
|
bne dpxlOdd
|
||||||
|
|
||||||
; Even x -> high nibble.
|
; Even x -> high nibble.
|
||||||
lda 11,s ; nibble
|
lda 15,s ; nibble
|
||||||
asl a
|
asl a
|
||||||
asl a
|
asl a
|
||||||
asl a
|
asl a
|
||||||
|
|
@ -3302,7 +3303,7 @@ pix = 0 ; pixels far ptr (D+0..2 = dpxlScra
|
||||||
|
|
||||||
dpxlOdd:
|
dpxlOdd:
|
||||||
; Odd x -> low nibble.
|
; Odd x -> low nibble.
|
||||||
lda 11,s ; nibble
|
lda 15,s ; nibble
|
||||||
and #0x0F
|
and #0x0F
|
||||||
sta dpxlNibPart
|
sta dpxlNibPart
|
||||||
lda [pix],y
|
lda [pix],y
|
||||||
|
|
@ -3315,6 +3316,8 @@ dpxlDone:
|
||||||
.a16
|
.a16
|
||||||
.a8
|
.a8
|
||||||
.i8
|
.i8
|
||||||
|
pla ; drop the stacked pix far pointer
|
||||||
|
pla ; (4 bytes -- see the prologue note)
|
||||||
pld
|
pld
|
||||||
plp
|
plp
|
||||||
rtl
|
rtl
|
||||||
|
|
@ -3411,10 +3414,6 @@ dpxmDone:
|
||||||
.globl dpxlNibPart
|
.globl dpxlNibPart
|
||||||
dpxlNibPart:
|
dpxlNibPart:
|
||||||
.zero 1
|
.zero 1
|
||||||
.section .bss.dpxlScratch,"aw"
|
|
||||||
.globl dpxlScratch
|
|
||||||
dpxlScratch:
|
|
||||||
.zero 4
|
|
||||||
|
|
||||||
|
|
||||||
; ====================================================================
|
; ====================================================================
|
||||||
|
|
@ -3432,14 +3431,14 @@ dpxlScratch:
|
||||||
; ABI: clang cdecl. pixels arrives in A:X (dereferenced via [pix],y);
|
; ABI: clang cdecl. pixels arrives in A:X (dereferenced via [pix],y);
|
||||||
; x0,y0,x1,y1,nibble on the stack. Same hybrid as DrawPixel: llvm-mc
|
; x0,y0,x1,y1,nibble on the stack. Same hybrid as DrawPixel: llvm-mc
|
||||||
; cannot encode [dp],y from a 16-bit bss symbol, so we repoint D at a
|
; cannot encode [dp],y from a 16-bit bss symbol, so we repoint D at a
|
||||||
; bank-0 scratch struct (dlnScratch), stash pixels (A:X) at
|
; bank-0 struct -- and that struct is the HARDWARE STACK, not a bss slot
|
||||||
; dlnScratch+0..2, and use [pix],y with pix=0 in the loop. The five
|
; (see the prologue note). We push pixels (A:X) and use [pix],y with pix=0
|
||||||
; word args are copied into DRAWPRIMS scratch up front (stack-relative),
|
; in the loop. The five word args are copied into DRAWPRIMS scratch up front
|
||||||
; so the loop never touches the stack frame again.
|
; (stack-relative), so the loop never touches the stack frame again.
|
||||||
; Prologue pushes: php(+1)+phb(+1)+phd(+2)=+4. On JSL entry x0@4,s
|
; Prologue pushes: php(+1)+phb(+1)+phd(+2)+phx(+2)+pha(+2)=+8. On JSL entry
|
||||||
; y0@6,s x1@8,s y1@10,s nib@12,s; after the 4 pushes:
|
; x0@4,s y0@6,s x1@8,s y1@10,s nib@12,s; after the 8 pushes:
|
||||||
; x0@8,s y0@10,s x1@12,s y1@14,s nib@16,s
|
; x0@12,s y0@14,s x1@16,s y1@18,s nib@20,s
|
||||||
; pix=0 (D-relative, = dlnScratch+0)
|
; pix=0 (D-relative, = the pushed far pointer)
|
||||||
; ====================================================================
|
; ====================================================================
|
||||||
|
|
||||||
.section .text.iigsDrawLineInner,"ax"
|
.section .text.iigsDrawLineInner,"ax"
|
||||||
|
|
@ -3454,26 +3453,33 @@ pix = 0 ; pixels far ptr (D+0..2 = bank0 sc
|
||||||
.a16
|
.a16
|
||||||
.i16
|
.i16
|
||||||
|
|
||||||
; Stash the pixels far pointer via D-RELATIVE stores (AFTER tcd): a plain
|
; Build the pix D-frame on the STACK (bank 0) -- the same fix the tile
|
||||||
; `sta dlnScratch` writes the LOAD bank (DBR) while [pix],y reads through
|
; routines already carry. `lda #dlnScratch; tcd` was NOT safe: the direct
|
||||||
; D, which is ALWAYS bank 0 -- the two disagreed, so every plot went
|
; page is ALWAYS bank 0, but dlnScratch is a BSS symbol whose address is an
|
||||||
; through a garbage bank-0 pointer (PERF-AUDIT.md #83: wrong pixels
|
; offset in the LOAD bank, so D landed on bank 0 at that offset -- somebody
|
||||||
; always; #86: a layout shift let the plots stomp the dln* walk state,
|
; else's memory. The stores and the [pix],y read agreed with each other, so
|
||||||
; hanging the Bresenham). Identical fix to iigsDrawPixelInner /
|
; the routine looked correct, but those four bytes lived in whatever bank 0
|
||||||
; iigsDrawCircleInner; phx/pha keep the N,s arg offsets unchanged.
|
; keeps at that address; once the link layout put it in the $D000+ language
|
||||||
phx ; save pix bank
|
; card region the pointer stopped surviving the round trip and every plot
|
||||||
pha ; save pix offset
|
; wrote into nowhere. That is what killed the corner-to-corner diagonal
|
||||||
lda #dlnScratch
|
; (0,0)-(319,199): a perfect Bresenham walk, 320 plots issued, zero pixels
|
||||||
tcd ; D = dlnScratch (bank 0); [pix],y == [0],y
|
; changed. The hardware stack IS bank 0, so a stack D-frame always matches.
|
||||||
pla
|
;
|
||||||
sta pix+0 ; D-relative -> bank0 scratch+0
|
; A = pix low16, X = pix high16. After the two pushes D+0 is the offset and
|
||||||
pla
|
; D+2 the bank, so `pix = 0` still names the whole far pointer, and the two
|
||||||
sta pix+2 ; D-relative -> bank0 scratch+2
|
; stores the old code needed are gone. The 4 bytes STAY on the stack for the
|
||||||
|
; life of the call, so every N,s argument offset below is 4 higher than the
|
||||||
|
; header's JSL-entry figures, and dlnExit drops them before PLD.
|
||||||
|
phx ; pix high16 (bank) -> 3,s
|
||||||
|
pha ; pix low16 (offset) -> 1,s
|
||||||
|
tsc
|
||||||
|
inc a ; A = SP+1
|
||||||
|
tcd ; D -> stacked pix; [pix],y == [0],y
|
||||||
|
|
||||||
; dx = |x1 - x0|, sx = sign(x1 - x0)
|
; dx = |x1 - x0|, sx = sign(x1 - x0)
|
||||||
lda 12,s ; x1
|
lda 16,s ; x1
|
||||||
sec
|
sec
|
||||||
sbc 8,s ; x0
|
sbc 12,s ; x0
|
||||||
bpl dlnDxPos
|
bpl dlnDxPos
|
||||||
eor #0xFFFF
|
eor #0xFFFF
|
||||||
clc
|
clc
|
||||||
|
|
@ -3489,9 +3495,9 @@ dlnDxPos:
|
||||||
dlnDxDone:
|
dlnDxDone:
|
||||||
|
|
||||||
; dy = -|y1 - y0|, sy = sign(y1 - y0). Bresenham uses negative dy.
|
; dy = -|y1 - y0|, sy = sign(y1 - y0). Bresenham uses negative dy.
|
||||||
lda 14,s ; y1
|
lda 18,s ; y1
|
||||||
sec
|
sec
|
||||||
sbc 10,s ; y0
|
sbc 14,s ; y0
|
||||||
bpl dlnDyPos
|
bpl dlnDyPos
|
||||||
sta dlnDy ; A is negative; that's dy with the sign already
|
sta dlnDy ; A is negative; that's dy with the sign already
|
||||||
lda #0xFFFF
|
lda #0xFFFF
|
||||||
|
|
@ -3514,15 +3520,15 @@ dlnDyDone:
|
||||||
sta dlnErr
|
sta dlnErr
|
||||||
|
|
||||||
; Copy x0, y0 to running state slots.
|
; Copy x0, y0 to running state slots.
|
||||||
lda 8,s ; x0
|
lda 12,s ; x0
|
||||||
sta dlnX
|
sta dlnX
|
||||||
lda 10,s ; y0
|
lda 14,s ; y0
|
||||||
sta dlnY
|
sta dlnY
|
||||||
|
|
||||||
; Cache x1, y1 so the loop's compare is fast.
|
; Cache x1, y1 so the loop's compare is fast.
|
||||||
lda 12,s ; x1
|
lda 16,s ; x1
|
||||||
sta dlnX1
|
sta dlnX1
|
||||||
lda 14,s ; y1
|
lda 18,s ; y1
|
||||||
sta dlnY1
|
sta dlnY1
|
||||||
|
|
||||||
; Precompute both 16-bit nibble OR-values ONCE (stays in M=16 the
|
; Precompute both 16-bit nibble OR-values ONCE (stays in M=16 the
|
||||||
|
|
@ -3532,7 +3538,7 @@ dlnDyDone:
|
||||||
; preserved by the mask + zero OR-high-byte, so the result is identical
|
; preserved by the mask + zero OR-high-byte, so the result is identical
|
||||||
; to the old 8-bit single-byte RMW. dlnNibHi/dlnNibLo have low byte =
|
; to the old 8-bit single-byte RMW. dlnNibHi/dlnNibLo have low byte =
|
||||||
; (nib<<4) / nib and high byte = 0.
|
; (nib<<4) / nib and high byte = 0.
|
||||||
lda 16,s ; nibble
|
lda 20,s ; nibble
|
||||||
and #0x000F
|
and #0x000F
|
||||||
sta dlnNibLo ; low byte = nib, high byte = 0
|
sta dlnNibLo ; low byte = nib, high byte = 0
|
||||||
asl a
|
asl a
|
||||||
|
|
@ -3631,6 +3637,8 @@ dlnSkipY:
|
||||||
brl dlnLoop
|
brl dlnLoop
|
||||||
|
|
||||||
dlnExit:
|
dlnExit:
|
||||||
|
pla ; drop the stacked pix far pointer
|
||||||
|
pla ; (4 bytes -- see the prologue note)
|
||||||
.a8
|
.a8
|
||||||
.i8
|
.i8
|
||||||
pld
|
pld
|
||||||
|
|
@ -3691,10 +3699,6 @@ dlnNibHi:
|
||||||
.globl dlnTmp
|
.globl dlnTmp
|
||||||
dlnTmp:
|
dlnTmp:
|
||||||
.zero 2
|
.zero 2
|
||||||
.section .bss.dlnScratch,"aw"
|
|
||||||
.globl dlnScratch
|
|
||||||
dlnScratch:
|
|
||||||
.zero 4
|
|
||||||
|
|
||||||
; ===== ported: circles =====
|
; ===== ported: circles =====
|
||||||
; ================================================================
|
; ================================================================
|
||||||
|
|
@ -3709,25 +3713,25 @@ dlnScratch:
|
||||||
;
|
;
|
||||||
; ABI (llvm-mos w65816 cdecl):
|
; ABI (llvm-mos w65816 cdecl):
|
||||||
; pix -> arg0 in A:X (A=offset low16, X low byte=bank). USED via
|
; pix -> arg0 in A:X (A=offset low16, X low byte=bank). USED via
|
||||||
; [pix],y, so it must live in the direct page. We stash it
|
; [pix],y, so it must live in the direct page. We PUSH it
|
||||||
; into bank-0 scratch dcPixPtr (offset@+0, bank@+2) BEFORE
|
; (offset@1,s bank@3,s) and point D at the pushed copy, so the
|
||||||
; setting D, then point D at dcPixPtr so the original
|
; original `[pix],y` (pix equ 0 -> [D+0],y) loads are
|
||||||
; `[pix],y` (pix equ 0 -> [D+0],y) loads are byte-identical.
|
; byte-identical. The stack is bank 0; a bss address is not.
|
||||||
; php/phb/phd do not touch A/X, so A:X still hold pix when
|
; php/phb/phd do not touch A/X, so A:X still hold pix when
|
||||||
; we capture them after the pushes.
|
; we capture them after the pushes.
|
||||||
; cx,cy,r,nibble -> on the stack. On JSL entry cx@4,s cy@6,s r@8,s
|
; cx,cy,r,nibble -> on the stack. On JSL entry cx@4,s cy@6,s r@8,s
|
||||||
; nib@10,s. After php(+1)+phb(+1)+phd(+2)=+4: cx@8,s
|
; nib@10,s. After php+phb+phd+phx+pha (+8): cx@12,s
|
||||||
; cy@10,s r@12,s nib@14,s. S never moves in the body
|
; cy@14,s r@16,s nib@18,s. S never moves in the body
|
||||||
; (no pushes in the loop), so these N,s reads stay valid.
|
; (no pushes in the loop), so these N,s reads stay valid.
|
||||||
; They are stack reads because D hosts the pix pointer.
|
; They are stack reads because D hosts the pix pointer.
|
||||||
; ================================================================
|
; ================================================================
|
||||||
.section .text.iigsDrawCircleInner,"ax"
|
.section .text.iigsDrawCircleInner,"ax"
|
||||||
.globl iigsDrawCircleInner
|
.globl iigsDrawCircleInner
|
||||||
pix = 0 ; D-relative: D points at dcPixPtr
|
pix = 0 ; D-relative: D points at the stacked far ptr
|
||||||
dcArgCx = 8 ; cx @ 8,s after +4 prologue
|
dcArgCx = 12 ; cx @ 12,s after php+phb+phd+phx+pha
|
||||||
dcArgCy = 10 ; cy @ 10,s
|
dcArgCy = 14 ; cy @ 14,s
|
||||||
dcArgR = 12 ; r @ 12,s
|
dcArgR = 16 ; r @ 16,s
|
||||||
dcArgNib = 14 ; nibble @ 14,s
|
dcArgNib = 18 ; nibble @ 18,s
|
||||||
iigsDrawCircleInner:
|
iigsDrawCircleInner:
|
||||||
php
|
php
|
||||||
phb
|
phb
|
||||||
|
|
@ -3736,22 +3740,20 @@ iigsDrawCircleInner:
|
||||||
.a16
|
.a16
|
||||||
.i16
|
.i16
|
||||||
|
|
||||||
; Stash the pix far pointer (still in A:X) into bank-0 scratch and
|
; STACK D-frame. The direct page is ALWAYS bank 0, but dcPixPtr is a BSS
|
||||||
; point D at it so [pix],y works unchanged.
|
; symbol whose address is an offset in the LOAD bank, so `lda #dcPixPtr; tcd`
|
||||||
; Store pix into the DP scratch via D-RELATIVE stores (AFTER tcd): a
|
; aimed D at bank 0 at that offset -- memory this program does not own. The
|
||||||
; plain `sta dcPixPtr` writes the LOAD bank (DBR) while [pix],y reads
|
; D-relative stores and the [pix],y reads agreed with each other, so it
|
||||||
; through D, which is ALWAYS bank 0 -- the two disagreed, so the plot
|
; looked correct until the link layout moved that offset out of reach. The
|
||||||
; pointer was garbage and nothing drew. phx/pha preserve pix across the
|
; hardware stack IS bank 0, so park the far pointer there. Same fix as the
|
||||||
; `lda #dcPixPtr` that clobbers A, then are pulled back so the N,s arg
|
; tile routines and iigsDrawLineInner. The 4 pushed bytes live for the whole
|
||||||
; offsets below do not shift.
|
; call, so the arg offsets above are 4 higher than the JSL-entry figures and
|
||||||
phx ; save pix bank
|
; the exit drops them before PLD.
|
||||||
pha ; save pix offset
|
phx ; pix high16 (bank) -> 3,s
|
||||||
lda #dcPixPtr
|
pha ; pix low16 (offset) -> 1,s
|
||||||
tcd ; D = &dcPixPtr (bank 0); [pix],y == [0],y
|
tsc
|
||||||
pla ; pix offset
|
inc a ; A = SP+1
|
||||||
sta pix+0 ; D-relative -> bank0:dcPixPtr+0
|
tcd ; D -> stacked pointer; [pix],y == [0],y
|
||||||
pla ; pix bank
|
|
||||||
sta pix+2 ; D-relative -> bank0:dcPixPtr+2
|
|
||||||
|
|
||||||
; Cache the nibble in 8-bit form: dcNibLo = nib, dcNibHi = nib<<4.
|
; Cache the nibble in 8-bit form: dcNibLo = nib, dcNibHi = nib<<4.
|
||||||
lda dcArgNib, s
|
lda dcArgNib, s
|
||||||
|
|
@ -4074,6 +4076,8 @@ dcErrGT:
|
||||||
dcExit:
|
dcExit:
|
||||||
.a8
|
.a8
|
||||||
.i8
|
.i8
|
||||||
|
pla ; drop the stacked far pointer
|
||||||
|
pla ; (4 bytes -- see the prologue note)
|
||||||
pld
|
pld
|
||||||
plb
|
plb
|
||||||
plp
|
plp
|
||||||
|
|
@ -4097,23 +4101,23 @@ dcExit:
|
||||||
; software-multiply helper for a runtime color (PERF-AUDIT #41).
|
; software-multiply helper for a runtime color (PERF-AUDIT #41).
|
||||||
;
|
;
|
||||||
; ABI (llvm-mos w65816 cdecl):
|
; ABI (llvm-mos w65816 cdecl):
|
||||||
; pix -> arg0 in A:X. USED via [fpix],y, so stashed into bank-0
|
; pix -> arg0 in A:X. USED via [fpix],y, so PUSHED (offset@1,s
|
||||||
; scratch fcPixPtr (offset@+0, bank@+2) and D pointed at
|
; bank@3,s) with D pointed at the pushed copy; fpix equ 0 ->
|
||||||
; it; fpix equ 0 -> [D+0],y.
|
; [D+0],y. The stack is bank 0; a bss address is not.
|
||||||
; cx,cy,r,nibble -> on the stack. JSL entry cx@4,s cy@6,s r@8,s
|
; cx,cy,r,nibble -> on the stack. JSL entry cx@4,s cy@6,s r@8,s
|
||||||
; nibble@10,s. After php+phb+phd (+4): cx@8,s cy@10,s
|
; nibble@10,s. After php+phb+phd+phx+pha (+8): cx@12,s
|
||||||
; r@12,s nibble@14,s. S is constant in the outer body;
|
; cy@14,s r@16,s nibble@18,s. S is constant in the outer body;
|
||||||
; the jsr-called helpers (fcMul16/fcDoSpan) only touch
|
; the jsr-called helpers (fcMul16/fcDoSpan) only touch
|
||||||
; scratch globals + [fpix],y, never N,s, so the inner
|
; scratch globals + [fpix],y, never N,s, so the inner
|
||||||
; jsr-pushed return PC never disturbs an arg read.
|
; jsr-pushed return PC never disturbs an arg read.
|
||||||
; ================================================================
|
; ================================================================
|
||||||
.section .text.iigsFillCircleInner,"ax"
|
.section .text.iigsFillCircleInner,"ax"
|
||||||
.globl iigsFillCircleInner
|
.globl iigsFillCircleInner
|
||||||
fpix = 0 ; D-relative: D points at fcPixPtr
|
fpix = 0 ; D-relative: D points at the stacked far ptr
|
||||||
fcArgCx = 8 ; cx @ 8,s after +4 prologue
|
fcArgCx = 12 ; cx @ 12,s after php+phb+phd+phx+pha
|
||||||
fcArgCy = 10 ; cy @ 10,s
|
fcArgCy = 14 ; cy @ 14,s
|
||||||
fcArgR = 12 ; r @ 12,s
|
fcArgR = 16 ; r @ 16,s
|
||||||
fcArgFill = 14 ; nibble @ 14,s
|
fcArgFill = 18 ; nibble @ 18,s
|
||||||
iigsFillCircleInner:
|
iigsFillCircleInner:
|
||||||
php
|
php
|
||||||
phb
|
phb
|
||||||
|
|
@ -4122,18 +4126,20 @@ iigsFillCircleInner:
|
||||||
.a16
|
.a16
|
||||||
.i16
|
.i16
|
||||||
|
|
||||||
; Stash the pix far pointer via D-RELATIVE stores (AFTER tcd) -- a plain
|
; STACK D-frame. The direct page is ALWAYS bank 0, but fcPixPtr is a BSS
|
||||||
; `sta fcPixPtr` writes the LOAD bank (DBR) while [fpix],y reads through
|
; symbol whose address is an offset in the LOAD bank, so `lda #fcPixPtr; tcd`
|
||||||
; D = bank 0 (the #83/#86 garbage-plot-pointer bug; same fix as
|
; aimed D at bank 0 at that offset -- memory this program does not own. The
|
||||||
; iigsDrawPixelInner). phx/pha keep the N,s arg offsets unchanged.
|
; D-relative stores and the [fpix],y reads agreed with each other, so it
|
||||||
phx ; save pix bank
|
; looked correct until the link layout moved that offset out of reach. The
|
||||||
pha ; save pix offset
|
; hardware stack IS bank 0, so park the far pointer there. Same fix as the
|
||||||
lda #fcPixPtr
|
; tile routines and iigsDrawLineInner. The 4 pushed bytes live for the whole
|
||||||
tcd ; D = fcPixPtr (bank 0); [fpix],y == [0],y
|
; call, so the arg offsets above are 4 higher than the JSL-entry figures and
|
||||||
pla
|
; the exit drops them before PLD.
|
||||||
sta 0 ; D-relative -> bank0 scratch+0 (fpix lo16)
|
phx ; fpix high16 (bank) -> 3,s
|
||||||
pla
|
pha ; fpix low16 (offset) -> 1,s
|
||||||
sta 2 ; D-relative -> bank0 scratch+2 (bank byte)
|
tsc
|
||||||
|
inc a ; A = SP+1
|
||||||
|
tcd ; D -> stacked pointer; [fpix],y == [0],y
|
||||||
|
|
||||||
; Derive the fill bytes from the raw nibble arg (#41).
|
; Derive the fill bytes from the raw nibble arg (#41).
|
||||||
; fcFillByte = doubled byte (full byte fills + low nibble), e.g. $33.
|
; fcFillByte = doubled byte (full byte fills + low nibble), e.g. $33.
|
||||||
|
|
@ -4248,6 +4254,8 @@ fcContinue:
|
||||||
fcExit:
|
fcExit:
|
||||||
.a8
|
.a8
|
||||||
.i8
|
.i8
|
||||||
|
pla ; drop the stacked far pointer
|
||||||
|
pla ; (4 bytes -- see the prologue note)
|
||||||
pld
|
pld
|
||||||
plb
|
plb
|
||||||
plp
|
plp
|
||||||
|
|
@ -4290,7 +4298,7 @@ fcMulSkip:
|
||||||
; trailing bytes.
|
; trailing bytes.
|
||||||
;
|
;
|
||||||
; Inputs: fcLeftCol, fcRightCol, fcSpanRowBase (M=16 values).
|
; Inputs: fcLeftCol, fcRightCol, fcSpanRowBase (M=16 values).
|
||||||
; Trashes A, X, Y, P. Preserves D and B (relies on D = &fcPixPtr).
|
; Trashes A, X, Y, P. Preserves D and B (relies on D = the pushed fpix).
|
||||||
; ----------------------------------------------------------------
|
; ----------------------------------------------------------------
|
||||||
fcDoSpan:
|
fcDoSpan:
|
||||||
.a16
|
.a16
|
||||||
|
|
@ -4433,17 +4441,15 @@ fcSbFull:
|
||||||
|
|
||||||
|
|
||||||
; ================================================================
|
; ================================================================
|
||||||
; Scratch slots (bank-0 BSS). dcSavedCol is kept for byte-faithful
|
; Scratch slots (bank-0 BSS). The old dcPixPtr / fcPixPtr / dpxlScratch /
|
||||||
; parity with the original DRAWPRIMS layout even though the inlined
|
; dlnScratch slots are gone: those held the arg0 far pointer for the
|
||||||
; plots no longer round-trip through it. dcPixPtr / fcPixPtr hold the
|
; [pix],y / [fpix],y indirect-long loads, and a BSS address cannot be
|
||||||
; stashed arg0 far pointer for the [pix],y / [fpix],y direct-page
|
; loaded into D (the direct page is always bank 0, the symbol is a LOAD-bank
|
||||||
; indirect-long loads.
|
; offset). Each routine now parks that pointer on the stack instead, which
|
||||||
|
; IS bank 0. dcSavedCol went with them -- the inlined plots stopped
|
||||||
|
; round-tripping through it long ago.
|
||||||
; ================================================================
|
; ================================================================
|
||||||
|
|
||||||
.section .bss.dcPixPtr,"aw"
|
|
||||||
.globl dcPixPtr
|
|
||||||
dcPixPtr:
|
|
||||||
.zero 4
|
|
||||||
|
|
||||||
.section .bss.dcX,"aw"
|
.section .bss.dcX,"aw"
|
||||||
.globl dcX
|
.globl dcX
|
||||||
|
|
@ -4490,15 +4496,7 @@ dcRowXP:
|
||||||
dcRowXN:
|
dcRowXN:
|
||||||
.zero 2
|
.zero 2
|
||||||
|
|
||||||
.section .bss.dcSavedCol,"aw"
|
|
||||||
.globl dcSavedCol
|
|
||||||
dcSavedCol:
|
|
||||||
.zero 2
|
|
||||||
|
|
||||||
.section .bss.fcPixPtr,"aw"
|
|
||||||
.globl fcPixPtr
|
|
||||||
fcPixPtr:
|
|
||||||
.zero 4
|
|
||||||
|
|
||||||
.section .bss.fcX,"aw"
|
.section .bss.fcX,"aw"
|
||||||
.globl fcX
|
.globl fcX
|
||||||
|
|
@ -5403,25 +5401,77 @@ mdrExit:
|
||||||
; ====================================================================
|
; ====================================================================
|
||||||
.section .text.iigsStageBands,"ax"
|
.section .text.iigsStageBands,"ax"
|
||||||
.globl iigsStageBandsClearAll
|
.globl iigsStageBandsClearAll
|
||||||
|
; GROUP-SKIPPED 2026-09-25. The blind version rewrote all 400 band bytes plus
|
||||||
|
; the 25 group bytes after EVERY present -- 200 word stores at ~12 cyc plus the
|
||||||
|
; C group loop, about 1.14 ms of the 2.20 ms an IDLE present cost, spent
|
||||||
|
; clearing what was already clear. The group map makes that avoidable: it is a
|
||||||
|
; conservative hint, so a group byte of ZERO guarantees all eight of its rows
|
||||||
|
; were never widened and their bands still hold the CLEAN sentinels. Only dirty
|
||||||
|
; groups pay. This also clears the group bytes, which the C side no longer does.
|
||||||
|
;
|
||||||
|
; Skipping can only ever leave a band marked dirty (costing one redundant row
|
||||||
|
; copy next present); it can never clear a band that should have stayed dirty,
|
||||||
|
; so it cannot produce a stale pixel. Measured hash-neutral against the
|
||||||
|
; unmodified build, op for op.
|
||||||
|
;
|
||||||
|
; Twelve group PAIRS are walked as 16-bit reads (groups 0..23 = rows 0..191);
|
||||||
|
; group 24 (rows 192..199) is tested on its own, because a 13th pair read would
|
||||||
|
; run one byte past the 25-byte group array and clear band bytes past row 199.
|
||||||
iigsStageBandsClearAll:
|
iigsStageBandsClearAll:
|
||||||
php
|
php
|
||||||
rep #0x30
|
rep #0x30
|
||||||
.a16
|
.a16
|
||||||
.i16
|
.i16
|
||||||
ldx #198
|
ldx #22
|
||||||
|
isbcPair:
|
||||||
|
lda gStageDirtyGroup,x
|
||||||
|
beq isbcPairNext ; 16 rows untouched: already CLEAN
|
||||||
|
stz gStageDirtyGroup,x
|
||||||
|
txa
|
||||||
|
asl ; group byte offset * 8 = first row
|
||||||
|
asl
|
||||||
|
asl
|
||||||
|
tay
|
||||||
lda #0xFFFF ; CLEAN_MIN in both bytes
|
lda #0xFFFF ; CLEAN_MIN in both bytes
|
||||||
isbcMinLoop:
|
sta gStageMinWord+0,y
|
||||||
sta gStageMinWord,x
|
sta gStageMinWord+2,y
|
||||||
dex
|
sta gStageMinWord+4,y
|
||||||
dex
|
sta gStageMinWord+6,y
|
||||||
bpl isbcMinLoop
|
sta gStageMinWord+8,y
|
||||||
ldx #198
|
sta gStageMinWord+10,y
|
||||||
|
sta gStageMinWord+12,y
|
||||||
|
sta gStageMinWord+14,y
|
||||||
lda #0 ; CLEAN_MAX in both bytes
|
lda #0 ; CLEAN_MAX in both bytes
|
||||||
isbcMaxLoop:
|
sta gStageMaxWord+0,y
|
||||||
sta gStageMaxWord,x
|
sta gStageMaxWord+2,y
|
||||||
|
sta gStageMaxWord+4,y
|
||||||
|
sta gStageMaxWord+6,y
|
||||||
|
sta gStageMaxWord+8,y
|
||||||
|
sta gStageMaxWord+10,y
|
||||||
|
sta gStageMaxWord+12,y
|
||||||
|
sta gStageMaxWord+14,y
|
||||||
|
isbcPairNext:
|
||||||
dex
|
dex
|
||||||
dex
|
dex
|
||||||
bpl isbcMaxLoop
|
bpl isbcPair
|
||||||
|
sep #0x20
|
||||||
|
.a8
|
||||||
|
lda gStageDirtyGroup+24
|
||||||
|
beq isbcDone
|
||||||
|
stz gStageDirtyGroup+24
|
||||||
|
rep #0x20
|
||||||
|
.a16
|
||||||
|
lda #0xFFFF
|
||||||
|
sta gStageMinWord+192
|
||||||
|
sta gStageMinWord+194
|
||||||
|
sta gStageMinWord+196
|
||||||
|
sta gStageMinWord+198
|
||||||
|
lda #0
|
||||||
|
sta gStageMaxWord+192
|
||||||
|
sta gStageMaxWord+194
|
||||||
|
sta gStageMaxWord+196
|
||||||
|
sta gStageMaxWord+198
|
||||||
|
isbcDone:
|
||||||
plp
|
plp
|
||||||
rtl
|
rtl
|
||||||
|
|
||||||
|
|
|
||||||
338
src/x68000/hal.c
338
src/x68000/hal.c
|
|
@ -42,6 +42,7 @@
|
||||||
// palette this port needs for SCB per-band palettes. There is no 320x200 mode on
|
// palette this port needs for SCB per-band palettes. There is no 320x200 mode on
|
||||||
// this machine, so the stage is centred and letterboxed.
|
// this machine, so the stage is centred and letterboxed.
|
||||||
#define X68K_CRTMOD_512_256 13
|
#define X68K_CRTMOD_512_256 13
|
||||||
|
#define X68K_CRTMOD_512_16 12 // 512x512, 16 colours, 31 kHz -- text plane
|
||||||
|
|
||||||
// _iocs_bitsns key-group numbers that carry the keys jlKeyE cares about.
|
// _iocs_bitsns key-group numbers that carry the keys jlKeyE cares about.
|
||||||
#define X68K_KEYGROUP_COUNT 15
|
#define X68K_KEYGROUP_COUNT 15
|
||||||
|
|
@ -53,6 +54,31 @@
|
||||||
// MC68901 MFP general-purpose I/O. Bit 4 is the CRTC's V-DISP line.
|
// MC68901 MFP general-purpose I/O. Bit 4 is the CRTC's V-DISP line.
|
||||||
#define X68K_MFP_GPIP ((volatile uint8_t *)0xE88001L)
|
#define X68K_MFP_GPIP ((volatile uint8_t *)0xE88001L)
|
||||||
#define X68K_GPIP_VDISP 0x10u
|
#define X68K_GPIP_VDISP 0x10u
|
||||||
|
|
||||||
|
// Which plane jlpPresent writes. 1 = the TEXT plane at $E00000, which is
|
||||||
|
// PLANAR at 128 bytes per row -- the stage's own layout -- so a present is a
|
||||||
|
// per-row copy per plane with no pixel conversion at all. 0 = the 256-colour
|
||||||
|
// GRAPHICS plane, where a pixel owns a whole 16-bit GVRAM word and every eight
|
||||||
|
// pixels cost sixteen table reads to assemble. Measured on the text plane:
|
||||||
|
// 0.5 bytes of traffic per pixel against GVRAM's 2, and no gather.
|
||||||
|
//
|
||||||
|
// The graphics path is kept because it is the one that needs no raster
|
||||||
|
// interrupt to honour the SCB: the 256-entry palette holds all sixteen
|
||||||
|
// 16-colour palettes at once, so (scb << 4) | nibble picks the band by value.
|
||||||
|
// The text plane has ONE 16-entry palette, so per-band palettes need the
|
||||||
|
// beam-chasing interrupt below.
|
||||||
|
#ifndef X68K_PRESENT_TEXT
|
||||||
|
#define X68K_PRESENT_TEXT 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// CRTC register 9: the raster the compare interrupt fires on. It does not
|
||||||
|
// reach the CPU directly -- it drives MFP GPIP6 (channel 14, vector $4E,
|
||||||
|
// exception address $138), which is the vector _CRTCRAS installs into.
|
||||||
|
#define X68K_CRTC_RASTER ((volatile uint16_t *)0xE80012L)
|
||||||
|
#define X68K_MFP_IERA ((volatile uint8_t *)0xE88007L) // reg 3
|
||||||
|
#define X68K_MFP_IMRA ((volatile uint8_t *)0xE88013L) // reg 9
|
||||||
|
#define X68K_MFP_GPIP6 0x40u // channel 14
|
||||||
|
#define X68K_RASTER_VEC_ADDR 0x00000138ul // vector $4E
|
||||||
#define X68K_VBL_SPIN_LIMIT 2000000ul
|
#define X68K_VBL_SPIN_LIMIT 2000000ul
|
||||||
#define X68K_TEE_SPIN_LIMIT 200000ul
|
#define X68K_TEE_SPIN_LIMIT 200000ul
|
||||||
|
|
||||||
|
|
@ -159,12 +185,17 @@ static bool gTickHooked = false;
|
||||||
static uint32_t tickHzFromMfp(void);
|
static uint32_t tickHzFromMfp(void);
|
||||||
uint32_t jlpMillisElapsed(void);
|
uint32_t jlpMillisElapsed(void);
|
||||||
uint16_t jlpFrameHz(void);
|
uint16_t jlpFrameHz(void);
|
||||||
static void buildSpreadTables(void);
|
|
||||||
static void spreadSetPalBase(uint8_t palBase);
|
|
||||||
static void calibrateTickHz(void);
|
static void calibrateTickHz(void);
|
||||||
static void installTickIsr(void);
|
static void installTickIsr(void);
|
||||||
|
#if X68K_PRESENT_TEXT
|
||||||
|
static void clearTextPlane(void);
|
||||||
|
static void disarmRaster(void);
|
||||||
|
#endif
|
||||||
|
#if !X68K_PRESENT_TEXT
|
||||||
|
static void buildSpreadTables(void);
|
||||||
|
static void spreadSetPalBase(uint8_t palBase);
|
||||||
static void uploadGfxPalette(const jlSurfaceT *src);
|
static void uploadGfxPalette(const jlSurfaceT *src);
|
||||||
|
#endif
|
||||||
|
|
||||||
// serial.c: drain the tiny (~64 B) RSDRV receive ring into the port's soft ring. Called from the
|
// serial.c: drain the tiny (~64 B) RSDRV receive ring into the port's soft ring. Called from the
|
||||||
// long-running paths below (present / VBL wait / input poll) so mid-frame serial bytes are never
|
// long-running paths below (present / VBL wait / input poll) so mid-frame serial bytes are never
|
||||||
|
|
@ -188,6 +219,27 @@ bool jlpInit(const jlConfigT *config) {
|
||||||
// if we were already supervisor -- in which case do not switch back.
|
// if we were already supervisor -- in which case do not switch back.
|
||||||
gPrevSsp = _dos_super(0);
|
gPrevSsp = _dos_super(0);
|
||||||
gPrevCrtMode = _iocs_crtmod(-1); // -1 queries without changing
|
gPrevCrtMode = _iocs_crtmod(-1); // -1 queries without changing
|
||||||
|
#if X68K_PRESENT_TEXT
|
||||||
|
// 16 colours, because the present writes the four 1bpp TEXT planes directly
|
||||||
|
// and that is what they are. _G_CLR_ON clears the GRAPHICS plane so it
|
||||||
|
// cannot show through underneath, and then the text plane is wiped to get
|
||||||
|
// rid of Human68k's desktop -- which the graphics-plane path could only
|
||||||
|
// leave bleeding over the frame (a reg[2] &= ~0x20 write to hide it did NOT
|
||||||
|
// take on MAME). Presenting INTO the text plane makes that bleed go away by
|
||||||
|
// construction: we own every pixel of it.
|
||||||
|
// Mode 13, NOT the 16-colour mode 12 you might expect for a 4bpp text
|
||||||
|
// plane. MEASURED: crtmod 12 costs about 45% of ALL CPU throughput --
|
||||||
|
// jlKeyDown fell 63728 -> 34624 ops/sec and jlMouseX 108679 -> 60071, and
|
||||||
|
// those touch nothing but RAM, so it is the mode and not the drawing. Mode
|
||||||
|
// 13 keeps full speed and displays the text plane anyway: the desktop
|
||||||
|
// "bleeding over our frame" that this port used to document as a cosmetic
|
||||||
|
// wart was the text plane being composited ON TOP, which is exactly what
|
||||||
|
// we now want. _G_CLR_ON blacks the graphics plane underneath and the wipe
|
||||||
|
// takes the desktop away.
|
||||||
|
_iocs_crtmod(X68K_CRTMOD_512_256);
|
||||||
|
_iocs_g_clr_on();
|
||||||
|
clearTextPlane();
|
||||||
|
#else
|
||||||
_iocs_crtmod(X68K_CRTMOD_512_256);
|
_iocs_crtmod(X68K_CRTMOD_512_256);
|
||||||
_iocs_g_clr_on(); // clear graphics + enable the plane
|
_iocs_g_clr_on(); // clear graphics + enable the plane
|
||||||
// crtmod 13 leaves the video controller in 65536-colour DIRECT mode (reg[0] & 3 == 3). This port
|
// crtmod 13 leaves the video controller in 65536-colour DIRECT mode (reg[0] & 3 == 3). This port
|
||||||
|
|
@ -199,6 +251,7 @@ bool jlpInit(const jlConfigT *config) {
|
||||||
// plane (the Drv0-3 + kana/romaji desktop that _iocs_crtmod leaves on) bleeds over our frame. A
|
// plane (the Drv0-3 + kana/romaji desktop that _iocs_crtmod leaves on) bleeds over our frame. A
|
||||||
// reg[2] &= ~0x20 write to hide it did NOT take on MAME (the desktop stayed visible), so that cosmetic
|
// reg[2] &= ~0x20 write to hide it did NOT take on MAME (the desktop stayed visible), so that cosmetic
|
||||||
// bleed is left as a follow-up; it does not affect the graphic layers we draw into.
|
// bleed is left as a follow-up; it does not affect the graphic layers we draw into.
|
||||||
|
#endif
|
||||||
// NOTE: installing vdispHandler via _iocs_vdispst HANGS the machine --
|
// NOTE: installing vdispHandler via _iocs_vdispst HANGS the machine --
|
||||||
// tested, no serial output at all, so it wedges before main() gets going.
|
// tested, no serial output at all, so it wedges before main() gets going.
|
||||||
// IOCS does NOT wrap the handler: _VDISPST writes the pointer straight into
|
// IOCS does NOT wrap the handler: _VDISPST writes the pointer straight into
|
||||||
|
|
@ -233,6 +286,9 @@ void jlpShutdown(void) {
|
||||||
__asm__ volatile ("move.w %0,%%sr" :: "d" (sr));
|
__asm__ volatile ("move.w %0,%%sr" :: "d" (sr));
|
||||||
gTickHooked = false;
|
gTickHooked = false;
|
||||||
}
|
}
|
||||||
|
#if X68K_PRESENT_TEXT
|
||||||
|
disarmRaster();
|
||||||
|
#endif
|
||||||
if (gPrevCrtMode >= 0) {
|
if (gPrevCrtMode >= 0) {
|
||||||
_iocs_crtmod(gPrevCrtMode);
|
_iocs_crtmod(gPrevCrtMode);
|
||||||
}
|
}
|
||||||
|
|
@ -360,6 +416,154 @@ static void calibrateTickHz(void) {
|
||||||
|
|
||||||
// ----- SCB per-band palette --------------------------------------------------
|
// ----- SCB per-band palette --------------------------------------------------
|
||||||
|
|
||||||
|
#if X68K_PRESENT_TEXT
|
||||||
|
// The text plane has one 16-entry palette, so honouring the SCB means changing
|
||||||
|
// it as the beam scans. A band is a RUN of scanlines sharing an SCB value, and
|
||||||
|
// only transitions matter: one CRTC raster interrupt per transition, which for
|
||||||
|
// a single-palette screen (Space Taxi reports one band) is none at all.
|
||||||
|
//
|
||||||
|
// SCB values are freely reusable, and reuse does NOT reduce the interrupt
|
||||||
|
// count: an A-B-A-B screen uses two palettes but still needs a reload at every
|
||||||
|
// change, because the hardware palette was overwritten in between. The cost is
|
||||||
|
// the number of TRANSITIONS, worst case one per scanline.
|
||||||
|
#define X68K_MAX_BANDS SURFACE_HEIGHT
|
||||||
|
|
||||||
|
extern void x68kRasterIsr(void); // src/x68000/x68kRasterIsr.s
|
||||||
|
|
||||||
|
// Every palette pre-converted to the hardware's word format, so the interrupt
|
||||||
|
// service copies words and does no arithmetic. Rebuilt only when a palette
|
||||||
|
// changes.
|
||||||
|
static uint16_t gScbPal[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE];
|
||||||
|
static uint16_t gBandLine[X68K_MAX_BANDS]; // display line the band starts on
|
||||||
|
static uint8_t gBandPal[X68K_MAX_BANDS]; // which palette it selects
|
||||||
|
static uint16_t gBandCount;
|
||||||
|
static volatile uint16_t gBandNext; // service cursor
|
||||||
|
static bool gRasterArmed;
|
||||||
|
static uint32_t gRasterOldVec; // $138 before we took it
|
||||||
|
|
||||||
|
|
||||||
|
// Interrupt service, called from the raw shim in x68kRasterIsr.s with every
|
||||||
|
// register already saved -- which is what lets this be ordinary C. Loads the
|
||||||
|
// current band's palette and re-points the compare register at the next band,
|
||||||
|
// wrapping to band 0 for the next frame.
|
||||||
|
void x68kRasterService(void) {
|
||||||
|
const uint16_t *pal;
|
||||||
|
uint16_t band;
|
||||||
|
uint16_t col;
|
||||||
|
|
||||||
|
band = gBandNext;
|
||||||
|
if (band >= gBandCount) {
|
||||||
|
band = 0u;
|
||||||
|
}
|
||||||
|
pal = gScbPal[gBandPal[band]];
|
||||||
|
for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) {
|
||||||
|
X68K_TEXT_PALETTE[col] = pal[col];
|
||||||
|
}
|
||||||
|
band = (uint16_t)((band + 1u < gBandCount) ? (band + 1u) : 0u);
|
||||||
|
gBandNext = band;
|
||||||
|
*X68K_CRTC_RASTER = gBandLine[band];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Quantise every palette once into the form the service copies.
|
||||||
|
static void buildScbPalettes(const jlSurfaceT *src) {
|
||||||
|
uint16_t pal;
|
||||||
|
uint16_t col;
|
||||||
|
|
||||||
|
for (pal = 0u; pal < SURFACE_PALETTE_COUNT; pal++) {
|
||||||
|
for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) {
|
||||||
|
gScbPal[pal][col] = x68kColorFromRgb12(src->palette[pal][col]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// One band per RUN of equal SCB values. Lines are stored already offset into
|
||||||
|
// display space, because that is what the compare register wants.
|
||||||
|
static void buildScbBands(const jlSurfaceT *src) {
|
||||||
|
uint16_t line;
|
||||||
|
uint8_t idx;
|
||||||
|
uint8_t prev;
|
||||||
|
|
||||||
|
gBandCount = 0u;
|
||||||
|
prev = 0xFFu;
|
||||||
|
for (line = 0u; line < SURFACE_HEIGHT; line++) {
|
||||||
|
idx = src->scb[line];
|
||||||
|
if (idx >= SURFACE_PALETTE_COUNT) {
|
||||||
|
idx = 0u;
|
||||||
|
}
|
||||||
|
if (idx != prev) {
|
||||||
|
if (gBandCount < X68K_MAX_BANDS) {
|
||||||
|
gBandLine[gBandCount] = (uint16_t)(line + X68K_ORIGIN_Y);
|
||||||
|
gBandPal[gBandCount] = idx;
|
||||||
|
gBandCount++;
|
||||||
|
}
|
||||||
|
prev = idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (gBandCount == 0u) { // no line claimed a palette
|
||||||
|
gBandLine[0] = X68K_ORIGIN_Y;
|
||||||
|
gBandPal[0] = 0u;
|
||||||
|
gBandCount = 1u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A single-palette screen needs no interrupt: load it once and leave the
|
||||||
|
// hardware alone. Anything else arms the compare on band 0 and lets the
|
||||||
|
// service walk the list.
|
||||||
|
// Mask GPIP6 and hand $138 back. This MUST run before the program exits: the
|
||||||
|
// handler lives in this program's RAM, so leaving the vector pointing at it
|
||||||
|
// sends the next raster interrupt into freed memory -- the same dangling-ISR
|
||||||
|
// trap this port already fixed for the Timer C and SCC RX vectors.
|
||||||
|
static void disarmRaster(void) {
|
||||||
|
uint16_t sr;
|
||||||
|
|
||||||
|
if (!gRasterArmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
__asm__ volatile ("move.w %%sr,%0\n\tori.w #0x0700,%%sr" : "=d" (sr));
|
||||||
|
*X68K_MFP_IMRA = (uint8_t)(*X68K_MFP_IMRA & (uint8_t)~X68K_MFP_GPIP6);
|
||||||
|
*X68K_MFP_IERA = (uint8_t)(*X68K_MFP_IERA & (uint8_t)~X68K_MFP_GPIP6);
|
||||||
|
if (gRasterOldVec != 0ul) {
|
||||||
|
static volatile uintptr_t addr = X68K_RASTER_VEC_ADDR;
|
||||||
|
*(volatile uint32_t *)addr = gRasterOldVec;
|
||||||
|
}
|
||||||
|
__asm__ volatile ("move.w %0,%%sr" :: "d" (sr));
|
||||||
|
gRasterArmed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void applyScbBands(void) {
|
||||||
|
uint16_t col;
|
||||||
|
|
||||||
|
if (gBandCount <= 1u) {
|
||||||
|
disarmRaster();
|
||||||
|
for (col = 0u; col < SURFACE_COLORS_PER_PALETTE; col++) {
|
||||||
|
X68K_TEXT_PALETTE[col] = gScbPal[gBandPal[0]][col];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gBandNext = 0u;
|
||||||
|
if (!gRasterArmed) {
|
||||||
|
// _CRTCRAS takes the handler and the line, and owns enabling GPIP6 in
|
||||||
|
// the MFP. The handler must be the raw shim, never a C function.
|
||||||
|
// Remember the old vector first so shutdown can put it back.
|
||||||
|
{
|
||||||
|
static volatile uintptr_t addr = X68K_RASTER_VEC_ADDR;
|
||||||
|
gRasterOldVec = *(volatile uint32_t *)addr;
|
||||||
|
}
|
||||||
|
(void)_iocs_crtcras(x68kRasterIsr, (int)gBandLine[0]);
|
||||||
|
gRasterArmed = true;
|
||||||
|
} else {
|
||||||
|
*X68K_CRTC_RASTER = gBandLine[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // X68K_PRESENT_TEXT
|
||||||
|
|
||||||
|
#if !X68K_PRESENT_TEXT
|
||||||
|
// ----- GRAPHICS-plane present support (only when X68K_PRESENT_TEXT is 0) -----
|
||||||
|
|
||||||
// Load all 16 palettes into the 256-entry graphics palette: entry p*16+c is
|
// Load all 16 palettes into the 256-entry graphics palette: entry p*16+c is
|
||||||
// palette p colour c, so a pixel byte of (p << 4) | c selects it directly.
|
// palette p colour c, so a pixel byte of (p << 4) | c selects it directly.
|
||||||
// Rebuilt only when a palette actually changes; the SCB flag is deliberately
|
// Rebuilt only when a palette actually changes; the SCB flag is deliberately
|
||||||
|
|
@ -459,6 +663,9 @@ static void spreadSetPalBase(uint8_t palBase) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif // !X68K_PRESENT_TEXT
|
||||||
|
|
||||||
|
|
||||||
// ----- Present --------------------------------------------------------------
|
// ----- Present --------------------------------------------------------------
|
||||||
|
|
||||||
// Copy the surface's four planes into the TEXT PLANE at $E00000.
|
// Copy the surface's four planes into the TEXT PLANE at $E00000.
|
||||||
|
|
@ -471,6 +678,130 @@ static void spreadSetPalBase(uint8_t palBase) {
|
||||||
// measured wait states punish, so that 4x cut is the win.
|
// measured wait states punish, so that 4x cut is the win.
|
||||||
//
|
//
|
||||||
// Dirty rows only, and only the marked band within a row.
|
// Dirty rows only, and only the marked band within a row.
|
||||||
|
#if X68K_PRESENT_TEXT
|
||||||
|
// Wipe the whole visible text plane once at start-up. Human68k leaves its
|
||||||
|
// desktop there, and we only ever write the centred stage rectangle, so
|
||||||
|
// without this the surround keeps the desktop. Long stores; one-time cost.
|
||||||
|
static void clearTextPlane(void) {
|
||||||
|
uint16_t plane;
|
||||||
|
uint16_t i;
|
||||||
|
|
||||||
|
for (plane = 0u; plane < X68K_BITPLANES; plane++) {
|
||||||
|
volatile uint32_t *p = (volatile uint32_t *)(X68K_TVRAM_PLANE0
|
||||||
|
+ ((uint32_t)plane * X68K_TVRAM_PLANE_STEP));
|
||||||
|
for (i = 0u; i < (uint16_t)(X68K_DISPLAY_H * (X68K_TVRAM_STRIDE / 4u)); i++) {
|
||||||
|
p[i] = 0ul;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Copy one row of one plane into the text plane. Both ends sit at the same
|
||||||
|
// offset from a 4-aligned base -- the stage plane's row stride is 40 and the
|
||||||
|
// text plane's is 128, both multiples of 4, and the centring offset
|
||||||
|
// X68K_ORIGIN_BYTE is 12 -- so once the leading bytes are stepped off, the rest
|
||||||
|
// moves a long at a time with no shifting. Inline rather than memcpy because a
|
||||||
|
// full present is 800 of these calls and the call alone was costing more than
|
||||||
|
// the bytes.
|
||||||
|
static inline __attribute__((always_inline))
|
||||||
|
void x68kCopyRowToText(volatile uint8_t *dst, const uint8_t *srcRow, uint16_t count) {
|
||||||
|
while (count != 0u && (((uintptr_t)dst & 3u) != 0u)) {
|
||||||
|
*dst++ = *srcRow++;
|
||||||
|
count--;
|
||||||
|
}
|
||||||
|
while (count >= 4u) {
|
||||||
|
*(volatile uint32_t *)dst = *(const uint32_t *)srcRow;
|
||||||
|
dst += 4;
|
||||||
|
srcRow += 4;
|
||||||
|
count -= 4u;
|
||||||
|
}
|
||||||
|
while (count != 0u) {
|
||||||
|
*dst++ = *srcRow++;
|
||||||
|
count--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Present to the TEXT plane. The stage and the text plane are both four 1bpp
|
||||||
|
// planes, so this is a copy -- no pixel conversion, no table, 0.5 bytes of bus
|
||||||
|
// traffic per pixel. The SCB is honoured by the raster interrupt above rather
|
||||||
|
// than by a per-pixel palette index.
|
||||||
|
void jlpPresent(const jlSurfaceT *src) {
|
||||||
|
X68kPlanarT *pd;
|
||||||
|
const uint8_t *sp;
|
||||||
|
uint16_t g;
|
||||||
|
uint16_t y;
|
||||||
|
uint16_t yEnd;
|
||||||
|
uint16_t plane;
|
||||||
|
uint16_t firstByte;
|
||||||
|
uint16_t rowBytes;
|
||||||
|
uint16_t blitRows = 0;
|
||||||
|
|
||||||
|
// Four rows of dirty band read as ONE long. A clean row's minWord is the
|
||||||
|
// 0xFF CLEAN sentinel and any touched row's is <= STAGE_DIRTY_FULL_MAX, so
|
||||||
|
// four clean rows read exactly 0xFFFFFFFF and the whole group skips. This
|
||||||
|
// is the trick src/dos/hal.c already used and this port was missing:
|
||||||
|
// visiting all two hundred rows one at a time cost roughly 0.9 ms of EVERY
|
||||||
|
// present, dirty or not. A failing group still runs the per-row clean test,
|
||||||
|
// so the rows written are byte-identical to the plain walk.
|
||||||
|
typedef uint32_t __attribute__((may_alias)) BandGroupT;
|
||||||
|
const BandGroupT *minGroups = (const BandGroupT *)(const void *)gStageMinWord;
|
||||||
|
|
||||||
|
if (src == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vdispPoll(); // keep the frame counter honest: see the note on vdispPoll
|
||||||
|
pd = x68kSurfacePlanar(src);
|
||||||
|
if (pd == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// gStagePaletteDirty and gStageScbDirty live in core/surface.c; consume
|
||||||
|
// both here the way src/iigs/hal.c does. A palette change alters what the
|
||||||
|
// bands load, so it forces the band apply too.
|
||||||
|
if (gStagePaletteDirty) {
|
||||||
|
buildScbPalettes(src);
|
||||||
|
gStagePaletteDirty = false;
|
||||||
|
gStageScbDirty = true;
|
||||||
|
}
|
||||||
|
if (gStageScbDirty) {
|
||||||
|
buildScbBands(src);
|
||||||
|
applyScbBands();
|
||||||
|
gStageScbDirty = false;
|
||||||
|
}
|
||||||
|
x68kSerialPump();
|
||||||
|
for (g = 0u; g < (uint16_t)(SURFACE_HEIGHT / 4u); g++) {
|
||||||
|
if (minGroups[g] == 0xFFFFFFFFul) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
yEnd = (uint16_t)(g * 4u + 4u);
|
||||||
|
for (y = (uint16_t)(g * 4u); y < yEnd; y++) {
|
||||||
|
if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Same pump cadence as the graphics path: a blitted row is line
|
||||||
|
// time, so keep the 64-byte RSDRV ring from overflowing.
|
||||||
|
if ((blitRows & 1u) == 0u) {
|
||||||
|
x68kSerialPump();
|
||||||
|
}
|
||||||
|
blitRows++;
|
||||||
|
// Bands are 16-bit stage words = 4 pixels = 2 bytes of a plane.
|
||||||
|
firstByte = (uint16_t)(gStageMinWord[y] >> 1);
|
||||||
|
rowBytes = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u);
|
||||||
|
if ((uint16_t)(firstByte + rowBytes) > X68K_BYTES_PER_ROW) {
|
||||||
|
rowBytes = (uint16_t)(X68K_BYTES_PER_ROW - firstByte);
|
||||||
|
}
|
||||||
|
for (plane = 0u; plane < X68K_BITPLANES; plane++) {
|
||||||
|
sp = pd->planes[plane] + ((uint16_t)y * X68K_BYTES_PER_ROW) + firstByte;
|
||||||
|
x68kCopyRowToText((volatile uint8_t *)(X68K_TVRAM_PLANE0
|
||||||
|
+ ((uint32_t)plane * X68K_TVRAM_PLANE_STEP)
|
||||||
|
+ x68kTvramOffset(0u, (uint16_t)(y + X68K_ORIGIN_Y))
|
||||||
|
+ X68K_ORIGIN_BYTE + firstByte),
|
||||||
|
sp, rowBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
void jlpPresent(const jlSurfaceT *src) {
|
void jlpPresent(const jlSurfaceT *src) {
|
||||||
X68kPlanarT *pd;
|
X68kPlanarT *pd;
|
||||||
volatile uint32_t *wordDst;
|
volatile uint32_t *wordDst;
|
||||||
|
|
@ -576,6 +907,7 @@ void jlpPresent(const jlSurfaceT *src) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif // X68K_PRESENT_TEXT
|
||||||
|
|
||||||
// ----- Input ----------------------------------------------------------------
|
// ----- Input ----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -242,19 +242,52 @@ uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The Z8530's register pointer is ONE register for the whole chip, shared by
|
||||||
|
// both channels, and selecting a register is a separate access from using it.
|
||||||
|
// So any two-access sequence has to be atomic or the two accesses belong to
|
||||||
|
// different transactions: the interrupting write lands as DATA for whatever
|
||||||
|
// register the interrupted code had selected, and the interrupted code's own
|
||||||
|
// data write then goes to WR0 as a command.
|
||||||
|
//
|
||||||
|
// Both IOCS traps below do such a sequence on channel A, and our own channel A
|
||||||
|
// receive ISR writes the control port to drop the interrupt-under-service. An
|
||||||
|
// arriving byte can therefore land between a trap's pointer write and its data
|
||||||
|
// access -- corrupting a receiver register AND losing the IUS reset, which
|
||||||
|
// wedges the SCC's interrupt daisy chain so no further byte ever interrupts.
|
||||||
|
// Masking around each trap closes that window.
|
||||||
|
//
|
||||||
|
// The mask goes around each CALL and not around the spin loop: holding it for
|
||||||
|
// a whole byte time would be the receive latency the ISR exists to avoid. One
|
||||||
|
// trap is a few microseconds, far inside a byte at any supported baud.
|
||||||
|
//
|
||||||
|
// The two INTERRUPT handlers cannot collide with each other -- IOCS's channel B
|
||||||
|
// mouse handler and our channel A handler are the same 68000 interrupt level,
|
||||||
|
// so taking one masks the other -- and serial.c's own direct RR2 read in
|
||||||
|
// sccIsrInstall masks already. What is left is IOCS's internals interrupting
|
||||||
|
// each other, which is the ROM's business and not reachable from here.
|
||||||
uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
|
uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
|
||||||
uint16_t n;
|
uint16_t n;
|
||||||
uint32_t guard;
|
uint32_t guard;
|
||||||
|
uint16_t sr;
|
||||||
|
int ready;
|
||||||
|
|
||||||
for (n = 0u; n < len; n++) {
|
for (n = 0u; n < len; n++) {
|
||||||
guard = 0ul;
|
guard = 0ul;
|
||||||
while (_iocs_osns232c() == 0) {
|
for (;;) {
|
||||||
|
sr = x68kIrqMask();
|
||||||
|
ready = _iocs_osns232c();
|
||||||
|
x68kIrqRestore(sr);
|
||||||
|
if (ready != 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
guard++;
|
guard++;
|
||||||
if (guard > X68K_TX_SPIN_LIMIT) {
|
if (guard > X68K_TX_SPIN_LIMIT) {
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sr = x68kIrqMask();
|
||||||
_iocs_out232c((int)buf[n]);
|
_iocs_out232c((int)buf[n]);
|
||||||
|
x68kIrqRestore(sr);
|
||||||
}
|
}
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
36
src/x68000/x68kRasterIsr.s
Normal file
36
src/x68000/x68kRasterIsr.s
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
| Sharp X68000 SCB raster interrupt.
|
||||||
|
|
|
||||||
|
| Per-band palettes need the palette changed as the beam scans, because the
|
||||||
|
| text plane has ONE 16-entry hardware palette and the bands need different
|
||||||
|
| ones at the same time. The CRTC's raster compare (register 9 at $E80012)
|
||||||
|
| provides the trigger.
|
||||||
|
|
|
||||||
|
| It does NOT raise a CPU interrupt of its own: it drives the MFP's GPIP6 line,
|
||||||
|
| which is MFP channel 14, vector $40|14 = $4E, exception address $138. That is
|
||||||
|
| exactly the vector _CRTCRAS installs into, and IOCS does NOT wrap the
|
||||||
|
| handler -- the pointer goes straight into the vector. So this must be a RAW
|
||||||
|
| interrupt routine. A C function (RTS, and free to clobber d0/d1/a0/a1) can
|
||||||
|
| never be valid there; the port already proved that by hanging the machine
|
||||||
|
| with one on $134 (see the _VDISPST note in hal.c).
|
||||||
|
|
|
||||||
|
| This shim saves EVERY register, calls the C service, restores, drops the
|
||||||
|
| MFP's in-service bit for channel 14, and returns with RTE. Saving everything
|
||||||
|
| is exactly what lets the service side be an ordinary C function.
|
||||||
|
|
|
||||||
|
| GAS m68k syntax, ELF target -- symbols carry NO leading underscore.
|
||||||
|
|
||||||
|
.equ MFP_ISRA, 0xE8800F | MFP reg 7, in-service A
|
||||||
|
.equ ISRA_NOT_G6, 0xBF | clear bit 6 (channel 14), keep the rest
|
||||||
|
|
||||||
|
.text
|
||||||
|
.even
|
||||||
|
.globl x68kRasterIsr
|
||||||
|
|
||||||
|
x68kRasterIsr:
|
||||||
|
movem.l %d0-%d7/%a0-%a6,%sp@-
|
||||||
|
jsr x68kRasterService
|
||||||
|
movem.l %sp@+,%d0-%d7/%a0-%a6
|
||||||
|
| Ack LAST: the in-service bit holds off another channel-14
|
||||||
|
| interrupt while the service reprograms the compare register.
|
||||||
|
move.b #ISRA_NOT_G6,MFP_ISRA
|
||||||
|
rte
|
||||||
128
tools/gradeAudioPhases.py
Executable file
128
tools/gradeAudioPhases.py
Executable file
|
|
@ -0,0 +1,128 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Grade a recorded WAV against expected loudness phases.
|
||||||
|
|
||||||
|
Shared by scripts/verify-amiga-audio.sh and scripts/verify-dos-audio.sh.
|
||||||
|
Both ports emit sound through hardware no emulator here will let us tap
|
||||||
|
register by register (Paula via fs-uae, a Sound Blaster via DOSBox), so
|
||||||
|
those gates record what actually came out and grade it. The IIgs and ST
|
||||||
|
gates read their chips directly and do not need this.
|
||||||
|
|
||||||
|
The scripts drive the AUDIO example through four phases whose expected
|
||||||
|
loudness differs by a wide margin, which is what makes recorded audio
|
||||||
|
gradeable at all rather than a judgement call:
|
||||||
|
|
||||||
|
music the baked tracker song, loud
|
||||||
|
paused P pressed: near silence
|
||||||
|
sfx SPACE during the pause, so ANY sound is the digital SFX
|
||||||
|
resumed P again: loud once more
|
||||||
|
|
||||||
|
Firing the SFX into a paused song is the whole trick. Mixed under the
|
||||||
|
music an 8-bit one-shot barely moves the total, but against silence it is
|
||||||
|
unambiguous -- and it still proves the two layers coexist, because the
|
||||||
|
music comes back afterwards on the same hardware.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
gradeAudioPhases.py <label> <file.wav> <phase> [<phase> ...]
|
||||||
|
|
||||||
|
phase = name:startSec:endSec:minRms:maxRms
|
||||||
|
minRms or maxRms may be empty to skip that bound.
|
||||||
|
|
||||||
|
Exit code:
|
||||||
|
0 = every phase inside its bounds
|
||||||
|
1 = at least one phase out of bounds, or the file is unusable
|
||||||
|
2 = usage error
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import wave
|
||||||
|
|
||||||
|
|
||||||
|
def readMono(path):
|
||||||
|
"""-> (samples as a list of floats, sample rate). Averages channels."""
|
||||||
|
with wave.open(path, 'rb') as w:
|
||||||
|
channels = w.getnchannels()
|
||||||
|
width = w.getsampwidth()
|
||||||
|
rate = w.getframerate()
|
||||||
|
count = w.getnframes()
|
||||||
|
raw = w.readframes(count)
|
||||||
|
if width != 2:
|
||||||
|
raise ValueError(f"expected 16-bit samples, got {width * 8}-bit")
|
||||||
|
import array
|
||||||
|
data = array.array('h')
|
||||||
|
data.frombytes(raw[:(len(raw) // (2 * channels)) * 2 * channels])
|
||||||
|
if channels == 1:
|
||||||
|
return data, rate
|
||||||
|
mono = array.array('d', [0.0]) * (len(data) // channels)
|
||||||
|
for i in range(len(mono)):
|
||||||
|
total = 0
|
||||||
|
base = i * channels
|
||||||
|
for c in range(channels):
|
||||||
|
total += data[base + c]
|
||||||
|
mono[i] = total / channels
|
||||||
|
return mono, rate
|
||||||
|
|
||||||
|
|
||||||
|
def rms(samples, rate, startSec, endSec):
|
||||||
|
a = int(startSec * rate)
|
||||||
|
b = min(int(endSec * rate), len(samples))
|
||||||
|
if b <= a:
|
||||||
|
return None
|
||||||
|
total = 0.0
|
||||||
|
for i in range(a, b):
|
||||||
|
total += samples[i] * samples[i]
|
||||||
|
return (total / (b - a)) ** 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
if len(argv) < 4:
|
||||||
|
print(__doc__, file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
label = argv[1]
|
||||||
|
path = argv[2]
|
||||||
|
specs = argv[3:]
|
||||||
|
|
||||||
|
try:
|
||||||
|
samples, rate = readMono(path)
|
||||||
|
except Exception as exc: # noqa: BLE001 - reported, not handled
|
||||||
|
print(f"{label}: FAIL - cannot read {path}: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
duration = len(samples) / float(rate)
|
||||||
|
if duration < 1.0:
|
||||||
|
print(f"{label}: FAIL - recorded only {duration:.2f}s of audio; the capture "
|
||||||
|
f"never attached to the emulator's output", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
fails = []
|
||||||
|
parts = []
|
||||||
|
for spec in specs:
|
||||||
|
bits = spec.split(':')
|
||||||
|
if len(bits) != 5:
|
||||||
|
print(f"{label}: bad phase spec {spec!r}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
name, t0, t1, lo, hi = bits
|
||||||
|
level = rms(samples, rate, float(t0), float(t1))
|
||||||
|
if level is None:
|
||||||
|
fails.append(f"phase '{name}' ({t0}-{t1}s) is past the end of a "
|
||||||
|
f"{duration:.1f}s recording")
|
||||||
|
continue
|
||||||
|
parts.append(f"{name}={level:.0f}")
|
||||||
|
if lo and level < float(lo):
|
||||||
|
fails.append(f"phase '{name}' RMS {level:.0f} is below {lo}: expected sound "
|
||||||
|
f"here and got near-silence")
|
||||||
|
if hi and level > float(hi):
|
||||||
|
fails.append(f"phase '{name}' RMS {level:.0f} is above {hi}: expected near-silence "
|
||||||
|
f"here and got sound")
|
||||||
|
|
||||||
|
print(f"{label}: {duration:.1f}s recorded, RMS " + " ".join(parts))
|
||||||
|
for f in fails:
|
||||||
|
print(f"{label}: {f}", file=sys.stderr)
|
||||||
|
if fails:
|
||||||
|
print(f"{label}: FAIL", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
print(f"{label}: PASS")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
Loading…
Add table
Reference in a new issue