Skip to content

Commit f3878c0

Browse files
fix: choice needs STI
1 parent 6efb0a5 commit f3878c0

3 files changed

Lines changed: 82 additions & 3 deletions

File tree

src/Spice86.Core/Emulator/InterruptHandlers/Common/MemoryWriter/MemoryAsmWriter.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ public void WriteNop() {
111111
WriteUInt8(0x90);
112112
}
113113

114+
/// <summary>
115+
/// Writes a STI instruction to memory. Re-enables hardware interrupts (IF=1).
116+
/// </summary>
117+
public void WriteSti() {
118+
WriteUInt8(0xFB);
119+
}
120+
114121
/// <summary>
115122
/// Writes a far CALL instruction to the given inMemoryAddressSwitcher default address. <br/>
116123
/// Throws UnrecoverableException if DefaultAddressValue is not initialized. <br/>

src/Spice86.Core/Emulator/InterruptHandlers/Dos/DosInt21Handler.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ public override SegmentedAddress WriteAssemblyInRam(MemoryAsmWriter memoryAsmWri
234234

235235
// Block sizes for jump offset calculation:
236236
// L_DEFAULT: callback(4) + IRET(1) = 5 bytes
237-
// L_WAIT_FOR_STDIN: callback(4) + CMP(6) + JNZ(2) + INT(2) + JMP(2) = 16 bytes
237+
// L_WAIT_FOR_STDIN: STI(1) + callback(4) + CMP(6) + JNZ(2) + INT(2) + JMP(2) = 17 bytes
238238
// L_DISPATCH: callback(4) + IRET(1) = 5 bytes
239239

240240
SegmentedAddress handlerAddress = memoryAsmWriter.CurrentAddress;
@@ -265,6 +265,11 @@ public override SegmentedAddress WriteAssemblyInRam(MemoryAsmWriter memoryAsmWri
265265
memoryAsmWriter.WriteIret();
266266

267267
// L_WAIT_FOR_STDIN: poll STDIN readiness in a loop with INT 28h idle
268+
// STI — re-enable hardware interrupts so IRQ1 (keyboard) can fire.
269+
// INT 21h clears IF per x86 spec; without STI the polling loop would
270+
// spin forever because keyboard scancodes never reach BiosKeyboardBuffer.
271+
// This matches FreeDOS (INT 21h starts with STI) and DOSBox (idle uses STI+HLT).
272+
memoryAsmWriter.WriteSti();
268273
// callback(CheckStdinReady) — writes flag byte, 4 bytes
269274
memoryAsmWriter.RegisterAndWriteCallback(CallbackCheckStdinReady);
270275
// CMP byte CS:[flagOffset], 0 — 6 bytes
@@ -277,8 +282,8 @@ public override SegmentedAddress WriteAssemblyInRam(MemoryAsmWriter memoryAsmWri
277282
memoryAsmWriter.WriteJnz(4);
278283
// INT 28h (DOS idle)
279284
memoryAsmWriter.WriteInt(0x28);
280-
// JMP short back to callback(CheckStdinReady): -(4+6+2+2+2) = -16
281-
memoryAsmWriter.WriteJumpShort(-16);
285+
// JMP short back to STI: -(4+6+2+2+2+1) = -17
286+
memoryAsmWriter.WriteJumpShort(-17);
282287

283288
// L_DISPATCH: dispatch to C# handler for AH=07h/08h/0Ah
284289
memoryAsmWriter.RegisterAndWriteCallback(Run);

tests/Spice86.Tests/Dos/DosInt21IntegrationTests.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,73 @@ public void CheckStandardInputStatus_WithBreakEnabled_DetectsCtrlCAndInvokesInt2
12741274
testHandler.Results.Should().NotContain((byte)TestResult.Failure);
12751275
}
12761276

1277+
// ──────────────────────────────────────────────────────────────────────
1278+
// Phase 1 — Full hardware keyboard stack → AH=08h polling loop
1279+
// Reproduces the keyboard bug: INT 21h clears IF, so hardware IRQ1
1280+
// (keyboard) can never fire while the WriteAssemblyInRam polling loop
1281+
// is running. Keys injected through the full hardware stack never reach
1282+
// the BiosKeyboardBuffer.
1283+
// ──────────────────────────────────────────────────────────────────────
1284+
1285+
/// <summary>
1286+
/// Reproduces the live-emulation keyboard bug: a key pressed DURING the
1287+
/// AH=08h WriteAssemblyInRam polling loop never reaches BiosKeyboardBuffer
1288+
/// because INT 21h clears IF and the loop never re-enables interrupts (STI).
1289+
/// Full keyboard stack: HeadlessGui → InputEventHub → PS2Keyboard
1290+
/// → Intel8042Controller → IRQ1 (blocked by IF=0) → BiosKeyboardInt9Handler
1291+
/// → BiosKeyboardBuffer → ConsoleDevice → AH=08h.
1292+
/// </summary>
1293+
[Fact]
1294+
public void AH08h_ReadsKeyFromHardwareStack_WhenKeyInjectedDuringPolling() {
1295+
// Arrange: COM calls AH=08h immediately. The key arrives while the
1296+
// WriteAssemblyInRam polling loop is running with IF=0.
1297+
byte[] program = new byte[] {
1298+
// Call AH=08h (Character Input Without Echo) — enters polling loop
1299+
0xB4, 0x08, // mov ah, 08h
1300+
0xCD, 0x21, // int 21h
1301+
1302+
// Write AL to details port for diagnostics
1303+
0xBA, 0x98, 0x09, // mov dx, DetailsPort (0x998)
1304+
0xEE, // out dx, al
1305+
1306+
// Check AL == 0x31 ('1')
1307+
0x3C, 0x31, // cmp al, 31h
1308+
0x75, 0x04, // jne failed
1309+
1310+
// Success
1311+
0xB0, 0x00, // mov al, TestResult.Success
1312+
0xEB, 0x02, // jmp writeResult
1313+
1314+
// failed:
1315+
0xB0, 0xFF, // mov al, TestResult.Failure
1316+
1317+
// writeResult:
1318+
0xBA, 0x99, 0x09, // mov dx, ResultPort
1319+
0xEE, // out dx, al
1320+
0xF4 // hlt
1321+
};
1322+
1323+
// Act — inject '1' through the full hardware keyboard stack at cycle 50.
1324+
// The key arrives while AH=08h is in its polling loop with IF=0.
1325+
DosTestHandler testHandler = RunDosTest(program, keyInjectionAction: SimulateDigit1);
1326+
1327+
// Assert
1328+
testHandler.Details.Should().NotBeEmpty("AH=08h should write AL to details port");
1329+
byte actualAl = testHandler.Details[0];
1330+
actualAl.Should().Be(0x31,
1331+
"AH=08h should read '1' (0x31) from the hardware keyboard stack during polling");
1332+
testHandler.Results.Should().Contain((byte)TestResult.Success);
1333+
testHandler.Results.Should().NotContain((byte)TestResult.Failure);
1334+
}
1335+
1336+
/// <summary>
1337+
/// Simulates pressing the '1' key through the full UI → hardware keyboard stack.
1338+
/// </summary>
1339+
private static void SimulateDigit1(HeadlessGui gui) {
1340+
gui.SimulateKeyPress(PhysicalKey.Digit1);
1341+
gui.SimulateKeyRelease(PhysicalKey.Digit1);
1342+
}
1343+
12771344
// ──────────────────────────────────────────────────────────────────────
12781345
// Phase 0 — STDIN/STDOUT handle routing tests
12791346
// These tests redirect STDIN (handle 0) to a file stream and verify that

0 commit comments

Comments
 (0)