Writing the repeat address stops loop start flags from overwriting it until
the next key on. Hardware fetches the first block's header on each of the
first three samples of the key on delay, so writes before the last fetch are
overwritten and do not latch; later writes latch regardless of pitch or how
long the first block plays.
This replaces the first-block heuristic and removes is_first_block.
Key on/off is only processed on every second sample, and a keyed-on voice
stays silent for four samples before the first attack tick, so ENVX first
changes six or seven samples after the KON write.
Key on clears has_samples, so the remaining delay is only checked before
reading a new block, and voices that are idle or mid-block cost the same as
before. Key off during the delay does not cut it short. IRQ prediction
accounts for the delay before the first block read.
Require decay to cross the sustain threshold before entering sustain, including
when the threshold is the maximum envelope level. Keep release ending at zero.
Start the pitch counter at sample index three so the first interpolation window
contains four decoded samples. Add hardware capture regressions for pitches
0x2000 and 0x1500, where ring rotation cannot hide the initial source phase.
Retain one fractional bit in each Gaussian product before accumulation to match
hardware captures. Add capture regressions at unity and fractional pitch, and
update the scalar/SIMD comparison.
Updating before the event is due can't raise an IRQ now that it's
scheduled for exactly when the first one happens, so all it changes is
the counters. Reading the counter or mode works out what the counter
would be instead, and only updates when the timer being read reaches
its target or overflows. Reads of GPU-clocked or paused timers don't
need the sysclk timers at all.
InvokeEarly() moves the event to its next interval before running the
callback. For the timers this is one tick away, so every register
access moved the event to the front of the queue, only for the callback
to schedule it back to where it was. The new variant leaves the event
alone, and relies on the callback to schedule it.
The CRTC event is only scheduled for the next IRQ or gate change of the
timers it updates. Nothing redid that after the counter, target or mode
was written, so a closer target or newly enabled sync mode wasn't seen
until the event next ran for some other reason, up to a frame later.
GetTicksUntilIRQ() said there was no IRQ coming when the counter was at
or above the target, or when the overflow IRQ was used with a resetting
target. The CRTC event wasn't scheduled for these, so they were raised
whenever it happened to run next.
It was treated as reached every time the counter was updated, including
when no ticks were added, so with the target IRQ enabled any access to
a timer register raised it. Now it's only reached when the counter is
reset by the mode write, and when it wraps around.
The partial tick in sysclk_div_8_carry was ignored, so the IRQ fired up
to 7 cycles after the counter reached the target. Since the carry
changes whenever the counters are updated, accessing any timer register
moved the IRQ around as well.
GetTicksUntilNextInterrupt() was still treating 0xFFFF as the overflow
point, and a counter sitting at the target as about to reach it. Both
cost an extra one tick event, or eight of them with sysclk/8.
The counter wasn't brought back into range before comparing it with the
target, so wrapping around always counted as reaching it. This raised
the target IRQ early, toggled the IRQ twice per loop in toggle mode, and
gave the wrong counter value when it was set above a resetting target.
SetGate() rescheduled the sysclk event without updating the counters
first when the gated timer was GPU-clocked, so the next IRQ got pushed
back by however long it had been since the last update. Gate changes
that were more frequent than the IRQ stopped it from firing at all.
Rectangles and axis-aligned quads are compared against a simple
per-pixel version of the pixel pipeline, clipped triangles against
the same triangle drawn without clipping, and VRAM copies against a
row-by-row copy.
The scalar loop only advanced the source pointer when the destination
pixel was not masked, so every pixel after a masked one was written
with the wrong source data for the rest of the transfer.
The vectorized loop always advances, so this only affected the trailing
pixels of each row, writes which wrap around, and the scalar
implementation.
Covers IR/colour saturation, 44-bit MAC overflow wrapping, NCLIP and ORGB
against reference formulas, and hashes the register state after random
instructions through both the interpreter and recompiler entry points.