pub struct Cop0 { /* private fields */ }Expand description
The VR4300 system control coprocessor register file.
Implementations§
Source§impl Cop0
impl Cop0
Sourcepub const UNUSED: [u8; 7]
pub const UNUSED: [u8; 7]
The COP0 register numbers that are “Reserved for future use” (UM Table 1-2, p. 46) and behave as a single shared write latch rather than as storage.
Writing one goes nowhere. Reading one returns the value of the most
recent MTC0/DMTC0 to any COP0 register — so a write-then-read of
register 7 returns what was written, and the same sequence with any
other COP0 write in between returns that value instead.
Measured, replacing a guess. The manual says nothing about these, so this implementation previously discarded writes and read zero, recorded as accuracy-ledger U-1. n64-systemtest documents and exercises the real behavior, sweeping five written values against three interposed ones specifically so an emulator cannot pass by echoing the first.
Sourcepub const fn new() -> Self
pub const fn new() -> Self
Cold-reset state (UM §6.4.4, p. 183; Fig. 6-16, p. 205).
Defined by the manual: Status.ERL and Status.BEV set, Status.TS/
SR/RP clear, Config.BE set, Config.EP clear, Random = 31,
Wired = 0. Everything else the manual calls undefined, and is a
deterministic zero here (ADR 0004).
Sourcepub const fn set_now(&mut self, now: u64)
pub const fn set_now(&mut self, now: u64)
Advance the Count timeline to now (the scheduler’s count_ticks).
Called once per CPU step. Note this sets rather than increments: the position is derived, so a dropped or repeated call cannot desynchronize it from the master clock the way an increment would.
Sourcepub const fn timer_matches(&self) -> bool
pub const fn timer_matches(&self) -> bool
Has the timer fired — Count == Compare?
UM §6.3.4 (p. 165). The comparison is on the computed Count, so it
stays true regardless of how the timeline was reached.
Sourcepub const fn timer_edge(&mut self) -> bool
pub const fn timer_edge(&mut self) -> bool
Has the timer just reached Compare — the rising edge of the match?
§Why the edge and not the equality
Both Count and Compare reset to zero, so a plain
Cop0::timer_matches is true on the very first step and latches IP7
before a single instruction retires. n64-systemtest catches this exactly:
it reads Cause during an AdEL exception and expects 0x10, while a
spuriously-latched IP7 (bit 15) makes it 0x8010.
The timer fires when Count becomes equal to Compare, which is once
per wrap of the 32-bit counter, not continuously while they happen to be
equal. Tracking the previous value is what distinguishes the two, and at
power-on there is no transition into equality — the two simply start
there.
§Why this asks whether Compare was crossed, not whether it is hit
Count is derived from the master clock, so it keeps advancing while the
pipeline is stalled — this poll is not guaranteed to land on every value
the counter takes. An instantaneous now == Compare test therefore misses
the match whenever the gap between two polls steps over it, and a
69-PCycle multiply interlock (UM Table 3-12) steps over it comfortably.
The failure that produces is not a late interrupt but a lost one:
IP7 never latches, and software waiting on the timer hangs forever.
So the question is whether Compare lies in the half-open interval
(last_count, now], in wrapping arithmetic so a counter wrap is just
another interval. Excluding last_count itself preserves the edge
semantics above: sitting on Compare is not a transition into it.
Sourcepub const fn set_ip(&mut self, bit: u8, on: bool)
pub const fn set_ip(&mut self, bit: u8, on: bool)
Set or clear a Cause.IP bit.
bit is 0..=7. IP1:IP0 are software interrupts and are written through
MTC0 instead; this is the hardware path for IP2 (RCP) and IP7
(timer).
Sourcepub const fn interrupt_pending(&self) -> bool
pub const fn interrupt_pending(&self) -> bool
Is an interrupt currently recognized?
All four conditions, and each one matters (UM §6.1 p. 160, §6.3.5 p. 168, Fig. 14-4 p. 357):
Status.IE— the global enable.Status.EXLclear — a handler is not interrupted by the thing it is handling. This is whyEXLimplies interrupts-off withoutIEbeing touched.Status.ERLclear — likewise for the error path.Cause.IP & Status.IM— at least one pending and unmasked.
Dropping the EXL/ERL terms is the classic version of this bug: it
works until the first interrupt arrives inside a handler, and then
re-enters it forever.
Sourcepub const fn read(&self, n: u8) -> u64
pub const fn read(&self, n: u8) -> u64
Read a register’s architectural value.
ARCH_MASK is applied here as well as on the way in, so a 32-bit
register can never return non-zero upper bits and EntryHi.Fill always
reads zero — regardless of how the stored value arrived. That matters
because Cop0::set_hardware exists precisely to bypass the write
masks, and exception dispatch will feed it raw faulting addresses.
Sourcepub const fn is_unused(n: u8) -> bool
pub const fn is_unused(n: u8) -> bool
Is this one of the Cop0::UNUSED register numbers?
Sourcepub const fn write(&mut self, n: u8, value: u64)
pub const fn write(&mut self, n: u8, value: u64)
Write a register, applying its writable-bit mask.
Bits outside WRITE_MASK keep their previous value, which is what
hardware does and is not the same as writing zero to them.
Registers 7, 21..=25 and 31 are “Reserved for future use” (UM Table 1-2,
p. 46) and are not storage — see Cop0::UNUSED. Writes to them go
nowhere; reads return the shared write latch.
Sourcepub const fn set_hardware(&mut self, n: u8, value: u64)
pub const fn set_hardware(&mut self, n: u8, value: u64)
Force a value past the writable-bit mask, for hardware-owned fields.
Exception dispatch writes Cause.ExcCode, EPC and BadVAddr, all of
which are read-only or partly read-only to software. Routing those
through Cop0::write would require widening the masks, which would
also let MTC0 write them — the exact bug the masks exist to prevent.
Sourcepub const fn mfc0(&self, n: u8) -> u64
pub const fn mfc0(&self, n: u8) -> u64
MFC0 rt, rd — read the low 32 bits, sign-extended into the 64-bit GPR.
Sign-extension applies even to a register that is architecturally 64 bits
wide: MFC0 is defined as a 32-bit move, so MFC0 of an EPC whose bit
31 is set yields a sign-extended value, not a truncated one.
Sourcepub const fn dmfc0(&self, n: u8) -> u64
pub const fn dmfc0(&self, n: u8) -> u64
DMFC0 rt, rd — read the full 64 bits.
On a 32-bit-wide register this is the same as Cop0::mfc0 except for
sign-extension: the upper half is zero rather than a copy of bit 31.
Sourcepub const fn mtc0(&mut self, n: u8, value: u64)
pub const fn mtc0(&mut self, n: u8, value: u64)
MTC0 rd, rt — write the low 32 bits.
For a 64-bit register the upper half is cleared, not preserved: the value written is the sign-extended 32-bit operand, which is how software legitimately writes a KSEG0 address into a 64-bit register.
Sourcepub const fn tick_random(&mut self)
pub const fn tick_random(&mut self)
Advance Random by one instruction (UM §5.4.2, p. 147).
“decrements as each instruction executes”, reloading 31 when it reaches
the Wired floor.
§It is a plain 6-bit down-counter, and that matters when Wired > 31
The reload fires on cur == wired, not cur <= wired, and the
decrement wraps 0 → 63. For the ordinary case (Wired <= 31) the two
readings agree: the counter walks 31 down to Wired either way.
They diverge once Wired exceeds 31, which software can arrange because
Wired is six bits wide. Under <= the counter is immediately at or
below the floor and pins at 31 forever; under == it walks 31 → 0 → 63 →
Wired, covering the whole range. n64-systemtest checks exactly that,
and sampling a pinned register cannot be distinguished from sampling a
slow one without it.