The Main Execution Function

The Main Execution Function#

This article provides an overview of the inner workings of the main execution function in vAmiga. Specifically, it explains what happens when Moira::execute() is called. Below is the structure of the main execution function:

void
Moira::execute()
{
    ...

    // Take the fast path or the slow path
    if (!flags) {

        //
        // Fast path
        //

        reg.pc += 2;
        try {
            (this->*exec[queue.ird])(queue.ird);
        } catch (const std::exception &exc) {
            processException(exc);
        }

    } else {
    
        //
        // Slow path
        //

        ...
    }
    ...
}

The Fast Path#

The first step in execution is a check on the flags variable. Flags are typically set only in exceptional circumstances, so in most cases, the variable will be 0. This allows Moira to execute instructions on the fast path, skipping time-consuming tasks. In this path, the program counter (reg.pc) is incremented by 2, and the appropriate instruction handler is called by looking up a function pointer from the exec table. The call to the instruction handler is enclosed in a try-catch block, as exceptions may be thrown during instruction execution. For instance, if an address error occurs, the handler will throw an AddressError exception. This exception is caught, and processException handles it accordingly.

The Slow Path#

When at least one bit in flags is set, Moira enters the slow path, where it evaluates each flag individually and performs special actions as needed. Below is a detailed breakdown of the slow path code:

        if (flags & (HALTED | TRACE_EXC | TRACING)) {

            // Only continue if the CPU is not halted
            if (flags & HALTED) {
                sync(2);
                return;
            }

            // Process pending trace exception (if any)
            if (flags & TRACE_EXC) {
                execException(M68kException::TRACE);
                goto done;
            }

            // Check if the T flag is set inside the status register
            if ((flags & TRACING) && !(flags & STOPPED)) {
                flags |= TRACE_EXC;
            }
        }
        
        ... 
        
        // Execute the instruction
        reg.pc += 2;

        if (flags & LOOPING) {

            assert(loop[queue.ird]);
            (this->*loop[queue.ird])(queue.ird);

        } else {

            try {
                (this->*exec[queue.ird])(queue.ird);
            } catch (const std::exception &exc) {
                processException(exc);
            }
        }
        
     done:

        // Check if a breakpoint has been reached
        if (flags & CHECK_BP) {

            // Don't break if the instruction won't be executed due to tracing
            if (flags & TRACE_EXC) return;

            // Check if a softstop has been reached
            if (debugger.softstopMatches(reg.pc0)) didReachSoftstop(reg.pc0);

            // Check if a breakpoint has been reached
            if (debugger.breakpointMatches(reg.pc0)) didReachBreakpoint(reg.pc0);
        }
    }
}

On the slow path, the instruction handler is called after checking various conditions related to the CPU state. The CPU_IS_LOOPING flag is specifically examined to determine if the CPU is in loop mode. If the flag is set, Moira uses a special loop-mode handler to optimize execution. Loop mode, which was introduced with the M68010, helps speed up the execution of specific loop constructs. However, it was removed in the M68020 due to the introduction of caches, making it obsolete. After returning from the instruction handler, Moira checks if any breakpoints have been hit. If any are reached, it triggers the appropriate handling procedures. In the next section, we’ll explore the details of a typical instruction handler.