Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions doc/cfgcpuReadme.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,50 @@ An `ExecutionContext` tracks graph state within a single flow of control:
- **Variant merging** minimizes SelectorNode creation. Most self-modifying code only touches operands (non-final fields), not opcodes.
- **Null wildcards in signatures** give a uniform matching mechanism that works for both merged instructions and SelectorNode dispatch.
- **Observer-based replacement** (`InstructionReplacerRegistry`) keeps caches and graph consistent when nodes are merged, without coupling components.
- **CfgBlock as a structural overlay** - blocks group straight-line instruction sequences without duplicating edge state. Successors/predecessors delegate by reference to the terminator/entry, so they stay in sync with the instruction-level graph automatically.
- **O(1) block liveness** via a maintained counter rather than iterating all contained instructions on every check.
- **Block boundary from instruction properties** - the linker determines boundaries exclusively from `IsBlockTerminator` and `IsBlockStarter` flags (set at parse time) plus static memory adjacency, never from `Kind` or `MaxSuccessorsCount` directly.
- **Monotonic discovery** - `IsDiscoveryComplete` flips from false to true exactly once and never back, simplifying reasoning about block state.

## CfgBlock

A `CfgBlock` groups a contiguous sequence of instructions that always execute together: one entry, one exit, no intermediate join points. It's a structural overlay on the instruction-level CFG - the instruction-level edges remain the single source of truth.

### Structure

- `Entry` - first instruction in the block.
- `Terminator` - last instruction (may be a `CfgInstruction` or a `SelectorNode`).
- `Instructions` - ordered list from entry through terminator inclusive.
- `IsDiscoveryComplete` - true once the linker has finalised the block.
- `IsLive` - true if every contained instruction is live (O(1) via maintained counter).

### Edge Delegation

Block-level `Successors` returns `Terminator.Successors` by reference. Block-level `Predecessors` returns `Entry.Predecessors` by reference. No separate block-edge state is stored - this eliminates sync bugs.

### Block Construction (NodeLinker)

The linker builds blocks incrementally as instruction-level edges are added:

1. **Bootstrap** - first edge from a node opens a one-node block for it.
2. **Continuation** - if the predecessor is not a terminator, the next node is not a starter, and they're memory-adjacent, the next node is appended to the predecessor's block.
3. **Boundary** - otherwise, the predecessor's block is closed and the next node gets its own block (new or split from an existing one).
4. **Split** - when a new edge targets the interior of an existing block, the block is split at that point.

### Self-Modifying Code and Blocks

When `ReplaceInstruction` fires (variant merging), the replacement happens in-place within the block (`ReplaceInPlace`). The new instruction inherits the block position and back-pointer. Subsequent edge rewires hit the intra-block idempotency check and don't cause spurious splits.

When a `SelectorNode` is injected via `CreateSelectorNodeBetween`, it either absorbed into the predecessor's block as its terminator (if the continuation rule applies) or lands in its own one-node block (if the predecessor is already a terminator).

### Hot-Path Execution

`CfgCpu.ExecuteNext` dispatches two ways depending on the resolved next node:

- **Hot path** – if the next node is the entry of a discovered, live block (`next == block.Entry`), `ExecuteBlock` walks the block's instruction list directly without re-entering the feeder between steps. This skips feeder lookup, linker calls, and memory reconciliation for every non-terminator instruction in the block.
- **Cold path** – in every other case (incomplete block, non-live block, or next node is an interior node of a complete live block), `ExecuteOneNode` steps exactly that one node.

The interior-node cold-step case is expected and valid. It arises during incremental discovery, selector insertion, instruction replacement, fault edges, and other graph surgery – none of which should be treated as an error. External interrupts fire once per dispatch, after the last node actually executed (block or single node).

## Core Classes

Expand Down
121 changes: 106 additions & 15 deletions src/Spice86.Core/Emulator/CPU/CfgCpu/CfgCpu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu;
using Spice86.Core.Emulator.VM.Breakpoint;
using Spice86.Shared.Emulator.Memory;
using Spice86.Shared.Interfaces;
using Spice86.Shared.Utils;

public class CfgCpu : IFunctionHandlerProvider, IClearable {
private readonly ILoggerService _loggerService;
Expand All @@ -26,15 +27,20 @@ public class CfgCpu : IFunctionHandlerProvider, IClearable {
private readonly ExecutionContextManager _executionContextManager;
private readonly InstructionReplacerRegistry _replacerRegistry = new();
private readonly CpuHeavyLogger? _cpuHeavyLogger;
private readonly EmulatorBreakpointsManager _emulatorBreakpointsManager;
private readonly IPauseHandler _pauseHandler;

public CfgCpu(IMemory memory, State state, IOPortDispatcher ioPortDispatcher, CallbackHandler callbackHandler,
DualPic dualPic, EmulatorBreakpointsManager emulatorBreakpointsManager,
IPauseHandler pauseHandler,
FunctionCatalogue functionCatalogue,
bool useCodeOverride, bool failOnInvalidOpcode, bool allowIvtAddress0, ILoggerService loggerService, CfgNodeExecutionCompiler executionCompiler, CpuHeavyLogger? cpuHeavyLogger = null) {
_loggerService = loggerService;
_state = state;
_dualPic = dualPic;
_cpuHeavyLogger = cpuHeavyLogger;
_emulatorBreakpointsManager = emulatorBreakpointsManager;
_pauseHandler = pauseHandler;

CfgNodeFeeder = new(memory, state, emulatorBreakpointsManager, _replacerRegistry, executionCompiler);
_executionContextManager = new(memory, state, CfgNodeFeeder, _replacerRegistry, functionCatalogue, useCodeOverride, loggerService, cpuHeavyLogger);
Expand All @@ -56,35 +62,120 @@ public CfgCpu(IMemory memory, State state, IOPortDispatcher ioPortDispatcher, Ca
public FunctionHandler FunctionHandlerInUse => ExecutionContextManager.CurrentExecutionContext.FunctionHandler;
public bool IsInitialExecutionContext => ExecutionContextManager.CurrentExecutionContext.Depth == 0;
private ExecutionContext CurrentExecutionContext => _executionContextManager.CurrentExecutionContext;

public ICfgNode ToExecute() {
return CfgNodeFeeder.GetLinkedCfgNodeToExecute(CurrentExecutionContext);
}

/// <inheritdoc />
/// <summary>
/// Returns the block to execute on the hot path if the node is the entry point of a discovered,
/// live block, or null if the cold path should be taken.
/// Entering a live block via something else than the entry point happens just after block discovery is completed.
/// In this case, next is the terminator, appended to block, Block became complete but cold path needs to be done one last time.
/// </summary>
private CfgBlock? HotPathBlock(ICfgNode next) {
if (next.ContainingBlock is { IsDiscoveryComplete: true, IsLive: true } block
&& next.Id == block.Entry.Id) {
return block;
}
return null;
}

/// <summary>
/// Resolves the next node via the cold-path entry edge, then dispatches hot or cold based
/// on whether the node belongs to a discovered, live block. Hot path runs the block walker;
/// cold path steps a single node. <see cref="HandleExternalInterrupt"/> fires exactly once
/// at the boundary, on the last node actually executed.
/// </summary>
public void ExecuteNext() {
ICfgNode toExecute = ToExecute();
ICfgNode next = ToExecute();
ICfgNode lastExecuted;

CfgBlock? hotBlock = HotPathBlock(next);
if (hotBlock is not null) {
lastExecuted = ExecuteBlock(hotBlock);
} else {
ExecuteOneNode(next);
lastExecuted = next;
}

// After execution, advance ExecutingNode to the next node so that a loop-level
// pause (EmulationLoop.WaitIfPaused) shows the node about to execute rather than
// the one that just finished.
ICfgNode? nextToExecute = CurrentExecutionContext.NodeToExecuteNextAccordingToGraph;
if (nextToExecute is not null) {
_executionContextManager.ExecutingNode = nextToExecute;
}

HandleExternalInterrupt(lastExecuted);
}

// Execute the node
/// <summary>
/// Executes a single CFG node: updates CS:IP logging, runs the compiled execution,
/// handles any <see cref="CpuException"/> via the instruction execution helper, increments
/// cycles, and records last-executed / next-to-execute state. Returns <c>false</c> when a
/// CpuException was observed, signalling the caller to stop stepping.
/// </summary>
internal bool ExecuteOneNode(ICfgNode node) {
_executionContextManager.ExecutingNode = node;
if (_emulatorBreakpointsManager.HasActiveBreakpoints) {
_emulatorBreakpointsManager.CheckExecutionBreakPointsAt(
MemoryUtils.ToPhysicalAddress(node.Address.Segment, node.Address.Offset));
if (_state.CS != node.Address.Segment || _state.IP != node.Address.Offset) {
CurrentExecutionContext.NodeToExecuteNextAccordingToGraph = null;
return false;
}
_pauseHandler.WaitIfPaused();
}

bool faulted = false;
try {
_loggerService.LoggerPropertyBag.CsIp = toExecute.Address;
// Log instruction before execution if CPU heavy logging is enabled
_cpuHeavyLogger?.LogInstruction(toExecute);
toExecute.CompiledExecution(_instructionExecutionHelper);
_loggerService.LoggerPropertyBag.CsIp = node.Address;
_cpuHeavyLogger?.LogInstruction(node);
node.CompiledExecution(_instructionExecutionHelper);
} catch (CpuException e) {
if(toExecute is CfgInstruction cfgInstruction) {
if (node is CfgInstruction cfgInstruction) {
_instructionExecutionHelper.HandleCpuException(cfgInstruction, e);
}
faulted = true;
}

ICfgNode? nextToExecute = toExecute.GetNextSuccessor(_instructionExecutionHelper);
ICfgNode? nextToExecute = node.GetNextSuccessor(_instructionExecutionHelper);

_state.IncCycles();

// Register what was executed and what is next node according to the graph in the execution context for next pass
CurrentExecutionContext.LastExecuted = toExecute;
CurrentExecutionContext.LastExecuted = node;
CurrentExecutionContext.NodeToExecuteNextAccordingToGraph = nextToExecute;
HandleExternalInterrupt(toExecute);

return !faulted;
}

/// <summary>
/// Hot-path block walker: iterates a discovered, live <see cref="CfgBlock"/> from
/// its entry through its terminator, calling <see cref="ExecuteOneNode"/>
/// per instruction. Exits early if a node is non-live (memory mutated), a CpuException is
/// observed, or the terminator is reached. Does not fire interrupts between steps.
/// </summary>
internal ICfgNode ExecuteBlock(CfgBlock block) {
int count = block.Instructions.Count;
ICfgNode lastExecuted = block.Entry;

for (int i = 0; i < count; i++) {
ICfgNode node = block.Instructions[i];

if (!node.IsLive) {
CurrentExecutionContext.NodeToExecuteNextAccordingToGraph = null;
return lastExecuted;
}

bool ok = ExecuteOneNode(node);
lastExecuted = node;

if (!ok) {
return lastExecuted;
}
}

return lastExecuted;
}

/// <summary>
Expand Down Expand Up @@ -141,4 +232,4 @@ public void Clear() {
CfgNodeFeeder.InstructionsFeeder.Clear();
_executionContextManager.Clear();
}
}
}
Loading
Loading