Skip to content

Commit cdae590

Browse files
committed
feat: CFG Blocks baked into execution engine
1 parent e2863a8 commit cdae590

145 files changed

Lines changed: 17938 additions & 606 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

doc/cfgcpuReadme.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,50 @@ An `ExecutionContext` tracks graph state within a single flow of control:
314314
- **Variant merging** minimizes SelectorNode creation. Most self-modifying code only touches operands (non-final fields), not opcodes.
315315
- **Null wildcards in signatures** give a uniform matching mechanism that works for both merged instructions and SelectorNode dispatch.
316316
- **Observer-based replacement** (`InstructionReplacerRegistry`) keeps caches and graph consistent when nodes are merged, without coupling components.
317+
- **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.
318+
- **O(1) block liveness** via a maintained counter rather than iterating all contained instructions on every check.
319+
- **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.
320+
- **Monotonic discovery** - `IsDiscoveryComplete` flips from false to true exactly once and never back, simplifying reasoning about block state.
321+
322+
## CfgBlock
323+
324+
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.
325+
326+
### Structure
327+
328+
- `Entry` - first instruction in the block.
329+
- `Terminator` - last instruction (may be a `CfgInstruction` or a `SelectorNode`).
330+
- `Instructions` - ordered list from entry through terminator inclusive.
331+
- `IsDiscoveryComplete` - true once the linker has finalised the block.
332+
- `IsLive` - true if every contained instruction is live (O(1) via maintained counter).
333+
334+
### Edge Delegation
335+
336+
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.
337+
338+
### Block Construction (NodeLinker)
339+
340+
The linker builds blocks incrementally as instruction-level edges are added:
341+
342+
1. **Bootstrap** - first edge from a node opens a one-node block for it.
343+
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.
344+
3. **Boundary** - otherwise, the predecessor's block is closed and the next node gets its own block (new or split from an existing one).
345+
4. **Split** - when a new edge targets the interior of an existing block, the block is split at that point.
346+
347+
### Self-Modifying Code and Blocks
348+
349+
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.
350+
351+
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).
352+
353+
### Hot-Path Execution
354+
355+
`CfgCpu.ExecuteNext` dispatches two ways depending on the resolved next node:
356+
357+
- **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.
358+
- **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.
359+
360+
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).
317361

318362
## Core Classes
319363

src/Spice86.Core/Emulator/CPU/CfgCpu/CfgCpu.cs

Lines changed: 106 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace Spice86.Core.Emulator.CPU.CfgCpu;
1717
using Spice86.Core.Emulator.VM.Breakpoint;
1818
using Spice86.Shared.Emulator.Memory;
1919
using Spice86.Shared.Interfaces;
20+
using Spice86.Shared.Utils;
2021

2122
public class CfgCpu : IFunctionHandlerProvider, IClearable {
2223
private readonly ILoggerService _loggerService;
@@ -26,15 +27,20 @@ public class CfgCpu : IFunctionHandlerProvider, IClearable {
2627
private readonly ExecutionContextManager _executionContextManager;
2728
private readonly InstructionReplacerRegistry _replacerRegistry = new();
2829
private readonly CpuHeavyLogger? _cpuHeavyLogger;
30+
private readonly EmulatorBreakpointsManager _emulatorBreakpointsManager;
31+
private readonly IPauseHandler _pauseHandler;
2932

3033
public CfgCpu(IMemory memory, State state, IOPortDispatcher ioPortDispatcher, CallbackHandler callbackHandler,
3134
DualPic dualPic, EmulatorBreakpointsManager emulatorBreakpointsManager,
35+
IPauseHandler pauseHandler,
3236
FunctionCatalogue functionCatalogue,
3337
bool useCodeOverride, bool failOnInvalidOpcode, bool allowIvtAddress0, ILoggerService loggerService, CfgNodeExecutionCompiler executionCompiler, CpuHeavyLogger? cpuHeavyLogger = null) {
3438
_loggerService = loggerService;
3539
_state = state;
3640
_dualPic = dualPic;
3741
_cpuHeavyLogger = cpuHeavyLogger;
42+
_emulatorBreakpointsManager = emulatorBreakpointsManager;
43+
_pauseHandler = pauseHandler;
3844

3945
CfgNodeFeeder = new(memory, state, emulatorBreakpointsManager, _replacerRegistry, executionCompiler);
4046
_executionContextManager = new(memory, state, CfgNodeFeeder, _replacerRegistry, functionCatalogue, useCodeOverride, loggerService, cpuHeavyLogger);
@@ -56,35 +62,120 @@ public CfgCpu(IMemory memory, State state, IOPortDispatcher ioPortDispatcher, Ca
5662
public FunctionHandler FunctionHandlerInUse => ExecutionContextManager.CurrentExecutionContext.FunctionHandler;
5763
public bool IsInitialExecutionContext => ExecutionContextManager.CurrentExecutionContext.Depth == 0;
5864
private ExecutionContext CurrentExecutionContext => _executionContextManager.CurrentExecutionContext;
59-
6065
public ICfgNode ToExecute() {
6166
return CfgNodeFeeder.GetLinkedCfgNodeToExecute(CurrentExecutionContext);
6267
}
6368

64-
/// <inheritdoc />
69+
/// <summary>
70+
/// Returns the block to execute on the hot path if the node is the entry point of a discovered,
71+
/// live block, or null if the cold path should be taken.
72+
/// Entering a live block via something else than the entry point happens just after block discovery is completed.
73+
/// In this case, next is the terminator, appended to block, Block became complete but cold path needs to be done one last time.
74+
/// </summary>
75+
private CfgBlock? HotPathBlock(ICfgNode next) {
76+
if (next.ContainingBlock is { IsDiscoveryComplete: true, IsLive: true } block
77+
&& next.Id == block.Entry.Id) {
78+
return block;
79+
}
80+
return null;
81+
}
82+
83+
/// <summary>
84+
/// Resolves the next node via the cold-path entry edge, then dispatches hot or cold based
85+
/// on whether the node belongs to a discovered, live block. Hot path runs the block walker;
86+
/// cold path steps a single node. <see cref="HandleExternalInterrupt"/> fires exactly once
87+
/// at the boundary, on the last node actually executed.
88+
/// </summary>
6589
public void ExecuteNext() {
66-
ICfgNode toExecute = ToExecute();
90+
ICfgNode next = ToExecute();
91+
ICfgNode lastExecuted;
92+
93+
CfgBlock? hotBlock = HotPathBlock(next);
94+
if (hotBlock is not null) {
95+
lastExecuted = ExecuteBlock(hotBlock);
96+
} else {
97+
ExecuteOneNode(next);
98+
lastExecuted = next;
99+
}
100+
101+
// After execution, advance ExecutingNode to the next node so that a loop-level
102+
// pause (EmulationLoop.WaitIfPaused) shows the node about to execute rather than
103+
// the one that just finished.
104+
ICfgNode? nextToExecute = CurrentExecutionContext.NodeToExecuteNextAccordingToGraph;
105+
if (nextToExecute is not null) {
106+
_executionContextManager.ExecutingNode = nextToExecute;
107+
}
108+
109+
HandleExternalInterrupt(lastExecuted);
110+
}
67111

68-
// Execute the node
112+
/// <summary>
113+
/// Executes a single CFG node: updates CS:IP logging, runs the compiled execution,
114+
/// handles any <see cref="CpuException"/> via the instruction execution helper, increments
115+
/// cycles, and records last-executed / next-to-execute state. Returns <c>false</c> when a
116+
/// CpuException was observed, signalling the caller to stop stepping.
117+
/// </summary>
118+
internal bool ExecuteOneNode(ICfgNode node) {
119+
_executionContextManager.ExecutingNode = node;
120+
if (_emulatorBreakpointsManager.HasActiveBreakpoints) {
121+
_emulatorBreakpointsManager.CheckExecutionBreakPointsAt(
122+
MemoryUtils.ToPhysicalAddress(node.Address.Segment, node.Address.Offset));
123+
if (_state.CS != node.Address.Segment || _state.IP != node.Address.Offset) {
124+
CurrentExecutionContext.NodeToExecuteNextAccordingToGraph = null;
125+
return false;
126+
}
127+
_pauseHandler.WaitIfPaused();
128+
}
129+
130+
bool faulted = false;
69131
try {
70-
_loggerService.LoggerPropertyBag.CsIp = toExecute.Address;
71-
// Log instruction before execution if CPU heavy logging is enabled
72-
_cpuHeavyLogger?.LogInstruction(toExecute);
73-
toExecute.CompiledExecution(_instructionExecutionHelper);
132+
_loggerService.LoggerPropertyBag.CsIp = node.Address;
133+
_cpuHeavyLogger?.LogInstruction(node);
134+
node.CompiledExecution(_instructionExecutionHelper);
74135
} catch (CpuException e) {
75-
if(toExecute is CfgInstruction cfgInstruction) {
136+
if (node is CfgInstruction cfgInstruction) {
76137
_instructionExecutionHelper.HandleCpuException(cfgInstruction, e);
77138
}
139+
faulted = true;
78140
}
79141

80-
ICfgNode? nextToExecute = toExecute.GetNextSuccessor(_instructionExecutionHelper);
81-
142+
ICfgNode? nextToExecute = node.GetNextSuccessor(_instructionExecutionHelper);
143+
82144
_state.IncCycles();
83145

84-
// Register what was executed and what is next node according to the graph in the execution context for next pass
85-
CurrentExecutionContext.LastExecuted = toExecute;
146+
CurrentExecutionContext.LastExecuted = node;
86147
CurrentExecutionContext.NodeToExecuteNextAccordingToGraph = nextToExecute;
87-
HandleExternalInterrupt(toExecute);
148+
149+
return !faulted;
150+
}
151+
152+
/// <summary>
153+
/// Hot-path block walker: iterates a discovered, live <see cref="CfgBlock"/> from
154+
/// its entry through its terminator, calling <see cref="ExecuteOneNode"/>
155+
/// per instruction. Exits early if a node is non-live (memory mutated), a CpuException is
156+
/// observed, or the terminator is reached. Does not fire interrupts between steps.
157+
/// </summary>
158+
internal ICfgNode ExecuteBlock(CfgBlock block) {
159+
int count = block.Instructions.Count;
160+
ICfgNode lastExecuted = block.Entry;
161+
162+
for (int i = 0; i < count; i++) {
163+
ICfgNode node = block.Instructions[i];
164+
165+
if (!node.IsLive) {
166+
CurrentExecutionContext.NodeToExecuteNextAccordingToGraph = null;
167+
return lastExecuted;
168+
}
169+
170+
bool ok = ExecuteOneNode(node);
171+
lastExecuted = node;
172+
173+
if (!ok) {
174+
return lastExecuted;
175+
}
176+
}
177+
178+
return lastExecuted;
88179
}
89180

90181
/// <summary>
@@ -141,4 +232,4 @@ public void Clear() {
141232
CfgNodeFeeder.InstructionsFeeder.Clear();
142233
_executionContextManager.Clear();
143234
}
144-
}
235+
}

0 commit comments

Comments
 (0)