// SPDX-License-Identifier: MIT // Copyright (c) 2026 Raphael Amorim const { irqRaise, IC_TIMER0, IC_TIMER1, IC_TIMER2 } = import("irq"); pub const TimerChannel = struct { counter: f32, target: u32, syncEnable: u32, syncMode: u32, irqTarget: u32, irqMax: u32, irqRepeat: u32, irqToggle: u32, irq: u32, targetReached: u32, maxReached: u32, resetTarget: u32, irqFired: u32, paused: u32, blankOnce: u32, clkSource: u32, pub fn init() Self { return Self { counter: 0.0, target: 0, syncEnable: 0, syncMode: 0, irqTarget: 0, irqMax: 0, irqRepeat: 0, irqToggle: 0, irq: 0, targetReached: 0, maxReached: 0, resetTarget: 0, irqFired: 0, paused: 0, blankOnce: 0, clkSource: 0, }; } }; pub const Timer = struct { hblank: u32, vblank: u32, channels: [3]TimerChannel, // Timer 0 dot-clock ticks per sysclk cycle = (11/7)/hdiv, refreshed from // the GPU display mode each frame (setDotclock). // Default = 320-wide (hdiv 8). dotDiv: f32, pub fn init() Self { return Self { hblank: 0, vblank: 0, channels: [ TimerChannel.init(), TimerChannel.init(), TimerChannel.init(), ], dotDiv: 11.0 / 7.0 / 8.0, }; } pub fn setDotclock(self: mut Self, dm: u32) { if ((dm & 0x40) != 0) { self.dotDiv = 11.0 / 7.0 / 7.0; return; } const hr: u32 = dm & 3; var div: f32 = 10.0; if (hr == 1) { div = 8.0; } else if (hr == 2) { div = 5.0; } else if (hr == 3) { div = 4.0; } self.dotDiv = 11.0 / 7.0 / div; } pub fn getMode(self: mut Self, idx: u32) u32 { var v: u32 = 0; v = v | ((self.channels[idx].syncEnable & 1) << 0); v = v | ((self.channels[idx].syncMode & 3) << 1); v = v | ((self.channels[idx].resetTarget & 1) << 3); v = v | ((self.channels[idx].irqTarget & 1) << 4); v = v | ((self.channels[idx].irqMax & 1) << 5); v = v | ((self.channels[idx].irqRepeat & 1) << 6); v = v | ((self.channels[idx].irqToggle & 1) << 7); v = v | ((self.channels[idx].clkSource & 3) << 8); v = v | ((self.channels[idx].irq & 1) << 10); v = v | ((self.channels[idx].targetReached & 1) << 11); v = v | ((self.channels[idx].maxReached & 1) << 12); self.channels[idx].targetReached = 0; self.channels[idx].maxReached = 0; return v; } pub fn setMode(self: mut Self, idx: u32, val: u32) { self.channels[idx].syncEnable = (val >> 0) & 1; self.channels[idx].syncMode = (val >> 1) & 3; self.channels[idx].resetTarget = (val >> 3) & 1; self.channels[idx].irqTarget = (val >> 4) & 1; self.channels[idx].irqMax = (val >> 5) & 1; self.channels[idx].irqRepeat = (val >> 6) & 1; self.channels[idx].irqToggle = (val >> 7) & 1; self.channels[idx].clkSource = (val >> 8) & 3; self.channels[idx].targetReached = 0; self.channels[idx].maxReached = 0; self.channels[idx].irq = 1; self.channels[idx].irqFired = 0; self.channels[idx].counter = 0.0; self.channels[idx].blankOnce = 0; self.channels[idx].paused = 0; const syncMode: u32 = self.channels[idx].syncMode; const syncEn: u32 = self.channels[idx].syncEnable; if (idx == 0) { if (syncMode != 1 && syncMode != 2 && syncEn != 0) { var p: u32 = self.hblank; if (syncMode == 3) { p = 1; } self.channels[idx].paused = p; } } if (idx == 1) { if (syncMode != 1 && syncMode != 2 && syncEn != 0) { var p: u32 = self.vblank; if (syncMode == 3) { p = 1; } self.channels[idx].paused = p; } } if (idx == 2) { if (syncEn != 0) { if (syncMode == 0 || syncMode == 3) { self.channels[idx].paused = 1; } } } } pub fn handleIrq(self: mut Self, idx: u32, ic: *mut[] u32, cop0: *mut[] u32) { const counter: f32 = self.channels[idx].counter; const target: u32 = self.channels[idx].target; var fireIrq: u32 = 0; if (counter > (target as f32)) { self.channels[idx].targetReached = 1; if (self.channels[idx].resetTarget != 0) { self.channels[idx].counter = 0.0; } if (self.channels[idx].irqTarget != 0) { fireIrq = 1; } } if (counter > 65535.0) { self.channels[idx].counter = 0.0; self.channels[idx].maxReached = 1; if (self.channels[idx].irqMax != 0) { fireIrq = 1; } } if (fireIrq == 0) { return; } if (self.channels[idx].irqToggle == 0) { self.channels[idx].irq = 0; } else { self.channels[idx].irq = self.channels[idx].irq ^ 1; } const trigger: bool = self.channels[idx].irq == 0; if (self.channels[idx].irqRepeat == 0) { if (trigger && self.channels[idx].irqFired == 0) { self.channels[idx].irqFired = 1; } else { return; } } self.channels[idx].irq = 1; if (trigger) { var line: u32 = IC_TIMER0; match (idx) { 1 { line = IC_TIMER1; } 2 { line = IC_TIMER2; } _ {} } irqRaise(ic, cop0, line); } } // +0 counter (16-bit) // +4 mode (16-bit, read also clears targetReached / maxReached) // +8 target (16-bit) pub fn read32(self: mut Self, off: u32) u32 { const idx: u32 = off >> 4; const reg: u32 = off & 0xF; if (idx >= 3) { return 0; } match (reg) { 0 { return self.channels[idx].counter as u32; } 4 { return self.getMode(idx); } 8 { return self.channels[idx].target; } _ { return 0; } } } pub fn read16(self: mut Self, off: u32) u32 { return self.read32(off) & 0xFFFF; } pub fn read8(self: mut Self, off: u32) u32 { return self.read32(off) & 0xFF; } pub fn write32(self: mut Self, ic: *mut[] u32, cop0: *mut[] u32, off: u32, val: u32) { const idx: u32 = off >> 4; const reg: u32 = off & 0xF; if (idx >= 3) { return; } match (reg) { 0 { self.channels[idx].counter = (val & 0xFFFF) as f32; } 4 { self.setMode(idx, val); } 8 { self.channels[idx].target = val & 0xFFFF; } _ {} } self.handleIrq(idx, ic, cop0); } pub fn write16(self: mut Self, ic: *mut[] u32, cop0: *mut[] u32, off: u32, val: u32) { self.write32(ic, cop0, off, val & 0xFFFF); } pub fn write8(self: mut Self, ic: *mut[] u32, cop0: *mut[] u32, off: u32, val: u32) { self.write32(ic, cop0, off, val & 0xFF); } pub fn update(self: mut Self, ic: *mut[] u32, cop0: *mut[] u32) { self.updateCyc(ic, cop0, 2); } pub fn updateCyc(self: mut Self, ic: *mut[] u32, cop0: *mut[] u32, cyc: u32) { self.updateOne(0, 2, ic, cop0); self.updateOne(1, 2, ic, cop0); self.updateOne(2, 2, ic, cop0); } pub fn updateOne(self: mut Self, idx: u32, cyc: u32, ic: *mut[] u32, cop0: *mut[] u32) { if (self.channels[idx].paused != 0) { return; } // Timer 1 sourced from hblank is incremented in hblankBegin. if (idx == 1 && (self.channels[idx].clkSource & 1) != 0) { return; } const cf: f32 = cyc as f32; if (idx == 2 && self.channels[idx].clkSource >= 2) { self.channels[idx].counter = self.channels[idx].counter + cf / 8.0; } else if (idx == 0 && (self.channels[idx].clkSource & 1) != 0) { self.channels[idx].counter = self.channels[idx].counter + cf * self.dotDiv; } else { self.channels[idx].counter = self.channels[idx].counter + cf; } self.handleIrq(idx, ic, cop0); } pub fn hblankBegin(self: mut Self, ic: *mut[] u32, cop0: *mut[] u32) { self.hblank = 1; if ((self.channels[1].clkSource & 1) != 0 && self.channels[1].paused == 0) { self.channels[1].counter = self.channels[1].counter + 1.0; self.handleIrq(1, ic, cop0); } if (self.channels[0].syncEnable == 0) { return; } match (self.channels[0].syncMode) { 0 { self.channels[0].paused = 1; } 1 { self.channels[0].counter = 0.0; } 2 { self.channels[0].counter = 0.0; self.channels[0].paused = 0; } 3 { if (self.channels[0].blankOnce == 0) { self.channels[0].blankOnce = 1; self.channels[0].syncEnable = 0; self.channels[0].paused = 0; } } _ {} } } pub fn hblankEnd(self: mut Self) { self.hblank = 0; if (self.channels[0].syncEnable == 0) { return; } match (self.channels[0].syncMode) { 0 { self.channels[0].paused = 0; } 2 { self.channels[0].paused = 1; } _ {} } } pub fn vblankBegin(self: mut Self, ic: *mut[] u32, cop0: *mut[] u32) { self.vblank = 1; if (self.channels[1].syncEnable == 0) { return; } match (self.channels[1].syncMode) { 0 { self.channels[1].paused = 1; } 1 { self.channels[1].counter = 0.0; } 2 { self.channels[1].counter = 0.0; self.channels[1].paused = 0; } 3 { if (self.channels[1].blankOnce == 0) { self.channels[1].blankOnce = 1; self.channels[1].syncEnable = 0; self.channels[1].paused = 0; } } _ {} } } pub fn vblankEnd(self: mut Self) { self.vblank = 0; if (self.channels[1].syncEnable == 0) { return; } match (self.channels[1].syncMode) { 0 { self.channels[1].paused = 0; } 2 { self.channels[1].paused = 1; } _ {} } } };