Skip to content

Commit 21aff40

Browse files
committed
feat: CFG Blocks baked into execution engine
1 parent 424217c commit 21aff40

79 files changed

Lines changed: 15255 additions & 398 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: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,45 @@ 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+
When the next node belongs to a discovered, live block, `CfgCpu.ExecuteBlock` walks the block's instruction list directly without re-resolving through the feeder between steps. This skips the cold-path overhead (feeder lookup, linker call, memory reconciliation) for every non-terminator instruction in the block. External interrupts fire once at the block boundary, not between every instruction.
317356

318357
## Core Classes
319358

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

Lines changed: 87 additions & 16 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);
@@ -61,41 +67,106 @@ public ICfgNode ToExecute() {
6167
return CfgNodeFeeder.GetLinkedCfgNodeToExecute(CurrentExecutionContext);
6268
}
6369

64-
/// <inheritdoc />
70+
/// <summary>
71+
/// Resolves the next node via the cold-path entry edge, then dispatches hot or cold based
72+
/// on whether the node belongs to a discovered, live block. Hot path runs the block walker;
73+
/// cold path steps a single node. <see cref="HandleExternalInterrupt"/> fires exactly once
74+
/// at the boundary, on the last node actually executed.
75+
/// </summary>
6576
public void ExecuteNext() {
66-
ICfgNode toExecute = ToExecute();
77+
ICfgNode next = ToExecute();
78+
ICfgNode lastExecuted;
79+
80+
if (next.ContainingBlock is { IsDiscoveryComplete: true, IsLive: true } block) {
81+
lastExecuted = ExecuteBlock(block, next);
82+
} else {
83+
ExecuteOneNode(next);
84+
lastExecuted = next;
85+
}
86+
87+
HandleExternalInterrupt(lastExecuted);
88+
}
89+
90+
/// <summary>
91+
/// Executes a single CFG node: updates CS:IP logging, runs the compiled execution,
92+
/// handles any <see cref="CpuException"/> via the instruction execution helper, increments
93+
/// cycles, and records last-executed / next-to-execute state. Returns <c>false</c> when a
94+
/// CpuException was observed, signalling the caller to stop stepping.
95+
/// </summary>
96+
internal bool ExecuteOneNode(ICfgNode node) {
97+
if (_emulatorBreakpointsManager.HasActiveBreakpoints) {
98+
_emulatorBreakpointsManager.CheckExecutionBreakPointsAt(
99+
MemoryUtils.ToPhysicalAddress(node.Address.Segment, node.Address.Offset));
100+
if (_state.CS != node.Address.Segment || _state.IP != node.Address.Offset) {
101+
CurrentExecutionContext.NodeToExecuteNextAccordingToGraph = null;
102+
return false;
103+
}
104+
_pauseHandler.WaitIfPaused();
105+
}
67106

68-
// Execute the node
107+
bool faulted = false;
69108
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);
109+
_loggerService.LoggerPropertyBag.CsIp = node.Address;
110+
_cpuHeavyLogger?.LogInstruction(node);
111+
node.CompiledExecution(_instructionExecutionHelper);
74112
} catch (CpuException e) {
75-
if(toExecute is CfgInstruction cfgInstruction) {
113+
if (node is CfgInstruction cfgInstruction) {
76114
_instructionExecutionHelper.HandleCpuException(cfgInstruction, e);
77115
}
116+
faulted = true;
78117
}
79118

80-
ICfgNode? nextToExecute = toExecute.GetNextSuccessor(_instructionExecutionHelper);
81-
119+
ICfgNode? nextToExecute = node.GetNextSuccessor(_instructionExecutionHelper);
120+
82121
_state.IncCycles();
83122

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;
123+
CurrentExecutionContext.LastExecuted = node;
86124
CurrentExecutionContext.NodeToExecuteNextAccordingToGraph = nextToExecute;
87-
HandleExternalInterrupt(toExecute);
125+
126+
return !faulted;
127+
}
128+
129+
/// <summary>
130+
/// Hot-path block walker: iterates a discovered, live <see cref="CfgBlock"/> from
131+
/// <paramref name="startAt"/> through its terminator, calling <see cref="ExecuteOneNode"/>
132+
/// per instruction. Exits early if a node is non-live (memory mutated), a CpuException is
133+
/// observed, or the terminator is reached. Does not fire interrupts between steps.
134+
/// </summary>
135+
internal ICfgNode ExecuteBlock(CfgBlock block, ICfgNode startAt) {
136+
int startIndex = block.IndexOf(startAt);
137+
int count = block.Instructions.Count;
138+
ICfgNode lastExecuted = startAt;
139+
140+
for (int i = startIndex; i < count; i++) {
141+
ICfgNode node = block.Instructions[i];
142+
143+
if (!node.IsLive) {
144+
CurrentExecutionContext.NodeToExecuteNextAccordingToGraph = null;
145+
return lastExecuted;
146+
}
147+
148+
bool ok = ExecuteOneNode(node);
149+
lastExecuted = node;
150+
151+
if (!ok) {
152+
return lastExecuted;
153+
}
154+
155+
if (i == count - 1) {
156+
return lastExecuted;
157+
}
158+
}
159+
160+
return lastExecuted;
88161
}
89162

90163
/// <summary>
91164
/// <inheritdoc />
92165
/// </summary>
93166
public void SignalEntry() {
94167
_executionContextManager.SignalEntry();
95-
// Parse the first instruction and register it as entry point
96168
CfgNodeFeeder.GetLinkedCfgNodeToExecute(CurrentExecutionContext);
97169
_executionContextManager.CurrentExecutionContext.FunctionHandler.Call(CallType.MACHINE, _state.IpSegmentedAddress, null, null);
98-
// expected return address from machine start is never defined.
99170
_executionContextManager.SignalNewExecutionContext(_state.IpSegmentedAddress, SegmentedAddress.ZERO);
100171
}
101172

@@ -141,4 +212,4 @@ public void Clear() {
141212
CfgNodeFeeder.InstructionsFeeder.Clear();
142213
_executionContextManager.Clear();
143214
}
144-
}
215+
}

0 commit comments

Comments
 (0)