mirror of
https://github.com/dingusdev/dingusppc.git
synced 2026-10-25 17:02:23 +00:00
viacuda: Keep timer 2 rollover and interrupt timing in sync
T2 readback wrapped after N+1 clocks, but its interrupt was scheduled for N+3. Mac OS Microseconds could see a wrapped counter with no pending interrupt, then clear the newly asserted interrupt by reading T2CL. The one-shot timer would never be serviced or rearmed. Abuse paces its intro text by repeatedly calling Microseconds until 150 ms have elapsed. This tight polling loop exposes the race. Once the interrupt is lost, the OS stops accumulating elapsed timer periods, while the hardware counter keeps wrapping. Microseconds then cycles within a roughly 84 ms window instead of advancing monotonically, so the 150 ms wait can no longer complete normally. The guest keeps running, but the text's pacing loop effectively stalls, accounting for the reported frozen or extremely slow scrolling. Use a shared time origin for counter readback and the N+1.5-clock interrupt, including the initial half-clock delay. Before reading T2 or IFR, also assert any overdue interrupt and cancel its queued callback, so realtime reads and acknowledgments don't depend on timer polling. Verified native Abuse intro and gameplay, native and Emscripten builds, and 84 timer checks covering rollover, acknowledgment and overdue reads. Co-Authored-By: joevt <950609+joevt@users.noreply.github.com> Co-Authored-By: Mihai Parparita <513813+mihaip@users.noreply.github.com>
This commit is contained in:
1 parent
21c6a80e2c
commit
4e254fc972
2 files changed
+34
-10
No files matched your search
+32
-10
@@ -160,10 +160,12 @@ uint8_t ViaCuda::read(int reg) {
|
||||
case VIA_T1LH:
|
||||
return this->via_t1lh;
|
||||
case VIA_T2CL:
|
||||
this->check_t2();
|
||||
this->_via_ifr &= ~VIA_IF_T2;
|
||||
update_irq();
|
||||
return this->calc_counter_val(this->t2_counter, this->t2_start_time) & 0xFFU;
|
||||
case VIA_T2CH:
|
||||
this->check_t2();
|
||||
return this->calc_counter_val(this->t2_counter, this->t2_start_time) >> 8;
|
||||
case VIA_SR:
|
||||
value = this->via_sr;
|
||||
@@ -175,6 +177,7 @@ uint8_t ViaCuda::read(int reg) {
|
||||
case VIA_PCR:
|
||||
return this->via_pcr;
|
||||
case VIA_IFR:
|
||||
this->check_t2();
|
||||
return this->_via_ifr;
|
||||
case VIA_IER:
|
||||
return (this->_via_ier | 0x80); // bit 7 always reads as "1"
|
||||
@@ -244,16 +247,7 @@ void ViaCuda::write(int reg, uint8_t value) {
|
||||
update_irq();
|
||||
// load initial value into counter 2
|
||||
this->t2_counter = (value << 8) | this->via_t2ll;
|
||||
// TODO: delay for one phase 2 clock
|
||||
// sample current vCPU time and remember it
|
||||
this->t2_start_time = TimerManager::get_instance()->current_time_ns();
|
||||
// set up timeout timer for T2
|
||||
TimerManager::get_instance()->add_oneshot_timer(this->t2_timer,
|
||||
(this->via_clk_dur * (this->t2_counter + 3) + (uint64_t(1) << 36)) >> 37,
|
||||
[this](uint64_t, uint64_t) {
|
||||
this->assert_t2_int();
|
||||
}
|
||||
);
|
||||
this->activate_t2(TimerManager::get_instance()->current_time_ns());
|
||||
break;
|
||||
case VIA_SR:
|
||||
this->via_sr = value;
|
||||
@@ -290,6 +284,9 @@ uint16_t ViaCuda::calc_counter_val(const uint16_t last_val, const uint64_t &star
|
||||
{
|
||||
// calculate current counter value based on elapsed time and timer frequency
|
||||
uint64_t cur_time = TimerManager::get_instance()->current_time_ns();
|
||||
// The loaded value remains visible during the initial half-clock delay.
|
||||
if (cur_time <= start_time)
|
||||
return last_val;
|
||||
uint64_t diff_hi;
|
||||
uint32_t diff_lo;
|
||||
_u32xu64(this->via_clk_freq, cur_time - start_time, diff_hi, diff_lo);
|
||||
@@ -314,6 +311,31 @@ void ViaCuda::activate_t1(uint64_t start_time) {
|
||||
);
|
||||
}
|
||||
|
||||
void ViaCuda::activate_t2(uint64_t start_time) {
|
||||
// T2's one-shot interrupt occurs N + 1.5 VIA clocks after loading N.
|
||||
// Timer begins counting after 0.5 VIA clocks
|
||||
start_time += (this->via_clk_dur + (uint64_t(1) << 37)) >> 38;
|
||||
this->t2_start_time = start_time;
|
||||
// Timer ends N + 1 VIA clocks after start time.
|
||||
uint64_t timeout = start_time +
|
||||
((this->via_clk_dur * (this->t2_counter + 1) + (uint64_t(1) << 36)) >> 37);
|
||||
TimerManager::get_instance()->add_absolute_timer(
|
||||
this->t2_timer, timeout, 0, [this](uint64_t, uint64_t) { this->assert_t2_int(); });
|
||||
}
|
||||
|
||||
void ViaCuda::check_t2() {
|
||||
// The process_events is where assert_t2_int is supposed to happen,
|
||||
// but if a register read doesn't see that an interrupt happened
|
||||
// when the counter wraps, then Microseconds in classic Mac OS will
|
||||
// fail, so we call assert_t2_int here which sets the IFR register
|
||||
// makes the interrupt happen as soon as possible.
|
||||
auto tm = TimerManager::get_instance();
|
||||
if (this->t2_timer.active && tm->current_time_ns() >= this->t2_timer.timeout_ns) {
|
||||
tm->cancel_timer(this->t2_timer);
|
||||
this->assert_t2_int();
|
||||
}
|
||||
}
|
||||
|
||||
void ViaCuda::print_enabled_ints() {
|
||||
const char* via_int_src[] = {"CA2", "CA1", "SR", "CB2", "CB1", "T2", "T1"};
|
||||
|
||||
|
||||
@@ -274,6 +274,8 @@ private:
|
||||
void schedule_sr_int(uint64_t timeout_ns);
|
||||
uint16_t calc_counter_val(const uint16_t last_val, const uint64_t &start_time);
|
||||
void activate_t1(uint64_t start_time);
|
||||
void activate_t2(uint64_t start_time);
|
||||
void check_t2();
|
||||
|
||||
// CUDA methods
|
||||
void cuda_init();
|
||||
|
||||
Reference in new issue
Block a user