docs/DSP: Document the behavior when main and extended opcodes both write to the same register (the write backlog)

For more information, ApplyWriteBackLog, WriteToBackLog, and ZeroWriteBackLog were added in b787f5f8f7 and the explanatory comment was added in fd40513fed, although it did not mention the specific instructions that could trigger this edge case. The statements about which registers can be written by main opcodes and extension opcodes are based on my own checking of all instructions in the manual.
This commit is contained in:
Pokechu22 2022-05-22 16:42:20 -07:00
parent bb01ba60d6
commit b349254ff4
3 changed files with 31 additions and 9 deletions

View file

@ -16,8 +16,23 @@
namespace DSP::Interpreter
{
// Not needed for game ucodes (it slows down interpreter + easier to compare int VS
// dspjit64 without it)
// Correctly handle instructions such as `INC'L $ac0 : $ac0.l, @$ar0` (encoded as 0x7660) where both
// the main opcode and the extension opcode modify the same register. See the "Extended opcodes"
// section in the manual for more details. No official uCode writes to the same register twice like
// this, so we don't emulate it by default (and also don't support it in the recompiler).
//
// Dolphin only supports this behavior in the interpreter when PRECISE_BACKLOG is defined.
// In ExecuteInstruction, if an extended opcode is in use, the extended opcode's behavior is
// executed first, followed by the main opcode's behavior. The extended opcode does not directly
// write to registers, but instead records the writes into a backlog (WriteToBackLog). The main
// opcode calls ZeroWriteBackLog after it is done reading the register values; this directly
// writes zero to all registers that have pending writes in the backlog. The main opcode then is
// free to write directly to registers it changes. Afterwards, ApplyWriteBackLog bitwise-ors the
// value of the register and the value in the backlog; if the main opcode didn't write to the
// register then ZeroWriteBackLog means that the pending value is being or'd with zero, so it's
// used without changes. When PRECISE_BACKLOG is not defined, ZeroWriteBackLog does nothing and
// ApplyWriteBackLog overwrites the register value with the value from the backlog (so writes from
// extended opcodes "win" over the main opcode).
//#define PRECISE_BACKLOG
Interpreter::Interpreter(DSPCore& dsp) : m_dsp_core{dsp}
@ -809,7 +824,7 @@ void Interpreter::ConditionalExtendAccum(int reg)
void Interpreter::ApplyWriteBackLog()
{
// Always make sure to have an extra entry at the end w/ -1 to avoid
// infinitive loops
// infinite loops
for (int i = 0; m_write_back_log_idx[i] != -1; i++)
{
u16 value = m_write_back_log[i];
@ -823,6 +838,11 @@ void Interpreter::ApplyWriteBackLog()
}
}
// The ext ops are calculated in parallel with the actual op. That means that
// both the main op and the ext op see the same register state as input. The
// output is simple as long as the main and ext ops don't change the same
// register. If they do the output is the bitwise OR of the result of both the
// main and ext ops.
void Interpreter::WriteToBackLog(int i, int idx, u16 value)
{
m_write_back_log[i] = value;
@ -840,7 +860,7 @@ void Interpreter::ZeroWriteBackLog()
{
#ifdef PRECISE_BACKLOG
// always make sure to have an extra entry at the end w/ -1 to avoid
// infinitive loops
// infinite loops
for (int i = 0; m_write_back_log_idx[i] != -1; i++)
{
OpWriteRegister(m_write_back_log_idx[i], 0);

View file

@ -235,11 +235,6 @@ private:
void ConditionalExtendAccum(int reg);
// The ext ops are calculated in parallel with the actual op. That means that
// both the main op and the ext op see the same register state as input. The
// output is simple as long as the main and ext ops don't change the same
// register. If they do the output is the bitwise OR of the result of both the
// main and ext ops.
void WriteToBackLog(int i, int idx, u16 value);
void ZeroWriteBackLog();
void ZeroWriteBackLogPreserveAcc(u8 acc);

View file

@ -4339,9 +4339,16 @@ Extended opcodes do not exist on their own. These opcodes can only be attached t
Specifically, opcodes where the first nybble is 0, 1, or 2 cannot be extended.
Opcodes where the first nybble is 4 or higher can be extended, using the 8 lower bits.
Opcodes where the first nybble is 3 can also be extended, but the main opcode is 9 bits and the extension opcode is 7 bits. For these instructions, the extension opcode is treated as if the first bit were 0 (i.e. \texttt{0xxxxxxx}).
(\Opcode{NX} has no behavior of its own, so it can be used to get an extended opcode's behavior on its own.)
Extended opcodes do not modify the program counter (\Register{\$pc} register).
Extended opcodes are run \textit{in parallel} with the main opcode; they see the same register state as the input. (For instance, \texttt{\Opcode{MOVR}\Opcode{'MV} \Register{\$ac1}, \Register{\$ax0.l} : \Register{\$ax0.l}, \Register{\$ac1.m}} (encoded as \Value{0x6113}) \textit{swaps} the values of \Register{\$ac1.m} and \Register{\$ax0.l} (and also extends the new value of \Register{\$ac1.m} into \Register{\$ac1.l} and \Register{\$ac1.h}).)
Since they are executed in parallel, the main and extension opcodes could theoretically write to the same registers. All opcodes that support extension only modify a main accumulator \Register{\$acD}, as well as \Register{\$prod}, \Register{\$sr}, and/or \Register{\$pc}, while the extension opcodes themselves generally only modify an additional accumulator \Register{\$axD} and addressing registers \Register{\$arS}. The exception is \Opcode{'L} and \Opcode{'LN}, which has the option of writing to \Register{\$acD}. Thus, \texttt{\Opcode{INC}\Opcode{'L} \Register{\$ac0} : \Register{\$ac0.l}, @\Register{\$ar0}} (encoded as \Value{0x7660}) increments \Register{\$ac0} (and thus \Register{\$ac0.l}), but also sets \Register{\$ac0.l} to the value in data memory at address \Register{\$ar0} and increments \Register{\$ar0}.
When the main and extension opcodes write to the same register, the register is set to the two values bitwise-or'd together. For the above example, \Register{\$ar0.l} would be set to \InlineExpression{(\Register{\$ar0.l} + 1) | MEM[\Register{\$ar0}]}. \textbf{Note that no official uCode writes to the same register twice like this.}
\pagebreak{}
\section{Alphabetical list of extended opcodes}