diff --git a/src/Spice86.Core/CLI/Configuration.cs b/src/Spice86.Core/CLI/Configuration.cs index 62d46f7024..620856b9c4 100644 --- a/src/Spice86.Core/CLI/Configuration.cs +++ b/src/Spice86.Core/CLI/Configuration.cs @@ -201,6 +201,13 @@ public sealed class Configuration : CommandSettings { [CommandOption("--Ems ")] public bool? Ems { get; init; } + /// + /// Determines whether DOS Upper Memory Blocks (UMBs) are available. + /// + [CommandOption("--Umb ")] + [DefaultValue(true)] + public bool Umb { get; set; } = true; + /// /// Specify the type of mouse to use. /// diff --git a/src/Spice86.Core/CLI/RuntimeOptions/DosOptions.cs b/src/Spice86.Core/CLI/RuntimeOptions/DosOptions.cs index fe015fa967..bb1a7ef358 100644 --- a/src/Spice86.Core/CLI/RuntimeOptions/DosOptions.cs +++ b/src/Spice86.Core/CLI/RuntimeOptions/DosOptions.cs @@ -8,6 +8,7 @@ namespace Spice86.Core.CLI.RuntimeOptions; /// Program load segment in emulated memory. /// Whether XMS services are enabled. /// Whether EMS services are enabled. +/// Whether DOS UMB support is enabled. /// Shared mutable DOS runtime state. public sealed record class DosOptions( string? CDrive, @@ -15,4 +16,5 @@ public sealed record class DosOptions( ushort ProgramEntryPointSegment, bool? Xms, bool? Ems, + bool Umb, DosRuntimeState DosRuntimeState); \ No newline at end of file diff --git a/src/Spice86.Core/CLI/RuntimeOptions/RuntimeOptionsMapper.cs b/src/Spice86.Core/CLI/RuntimeOptions/RuntimeOptionsMapper.cs index 3a95e7a659..6b3a621391 100644 --- a/src/Spice86.Core/CLI/RuntimeOptions/RuntimeOptionsMapper.cs +++ b/src/Spice86.Core/CLI/RuntimeOptions/RuntimeOptionsMapper.cs @@ -80,6 +80,7 @@ public static DosOptions ToDosOptions(Configuration configuration, DosRuntimeSta configuration.ProgramEntryPointSegment, configuration.Xms, configuration.Ems, + configuration.Umb, dosRuntimeState); } diff --git a/src/Spice86.Core/Emulator/InterruptHandlers/Dos/DosInt21Handler.cs b/src/Spice86.Core/Emulator/InterruptHandlers/Dos/DosInt21Handler.cs index 3613a536e6..1460ab1960 100644 --- a/src/Spice86.Core/Emulator/InterruptHandlers/Dos/DosInt21Handler.cs +++ b/src/Spice86.Core/Emulator/InterruptHandlers/Dos/DosInt21Handler.cs @@ -798,10 +798,9 @@ public void AllocateMemoryBlock(bool calledFromVm) { LogDosError(calledFromVm); // did not find something good, error SetCarryFlag(true, calledFromVm); - DosMemoryControlBlock largest = _dosMemoryManager.FindLargestFree(); // INSUFFICIENT MEMORY State.AX = (byte)DosErrorCode.InsufficientMemory; - State.BX = largest.Size; + State.BX = _dosMemoryManager.FindLargestFreeSizeForAllocation(); return; } State.AX = res.DataBlockSegment; @@ -1858,20 +1857,27 @@ public enum AllocationStrategySubFunction : byte { public void AllocationStrategyOrUpperMemoryLinkState(bool calledFromVm) { byte op = State.AL; if (op == (byte)AllocationStrategySubFunction.QueryMemoryAllocationStrategy) { - State.AX = (ushort)_dosMemoryManager.AllocationStrategy; + State.AX = _dosMemoryManager.AllocationStrategyValue; SetCarryFlag(false, calledFromVm); } else if (op == (byte)AllocationStrategySubFunction.SetMemoryAllocationStrategy) { - _dosMemoryManager.AllocationStrategy = (DosMemoryAllocationStrategy)State.BX; - State.AX = 0; - SetCarryFlag(false, calledFromVm); + if (_dosMemoryManager.TrySetAllocationStrategy(State.BX)) { + State.AX = 0; + SetCarryFlag(false, calledFromVm); + } else { + State.AX = 1; + SetCarryFlag(true, calledFromVm); + } } else if (op == (byte)AllocationStrategySubFunction.QueryUpperMemoryBlockState) { - // 01H = upper memory is currently linked - // 00H = not linked (all allocations go to conventional mem) - State.AL = 0x00; + // AL bit 0 is set when the UMB chain is linked to the conventional MCB chain. + State.AL = _dosMemoryManager.UmbChainState; SetCarryFlag(false, calledFromVm); } else if (op == (byte)AllocationStrategySubFunction.SetUpperMemoryBlockState) { - State.AX = 0x01; // 0001h (invalid function) - SetCarryFlag(true, calledFromVm); + if (_dosMemoryManager.SetUmbChainLinkState(State.BX)) { + SetCarryFlag(false, calledFromVm); + } else { + State.AX = 0x01; // 0001h (invalid function) + SetCarryFlag(true, calledFromVm); + } } else { throw GenerateUnhandledOperationException(op); } diff --git a/src/Spice86.Core/Emulator/InterruptHandlers/Dos/Xms/ExtendedMemoryManager.cs b/src/Spice86.Core/Emulator/InterruptHandlers/Dos/Xms/ExtendedMemoryManager.cs index 3e4698a0ca..8750730a30 100644 --- a/src/Spice86.Core/Emulator/InterruptHandlers/Dos/Xms/ExtendedMemoryManager.cs +++ b/src/Spice86.Core/Emulator/InterruptHandlers/Dos/Xms/ExtendedMemoryManager.cs @@ -6,6 +6,7 @@ namespace Spice86.Core.Emulator.InterruptHandlers.Dos.Xms; using Spice86.Core.Emulator.CPU; using Spice86.Core.Emulator.InterruptHandlers.Common.MemoryWriter; using Spice86.Core.Emulator.Memory; +using Spice86.Core.Emulator.OperatingSystem; using Spice86.Core.Emulator.OperatingSystem.Devices; using Spice86.Core.Emulator.OperatingSystem.Structures; using Spice86.Shared.Emulator.Memory; @@ -110,6 +111,7 @@ public sealed class ExtendedMemoryManager : IVirtualDevice { private readonly State _state; private readonly A20Gate _a20Gate; private readonly IMemory _memory; + private readonly DosMemoryManager _dosMemoryManager; /// /// Set on DOS XMS driver startup - we don't touch it if was enabled at startup. /// @@ -221,6 +223,7 @@ private class A20State { /// The CPU state for accessing registers during XMS operations. /// Helper for writing assembly code to memory. /// DOS memory tables for placing the XMS driver in memory. + /// The DOS memory manager used for UMB operations. /// The logger service for recording XMS operations. /// /// This constructor: @@ -232,7 +235,7 @@ private class A20State { /// public ExtendedMemoryManager(IMemory memory, State state, A20Gate a20Gate, MemoryAsmWriter memoryAsmWriter, DosTables dosTables, - ILogger loggerService) { + DosMemoryManager dosMemoryManager, ILogger loggerService) { uint headerAddress = MemoryUtils.ToPhysicalAddress(DosDeviceSegment, 0); Header = new DosDeviceHeader(memory, headerAddress) { @@ -243,6 +246,7 @@ public ExtendedMemoryManager(IMemory memory, State state, A20Gate a20Gate, _state = state; _a20Gate = a20Gate; _memory = memory; + _dosMemoryManager = dosMemoryManager; _loggerService = loggerService; // Place hookable callback in writable memory area var hookableCodeAddress = new SegmentedAddress((ushort)(dosTables @@ -1522,7 +1526,37 @@ private void ReallocateExtendedMemoryBlockInternal(int handle, uint newSizeInByt /// public void RequestUpperMemoryBlock() { _state.AX = 0; - _state.BL = (byte)XmsErrorCodes.UmbNoBlocksAvailable; + _state.BL = (byte)XmsErrorCodes.NotImplemented; + DosMemoryManager dosMemoryManager = _dosMemoryManager; + if (!dosMemoryManager.HasUpperMemoryBlocks) { + return; + } + + ushort previousStrategy = dosMemoryManager.AllocationStrategyValue; + byte previousLinkState = dosMemoryManager.UmbChainState; + if (previousLinkState == 0) { + dosMemoryManager.SetUmbChainLinkState(1); + } + dosMemoryManager.TrySetAllocationStrategy(0x40); + try { + DosMemoryControlBlock? block = dosMemoryManager.AllocateMemoryBlock(_state.DX); + if (block is not null) { + _state.AX = 1; + _state.BX = block.DataBlockSegment; + return; + } + + ushort largestUmbBlock = dosMemoryManager.FindLargestFreeSizeForAllocation(); + _state.DX = largestUmbBlock; + _state.BL = largestUmbBlock == 0 + ? (byte)XmsErrorCodes.UmbNoBlocksAvailable + : (byte)XmsErrorCodes.UmbOnlySmallerBlock; + } finally { + if (dosMemoryManager.UmbChainState != previousLinkState) { + dosMemoryManager.SetUmbChainLinkState(previousLinkState); + } + dosMemoryManager.TrySetAllocationStrategy(previousStrategy); + } } /// @@ -1540,7 +1574,16 @@ public void RequestUpperMemoryBlock() { /// public void ReleaseUpperMemoryBlock() { _state.AX = 0; - _state.BL = (byte)XmsErrorCodes.NotImplemented; + _state.BL = (byte)XmsErrorCodes.UmbInvalidSegment; + DosMemoryManager dosMemoryManager = _dosMemoryManager; + if (!dosMemoryManager.HasUpperMemoryBlocks) { + return; + } + + if (dosMemoryManager.FreeUpperMemoryBlock(_state.DX)) { + _state.AX = 1; + _state.BL = (byte)XmsErrorCodes.Ok; + } } /// diff --git a/src/Spice86.Core/Emulator/OperatingSystem/Dos.cs b/src/Spice86.Core/Emulator/OperatingSystem/Dos.cs index 2299705ba7..e9c6e30bd2 100644 --- a/src/Spice86.Core/Emulator/OperatingSystem/Dos.cs +++ b/src/Spice86.Core/Emulator/OperatingSystem/Dos.cs @@ -9,6 +9,7 @@ namespace Spice86.Core.Emulator.OperatingSystem; using Spice86.Core.Emulator.Devices.Video; using Spice86.Core.Emulator.Function; using Spice86.Core.Emulator.InterruptHandlers.Bios.Structures; +using Spice86.Core.Emulator.InterruptHandlers.Common.MemoryWriter; using Spice86.Core.Emulator.InterruptHandlers.Dos; using Spice86.Core.Emulator.InterruptHandlers.Dos.Ems; using Spice86.Core.Emulator.InterruptHandlers.Dos.Xms; @@ -160,6 +161,9 @@ public sealed class Dos : IDriveStatusProvider, IDiscSwapper, IDriveMountService /// public ExpandedMemoryManager? Ems { get; private set; } + /// + /// The XMS device driver. + /// public ExtendedMemoryManager? Xms { get; private set; } /// @@ -192,7 +196,9 @@ public sealed class Dos : IDriveStatusProvider, IDiscSwapper, IDriveMountService /// The logger service implementation. /// The sound channel creator, used to stream CD audio when an image is mounted. /// Notifier that surfaces per-drive read/write activity to the UI. - /// Optional XMS manager to expose through DOS. + /// The A20 gate used by XMS. + /// Writes the XMS callback into emulated memory. + /// The EMS manager shared with emulator services. public Dos(DosOptions options, IMemory memory, IFunctionHandlerProvider functionHandlerProvider, Stack stack, State state, BiosKeyboardBuffer biosKeyboardBuffer, KeyboardInt16Handler keyboardInt16Handler, @@ -201,14 +207,16 @@ public Dos(DosOptions options, IMemory memory, FloppyDiskTimingService floppyDiskTimingService, ISoundChannelCreator channelCreator, IDriveActivityNotifier activityNotifier, - ExtendedMemoryManager? xms) { + A20Gate a20Gate, + MemoryAsmWriter memoryAsmWriter, + ExpandedMemoryManager? emsManager) { _loggerService = loggerService; - Xms = xms; _biosKeyboardBuffer = biosKeyboardBuffer; _memory = memory; _biosDataArea = biosDataArea; _vgaFunctionality = vgaFunctionality; _activityNotifier = activityNotifier; + Ems = emsManager; DosTables = new DosTables(memory); ushort mediaIdSegment = DosTables.ReserveDosPrivateSegment((ushort)DosMediaIdTable.TableSizeInParagraphs); @@ -245,7 +253,14 @@ public Dos(DosOptions options, IMemory memory, // Initialize memory manager first - it must know about the root COMMAND.COM reserved space // Root PSP is at 0x60, environment at 0x68, so MCB chain starts after 0x6F // This matches FreeDOS where DOS_PSP + 16 paragraphs is reserved - MemoryManager = new DosMemoryManager(_memory, initialPspSegment, loggerService); + bool umbEnabled = options.Umb && options.Xms is not false; + MemoryManager = new DosMemoryManager(_memory, initialPspSegment, loggerService, + DosSysVars, umbEnabled, options.Ems is not false); + if (options.Xms is not false) { + // XMS uses the DOS memory manager to allocate upper memory blocks. + Xms = new ExtendedMemoryManager(memory, state, a20Gate, memoryAsmWriter, + DosTables, MemoryManager, loggerService); + } FcbManager = new(_memory, FileManager, DosDriveManager, _loggerService); IBatchDisplayCommandHandler batchDisplayCommandHandler = new DosBatchDisplayCommandHandler(_vgaFunctionality); @@ -261,7 +276,7 @@ public Dos(DosOptions options, IMemory memory, DosInt20Handler = new DosInt20Handler(_memory, functionHandlerProvider, stack, state, DosInt21Handler, _loggerService); DosInt2aHandler = new DosInt2aHandler(_memory, functionHandlerProvider, stack, state, _loggerService); DosInt2FHandler = new DosInt2fHandler(_memory, - functionHandlerProvider, stack, state, _loggerService, _mscdex, xms); + functionHandlerProvider, stack, state, _loggerService, _mscdex, Xms); DosInt25Handler = new DosDiskInt25Handler(_memory, DosDriveManager, functionHandlerProvider, stack, state, floppyDiskTimingService, _loggerService); DosInt26Handler = new DosDiskInt26Handler(_memory, DosDriveManager, @@ -279,13 +294,11 @@ public Dos(DosOptions options, IMemory memory, } OpenDefaultFileHandles(dosDevices); - if (options.Xms is not false && xms is not null) { - Xms = xms; - AddDevice(xms, ExtendedMemoryManager.DosDeviceSegment, 0); + if (options.Xms is not false && Xms is not null) { + AddDevice(Xms, ExtendedMemoryManager.DosDeviceSegment, 0); } - if (options.Ems is not false) { - Ems = new(_memory, functionHandlerProvider, stack, state, _loggerService); + if (options.Ems is not false && Ems is not null) { AddDevice(Ems.AsCharacterDevice(), ExpandedMemoryManager.DosDeviceSegment, 0); } } diff --git a/src/Spice86.Core/Emulator/OperatingSystem/DosMemoryManager.cs b/src/Spice86.Core/Emulator/OperatingSystem/DosMemoryManager.cs index dc97db941e..4bdd2d4d4d 100644 --- a/src/Spice86.Core/Emulator/OperatingSystem/DosMemoryManager.cs +++ b/src/Spice86.Core/Emulator/OperatingSystem/DosMemoryManager.cs @@ -10,22 +10,28 @@ namespace Spice86.Core.Emulator.OperatingSystem; using Spice86.Shared.Utils; using System.Linq; +using System.Text; /// /// Implements DOS memory operations, such as allocating and releasing MCBs. /// public class DosMemoryManager { internal const ushort LastFreeSegment = MemoryMap.GraphicVideoMemorySegment - 1; + private const ushort UmbChainStartSegment = 0x9FFF; + private const ushort FirstUmbMcbSegment = 0xD000; + private const ushort UmbBridgeOwner = 0x0008; + private const ushort UmbWithoutEmsParagraphs = 0x2000; + private const ushort UmbWithEmsParagraphs = 0x1000; private const ushort FakeMcbSize = 0xFFFF; private const byte FitTypeMask = 0x03; private const byte MaxValidFitType = 0x02; - private const byte ReservedBitsMask = 0x3C; private const byte HighMemMask = 0xC0; - private const byte HighMemFirstThenLow = 0x40; - private const byte HighMemOnlyNoFallback = 0x80; + private const byte HighMemFirstThenLow = 0x80; private readonly ILogger _loggerService; private readonly IMemory _memory; private readonly DosMemoryControlBlock _start; + private readonly DosMemoryControlBlock? _umbChainStart; + private readonly DosSysVars _dosSysVars; private readonly DosSwappableDataArea _sda; @@ -36,18 +42,23 @@ public class DosMemoryManager { /// The default strategy is to match MS-DOS behavior. /// This can be changed via INT 21h/58h (Get/Set Memory Allocation Strategy). /// - private DosMemoryAllocationStrategy _allocationStrategy = DosMemoryAllocationStrategy.FirstFit; + private ushort _allocationStrategy = (ushort)DosMemoryAllocationStrategy.FirstFit; /// - /// Initializes a new instance. + /// Initializes DOS memory with an optional DOSBox-compatible UMB chain. /// /// The memory bus. /// The initial PSP segment for MCB chain setup. /// The logger service implementation. - public DosMemoryManager(IMemory memory, ushort initialPspSegment, ILogger loggerService) { + /// The DOS InfoBlock containing UMB chain state. + /// Whether UMBs should be created. + /// Whether EMS reserves the upper half of the UMB area. + public DosMemoryManager(IMemory memory, ushort initialPspSegment, ILogger loggerService, + DosSysVars dosSysVars, bool umbEnabled, bool emsActive) { _loggerService = loggerService; _memory = memory; _sda = new(_memory, MemoryUtils.ToPhysicalAddress(DosSwappableDataArea.BaseSegment, 0)); + _dosSysVars = dosSysVars; ushort pspSegment = initialPspSegment; // The MCB starts 1 paragraph (16 bytes) before the 16 paragraph (256 bytes) PSP. Since @@ -55,57 +66,138 @@ public DosMemoryManager(IMemory memory, ushort initialPspSegment, ILogger logger // with its address by subtracting 1 paragraph from the PSP. ushort loadSegment = (ushort)(pspSegment - 1); _start = GetDosMemoryControlBlockFromSegment(loadSegment); - // LastFreeSegment and loadSegment are both valid segments that may be allocated, so we - // need to add 1 paragraph to the result to ensure that our calculated size doesn't exclude - // LastFreeSegment from being allocated. Some games do their own math to calculate the - // maximum free conventional memory from the last block that was allocated rather than - // asking the memory manager, and if we were off by one, allocation would fail. - ushort size = (ushort)((LastFreeSegment - loadSegment) + 1); - // We adjusted the start address above so that it starts with the MCB, but the MCB itself - // isn't actually useable space. We need it here in the DOS memory manager for accounting. - // Therefore subtract the size of the MCB (1 paragraph, which is 16 bytes) from the total - // size to get the useable space that we can allocate. - _start.Size = (ushort)(size - 1); + _dosSysVars.FirstMCB = loadSegment; + _dosSysVars.MemAllocScanStart = loadSegment; + ushort size; + if (umbEnabled) { + size = (ushort)(UmbChainStartSegment - loadSegment - 1); + } else { + // LastFreeSegment and loadSegment are both valid segments that may be allocated, so we + // need to add 1 paragraph to the result to ensure that our calculated size doesn't exclude + // LastFreeSegment from being allocated. Some games do their own math to calculate the + // maximum free conventional memory from the last block that was allocated rather than + // asking the memory manager, and if we were off by one, allocation would fail. + size = (ushort)((LastFreeSegment - loadSegment) + 1); + // The MCB itself isn't usable space, so subtract its paragraph from the total. + size = (ushort)(size - 1); + } + _start.Size = size; if (_loggerService.IsEnabled(LogLevel.Information)) { _loggerService.LogInformation( - "DOS available memory: {ConventionalFree} - in paragraphs: {DosFreeParagraphs}", - _start.AllocationSizeInBytes, _start.Size); + "DOS available memory: {ConventionalFreeKb} - in paragraphs: {DosFreeParagraphs}", + _start.AllocationSizeInBytes / 1024, _start.Size); } _start.SetFree(); _start.SetLast(); + + if (umbEnabled) { + _umbChainStart = GetDosMemoryControlBlockFromSegment(UmbChainStartSegment); + _umbChainStart.PspSegment = UmbBridgeOwner; + _umbChainStart.Size = (ushort)(FirstUmbMcbSegment - UmbChainStartSegment - 1); + _umbChainStart.SetNonLast(); + _umbChainStart.SetOwnerName("SC"); + + DosMemoryControlBlock firstUmb = GetDosMemoryControlBlockFromSegment(FirstUmbMcbSegment); + firstUmb.SetFree(); + firstUmb.Size = (ushort)((emsActive ? UmbWithEmsParagraphs : UmbWithoutEmsParagraphs) - 1); + firstUmb.SetLast(); + _dosSysVars.StartOfUMBChain = UmbChainStartSegment; + _dosSysVars.ChainingUMB = 0; + } else { + _umbChainStart = null; + _dosSysVars.StartOfUMBChain = 0xFFFF; + _dosSysVars.ChainingUMB = 0; + } + + // Initial detailed memory map for diagnostics + LogMemoryGraphic("startup"); } /// - /// Gets or sets the current memory allocation strategy (INT 21h/58h). + /// Gets whether an upper-memory chain was created. /// - public DosMemoryAllocationStrategy AllocationStrategy { - get => _allocationStrategy; - set { - // Validate the strategy - only allow valid combinations - byte fitType = (byte)((byte)value & FitTypeMask); - if (fitType > MaxValidFitType) { - // Invalid fit type, ignore - return; + public bool HasUpperMemoryBlocks => _umbChainStart is not null; + + /// + /// Gets the low bit of the DOS UMB chain link state. + /// + public byte UmbChainState => (byte)(_dosSysVars.ChainingUMB & 1); + + /// + /// Links or unlinks the UMB chain from the conventional MCB chain. + /// + /// Zero to unlink, one to link. + /// Whether the requested link state was applied. + public bool SetUmbChainLinkState(ushort linkState) { + if (!HasUpperMemoryBlocks || linkState > 1) { + return false; + } + if (UmbChainState == linkState) { + return true; + } + + DosMemoryControlBlock current = _start; + while (true) { + ushort currentSegment = MemoryUtils.ToSegment(current.BaseAddress); + ushort nextSegment = (ushort)(currentSegment + current.Size + 1); + if (nextSegment == UmbChainStartSegment) { + if (linkState == 1) { + current.SetNonLast(); + } else { + current.SetLast(); + } + _dosSysVars.ChainingUMB = (byte)linkState; + return true; } - // Validate bits 2-5 must be zero per DOS specification - if (((byte)value & ReservedBitsMask) != 0) { - return; + + if (current.IsLast) { + return false; } - byte highMemBits = (byte)((byte)value & HighMemMask); - if (highMemBits is not 0x00 and not HighMemFirstThenLow and not HighMemOnlyNoFallback) { - // Invalid high memory bits, ignore - return; + + DosMemoryControlBlock? next = current.GetNextOrDefault(); + if (next is null || !next.IsValid) { + return false; } - _allocationStrategy = value; + current = next; } } + /// + /// Gets or sets the current memory allocation strategy (INT 21h/58h). + /// + public DosMemoryAllocationStrategy AllocationStrategy { + get => (DosMemoryAllocationStrategy)_allocationStrategy; + set { + TrySetAllocationStrategy((ushort)value); + } + } + + /// + /// Gets the full allocation strategy value returned by INT 21h/58h. + /// + public ushort AllocationStrategyValue => _allocationStrategy; + + /// + /// Validates and updates the DOS memory allocation strategy. + /// + /// The strategy value from BX. + /// Whether the strategy was accepted. + public bool TrySetAllocationStrategy(ushort strategy) { + // DOSBox accepts and preserves all bits outside the low six-bit fit-type field. + if ((strategy & 0x3F) > MaxValidFitType) { + return false; + } + _allocationStrategy = strategy; + return true; + } + /// /// Allocates a memory block of the specified size. Returns null if no memory block could be found to fit the requested size. /// /// The requested size in paragraphs of the memory block. /// The allocated or null if no memory block could be found. public DosMemoryControlBlock? AllocateMemoryBlock(ushort requestedSizeInParagraphs) { + LogMemoryGraphic($"AllocateMemoryBlock - before requested:{requestedSizeInParagraphs}"); IEnumerable candidates = FindCandidatesForAllocation(requestedSizeInParagraphs); // Select block based on allocation strategy @@ -115,19 +207,33 @@ public DosMemoryAllocationStrategy AllocationStrategy { if (_loggerService.IsEnabled(LogLevel.Error)) { _loggerService.LogError("Could not find any MCB to fit {RequestedSize}", requestedSizeInParagraphs); } + LogMemoryGraphic($"AllocateMemoryBlock - failed requested:{requestedSizeInParagraphs}"); return null; } DosMemoryControlBlock block = blockOptional; - if (!SplitBlock(block, requestedSizeInParagraphs)) { + byte fitType = (byte)(_allocationStrategy & FitTypeMask); + if (fitType == (byte)DosMemoryAllocationStrategy.LastFit) { + DosMemoryControlBlock? allocatedBlock = SplitBlockFromEnd(block, requestedSizeInParagraphs); + if (allocatedBlock is null) { + if (_loggerService.IsEnabled(LogLevel.Error)) { + _loggerService.LogError("Could not split block {Block}", block); + } + LogMemoryGraphic($"AllocateMemoryBlock - split_failed block:{ConvertUtils.ToHex16(block.DataBlockSegment)}"); + return null; + } + block = allocatedBlock; + } else if (!SplitBlock(block, requestedSizeInParagraphs)) { // An issue occurred while splitting the block if (_loggerService.IsEnabled(LogLevel.Error)) { _loggerService.LogError("Could not split block {Block}", block); } + LogMemoryGraphic($"AllocateMemoryBlock - split_failed block:{ConvertUtils.ToHex16(block.DataBlockSegment)}"); return null; } block.PspSegment = _sda.CurrentProgramSegmentPrefix; + LogMemoryGraphic($"AllocateMemoryBlock - allocated seg:{ConvertUtils.ToHex16(block.DataBlockSegment)} size:{block.Size} psp:{ConvertUtils.ToHex16(block.PspSegment)}"); return block; } @@ -136,9 +242,24 @@ public DosMemoryAllocationStrategy AllocationStrategy { /// /// The largest free public DosMemoryControlBlock FindLargestFree() { - return EnumerateBlocks() + LogMemoryGraphic("FindLargestFree - before"); + DosMemoryControlBlock res = EnumerateAllocationBlocks() .Where(block => block.IsFree) .MaxBy(block => block.Size) ?? _start; + LogMemoryGraphic("FindLargestFree - after"); + return res; + } + + /// + /// Finds the largest free block considered by the current allocation strategy. + /// + /// The largest free block size, or zero if the strategy's chains have no free blocks. + public ushort FindLargestFreeSizeForAllocation() { + return EnumerateAllocationBlocks() + .Where(static block => block.IsFree) + .Select(static block => block.Size) + .DefaultIfEmpty((ushort)0) + .Max(); } /// @@ -156,6 +277,44 @@ public IEnumerable EnumerateBlocks() { } } + /// + /// Walks conventional-memory MCBs without following a linked UMB chain. + /// + public IEnumerable EnumerateConventionalBlocks() { + DosMemoryControlBlock? current = _start; + while (current is not null) { + yield return current; + if (current.IsLast) { + yield break; + } + + DosMemoryControlBlock? next = current.GetNextOrDefault(); + if (next is null || next.BaseAddress == _umbChainStart?.BaseAddress) { + yield break; + } + current = next; + } + } + + /// + /// Walks the UMB chain without changing whether it is linked to the conventional MCB chain. + /// The conventional-to-UMB bridge MCB is excluded because it is not allocatable UMB data. + /// + public IEnumerable EnumerateUpperMemoryBlocks() { + if (_umbChainStart is null) { + yield break; + } + + DosMemoryControlBlock? firstUpperMemoryBlock = _umbChainStart.GetNextOrDefault(); + if (firstUpperMemoryBlock is null) { + yield break; + } + + foreach (DosMemoryControlBlock block in EnumerateChain(firstUpperMemoryBlock)) { + yield return block; + } + } + /// /// Releases an MCB. /// @@ -171,14 +330,47 @@ public bool FreeMemoryBlock(ushort blockSegment) { /// The MCB to free. /// Whether the operation was successful. public bool FreeMemoryBlock(DosMemoryControlBlock block) { + LogMemoryGraphic($"FreeMemoryBlock - before seg:{ConvertUtils.ToHex16(block.DataBlockSegment)}"); if (!CheckValidOrLogError(block)) { + LogMemoryGraphic($"FreeMemoryBlock - invalid seg:{ConvertUtils.ToHex16(block.DataBlockSegment)}"); return false; } block.SetFree(); + LogMemoryGraphic($"FreeMemoryBlock - after freed seg:{ConvertUtils.ToHex16(block.DataBlockSegment)}"); return true; } + /// + /// Releases an allocated block in the UMB chain. + /// + /// The segment address of the UMB data block. + /// true if an allocated UMB was released; otherwise, false. + public bool FreeUpperMemoryBlock(ushort dataBlockSegment) { + if (_umbChainStart is null || dataBlockSegment == 0) { + return false; + } + + DosMemoryControlBlock? current = _umbChainStart; + bool isBridge = true; + while (current is not null) { + if (!CheckValidOrLogError(current)) { + return false; + } + + if (!isBridge && current.DataBlockSegment == dataBlockSegment) { + return !current.IsFree && FreeMemoryBlock(current); + } + + if (current.IsLast) { + break; + } + current = current.GetNextOrDefault(); + isBridge = false; + } + return false; + } + /// /// Extends or reduces a MCB. /// @@ -199,10 +391,12 @@ public DosErrorCode ModifyBlock(ushort blockSegment, ushort requestedSizeInParag /// Whether the operation was successful. public DosErrorCode TryModifyBlock(in ushort blockSegment, in ushort requestedSizeInParagraphs, out DosMemoryControlBlock block) { + LogMemoryGraphic($"TryModifyBlock - start seg:{ConvertUtils.ToHex16(blockSegment)} req:{requestedSizeInParagraphs}"); block = GetDosMemoryControlBlockFromSegment((ushort)(blockSegment - 1)); ushort newSizeInParagraphs = requestedSizeInParagraphs; if (!CheckValidOrLogError(block)) { + LogMemoryGraphic($"TryModifyBlock - invalid mcb seg:{ConvertUtils.ToHex16(blockSegment)}"); return DosErrorCode.MemoryControlBlockDestroyed; } @@ -216,6 +410,7 @@ public DosErrorCode TryModifyBlock(in ushort blockSegment, in ushort requestedSi if (_loggerService.IsEnabled(LogLevel.Error)) { _loggerService.LogError("Could not join MCB {Block}", block); } + LogMemoryGraphic($"TryModifyBlock - join_failed seg:{ConvertUtils.ToHex16(block.DataBlockSegment)}"); return DosErrorCode.InsufficientMemory; } @@ -229,6 +424,7 @@ public DosErrorCode TryModifyBlock(in ushort blockSegment, in ushort requestedSi _loggerService.LogTrace("Next MCB is {Block}", nextBlock); } } + LogMemoryGraphic($"TryModifyBlock - too_small seg:{ConvertUtils.ToHex16(block.DataBlockSegment)} req:{newSizeInParagraphs} size:{block.Size}"); return DosErrorCode.InsufficientMemory; } @@ -236,6 +432,7 @@ public DosErrorCode TryModifyBlock(in ushort blockSegment, in ushort requestedSi SplitBlock(block, newSizeInParagraphs); } block.PspSegment = _sda.CurrentProgramSegmentPrefix; + LogMemoryGraphic($"TryModifyBlock - success seg:{ConvertUtils.ToHex16(block.DataBlockSegment)} size:{block.Size}"); return DosErrorCode.NoError; } @@ -481,25 +678,62 @@ private bool CheckValidOrLogError(DosMemoryControlBlock? block) { } private List FindCandidatesForAllocation(int requestedSize) { - DosMemoryControlBlock? current = _start; List candidates = new(); - while (true) { + byte highMemoryStrategy = (byte)(_allocationStrategy & HighMemMask); + if (highMemoryStrategy != 0 && _umbChainStart is not null) { + AddAllocationCandidates(_umbChainStart, requestedSize, UmbChainState != 0, candidates); + if ((highMemoryStrategy & HighMemFirstThenLow) == 0) { + return candidates; + } + } + AddAllocationCandidates(_start, requestedSize, true, candidates); + return candidates; + } + + private void AddAllocationCandidates(DosMemoryControlBlock start, int requestedSize, + bool compressChain, List candidates) { + DosMemoryControlBlock? current = start; + while (current is not null) { if (!CheckValidOrLogError(current)) { - return new List(); + candidates.Clear(); + return; + } + if (compressChain) { + JoinBlocks(current, true); } - JoinBlocks(current, true); - if (current?.IsFree == true && current.Size >= requestedSize) { + if (current.IsFree && current.Size >= requestedSize) { candidates.Add(current); } - if (current?.IsLast == true) { - return candidates; + if (current.IsLast) { + return; } + current = current.GetNextOrDefault(); + } + } - DosMemoryControlBlock? next = current?.GetNextOrDefault(); + private IEnumerable EnumerateAllocationBlocks() { + byte highMemoryStrategy = (byte)(_allocationStrategy & HighMemMask); + if (highMemoryStrategy != 0 && _umbChainStart is not null) { + foreach (DosMemoryControlBlock block in EnumerateChain(_umbChainStart)) { + yield return block; + } + if ((highMemoryStrategy & HighMemFirstThenLow) == 0) { + yield break; + } + } + foreach (DosMemoryControlBlock block in EnumerateChain(_start)) { + yield return block; + } + } - if (next is not null) { - current = next; + private static IEnumerable EnumerateChain(DosMemoryControlBlock start) { + DosMemoryControlBlock? current = start; + while (current is not null) { + yield return current; + if (current.IsLast) { + yield break; } + current = current.GetNextOrDefault(); } } @@ -507,6 +741,61 @@ private DosMemoryControlBlock GetDosMemoryControlBlockFromSegment(ushort blockSe return new DosMemoryControlBlock(_memory, MemoryUtils.ToPhysicalAddress(blockSegment, 0)); } + /// + /// Dumps an ASCII memory map at debug level for diagnostic comparison across calls. + /// + /// A short context string indicating the caller or event. + private void LogMemoryGraphic(string context) { + if (!_loggerService.IsEnabled(LogLevel.Debug)) { + return; + } + + var blocks = EnumerateBlocks().ToList(); + if (blocks.Count == 0) { + _loggerService.LogDebug("DOS Memory Map ({Context}): no blocks", context); + return; + } + + int totalParagraphs = blocks.Sum(b => (int)b.Size + 1); + int width = 96; // fixed output width for comparability + int paragraphsPerChar = Math.Max(1, (int)Math.Ceiling(totalParagraphs / (double)width)); + + var sb = new StringBuilder(); + sb.AppendLine($"DOS Memory Map ({context}) total paragraphs: {totalParagraphs} (par/char~{paragraphsPerChar})"); + + var bar = new StringBuilder(); + foreach (var b in blocks) { + int blockParagraphs = (int)b.Size + 1; + int chars = Math.Max(1, (int)Math.Round(blockParagraphs / (double)paragraphsPerChar)); + char ch; + if (!b.IsValid) { + ch = '!'; + } else if (b.Size == FakeMcbSize) { + ch = 'x'; + } else if (b.IsFree) { + ch = '.'; + } else { + ch = '#'; + } + + for (int i = 0; i < chars; i++) { + bar.Append(ch); + } + } + + sb.AppendLine(bar.ToString()); + + // Detailed legend per block to match positions roughly. Include start segment and size + foreach (var b in blocks) { + string seg = ConvertUtils.ToHex16(b.DataBlockSegment); + string psp = ConvertUtils.ToHex16(b.PspSegment); + string owner = string.IsNullOrEmpty(b.Owner) ? "" : b.Owner.Trim(); + sb.AppendLine($"{seg} | {(b.IsFree ? "FREE " : "USED ")} Size:{b.Size,5} par Owner:{owner,-8} PSP:{psp} Last:{b.IsLast}"); + } + + _loggerService.LogDebug(sb.ToString()); + } + private bool JoinBlocks(DosMemoryControlBlock? block, bool onlyIfFree) { if (onlyIfFree && block?.IsFree == false) { // Do not touch blocks in use @@ -589,20 +878,36 @@ private bool SplitBlock(DosMemoryControlBlock block, ushort size) { return true; } + private DosMemoryControlBlock? SplitBlockFromEnd(DosMemoryControlBlock block, ushort size) { + ushort blockSize = block.Size; + if (blockSize < size) { + return null; + } + if (blockSize == size) { + return block; + } + + ushort mcbSegment = MemoryUtils.ToSegment(block.BaseAddress); + ushort allocationMcbSegment = (ushort)(mcbSegment + blockSize - size); + byte originalType = block.TypeField; + block.Size = (ushort)(blockSize - size - 1); + block.SetNonLast(); + block.SetFree(); + + DosMemoryControlBlock allocatedBlock = GetDosMemoryControlBlockFromSegment(allocationMcbSegment); + allocatedBlock.TypeField = originalType; + allocatedBlock.Size = size; + return allocatedBlock; + } + /// /// Selects a memory block based on the current allocation strategy. /// /// List of candidate blocks that fit the requested size. /// The selected block or null if none found. - /// - /// Note: High memory bits (bits 6-7) of the allocation strategy are currently not handled. - /// This method only implements low memory allocation strategies. UMB (Upper Memory Block) - /// support would need to be added to handle strategies like FirstFitHighThenLow (0x40) or - /// FirstFitHighOnlyNoFallback (0x80). - /// private DosMemoryControlBlock? SelectBlockByStrategy(IEnumerable candidates) { // Get the fit type from the lower 2 bits of the strategy - byte fitType = (byte)((byte)_allocationStrategy & FitTypeMask); + byte fitType = (byte)(_allocationStrategy & FitTypeMask); DosMemoryControlBlock? selectedBlock = null; @@ -672,8 +977,23 @@ public bool CheckMcbChain() { /// The PSP segment whose memory should be freed. /// true if all blocks were freed successfully, false if an error occurred. public bool FreeProcessMemory(ushort pspSegment) { - DosMemoryControlBlock? current = _start; + LogMemoryGraphic($"FreeProcessMemory - start psp:{ConvertUtils.ToHex16(pspSegment)}"); + if (!FreeProcessMemoryChain(_start, pspSegment, true)) { + LogMemoryGraphic($"FreeProcessMemory - failed corrupted psp:{ConvertUtils.ToHex16(pspSegment)}"); + return false; + } + if (_umbChainStart is not null && UmbChainState == 0 && + !FreeProcessMemoryChain(_umbChainStart, pspSegment, true)) { + LogMemoryGraphic($"FreeProcessMemory - failed corrupted psp:{ConvertUtils.ToHex16(pspSegment)}"); + return false; + } + + LogMemoryGraphic($"FreeProcessMemory - done psp:{ConvertUtils.ToHex16(pspSegment)}"); + return true; + } + private bool FreeProcessMemoryChain(DosMemoryControlBlock? current, ushort pspSegment, bool compressFreeBlocks) { + DosMemoryControlBlock? chainStart = current; while (current is not null) { if (!current.IsValid) { if (_loggerService.IsEnabled(LogLevel.Error)) { @@ -681,21 +1001,37 @@ public bool FreeProcessMemory(ushort pspSegment) { } return false; } - - // Free blocks owned by this PSP if (current.PspSegment == pspSegment) { current.SetFree(); - // Coalesce adjacent free blocks - JoinBlocks(current, true); } - if (current.IsLast) { break; } - current = current.GetNextOrDefault(); } + if (!compressFreeBlocks) { + return true; + } + + // Free all process blocks before coalescing so adjacent blocks owned by the same process + // are merged even when the chain was traversed from low to high addresses. + current = chainStart; + while (current is not null) { + if (!current.IsValid) { + if (_loggerService.IsEnabled(LogLevel.Error)) { + _loggerService.LogError("MCB chain corrupted while coalescing process memory"); + } + return false; + } + if (current.IsFree && !JoinBlocks(current, true)) { + return false; + } + if (current.IsLast) { + break; + } + current = current.GetNextOrDefault(); + } return true; } @@ -710,9 +1046,12 @@ public bool FreeEnvironmentBlock(ushort environmentSegment, ushort ownerPspSegme return true; } + LogMemoryGraphic($"FreeEnvironmentBlock - start env:{ConvertUtils.ToHex16(environmentSegment)} owner:{ConvertUtils.ToHex16(ownerPspSegment)}"); + ushort mcbSegment = (ushort)(environmentSegment - 1); DosMemoryControlBlock block = GetDosMemoryControlBlockFromSegment(mcbSegment); if (!CheckValidOrLogError(block)) { + LogMemoryGraphic($"FreeEnvironmentBlock - invalid mcb env:{ConvertUtils.ToHex16(environmentSegment)}"); return false; } @@ -722,12 +1061,13 @@ public bool FreeEnvironmentBlock(ushort environmentSegment, ushort ownerPspSegme "Environment block at {EnvSegment:X4} not owned by PSP {Owner:X4}, skipping free", environmentSegment, ownerPspSegment); } + LogMemoryGraphic($"FreeEnvironmentBlock - skipped not owner env:{ConvertUtils.ToHex16(environmentSegment)} owner:{ConvertUtils.ToHex16(ownerPspSegment)}"); return true; } block.SetFree(); JoinBlocks(_start, true); + LogMemoryGraphic($"FreeEnvironmentBlock - freed env:{ConvertUtils.ToHex16(environmentSegment)} owner:{ConvertUtils.ToHex16(ownerPspSegment)}"); return true; } } - diff --git a/src/Spice86.Core/Emulator/OperatingSystem/Enums/DosMemoryAllocationStrategy.cs b/src/Spice86.Core/Emulator/OperatingSystem/Enums/DosMemoryAllocationStrategy.cs index e473a2e324..58c9dc657d 100644 --- a/src/Spice86.Core/Emulator/OperatingSystem/Enums/DosMemoryAllocationStrategy.cs +++ b/src/Spice86.Core/Emulator/OperatingSystem/Enums/DosMemoryAllocationStrategy.cs @@ -28,38 +28,38 @@ public enum DosMemoryAllocationStrategy : byte { LastFit = 0x02, /// - /// First fit, try high memory first, then low. - /// Used when UMBs are linked to the MCB chain. + /// First fit in UMBs only. + /// Used whether or not UMBs are linked to the conventional MCB chain. /// - FirstFitHighThenLow = 0x40, + FirstFitHighOnlyNoFallback = 0x40, /// - /// Best fit, try high memory first, then low. - /// Used when UMBs are linked to the MCB chain. + /// Best fit in UMBs only. + /// Used whether or not UMBs are linked to the conventional MCB chain. /// - BestFitHighThenLow = 0x41, + BestFitHighOnlyNoFallback = 0x41, /// - /// Last fit, try high memory first, then low. - /// Used when UMBs are linked to the MCB chain. + /// Last fit in UMBs only. + /// Used whether or not UMBs are linked to the conventional MCB chain. /// - LastFitHighThenLow = 0x42, + LastFitHighOnlyNoFallback = 0x42, /// - /// First fit, high memory only (no fallback to low). + /// First fit, try UMBs first, then conventional memory. /// Used when UMBs are linked to the MCB chain. /// - FirstFitHighOnlyNoFallback = 0x80, + FirstFitHighThenLow = 0x80, /// - /// Best fit, high memory only (no fallback to low). + /// Best fit, try UMBs first, then conventional memory. /// Used when UMBs are linked to the MCB chain. /// - BestFitHighOnlyNoFallback = 0x81, + BestFitHighThenLow = 0x81, /// - /// Last fit, high memory only (no fallback to low). + /// Last fit, try UMBs first, then conventional memory. /// Used when UMBs are linked to the MCB chain. /// - LastFitHighOnlyNoFallback = 0x82 + LastFitHighThenLow = 0x82 } diff --git a/src/Spice86.Core/Emulator/OperatingSystem/README.md b/src/Spice86.Core/Emulator/OperatingSystem/README.md new file mode 100644 index 0000000000..9d35b50136 --- /dev/null +++ b/src/Spice86.Core/Emulator/OperatingSystem/README.md @@ -0,0 +1,152 @@ +# DOS and Memory Layout + +Spice86 implements DOS services in C#. Assembly code is written to emulated +memory where DOS programs expect interrupt handlers, device headers, and +entry points. The assembly stubs call back into the corresponding C# handlers. + +## Conventional memory + +Conventional memory ends at physical address `0xA0000` (640 KiB). The BIOS +reports the configured RAM size, capped at 640 KiB. DOS allocates conventional +memory through an MCB chain; its final block ends at segment `0x9FFF` when UMB +support is disabled. With UMB support enabled, a bridge MCB at `0x9FFF` can +link the conventional chain to the UMB chain at `0xD000`. + +Memory is allocated in paragraphs of 16 bytes. The DOS memory manager starts +with one free MCB, immediately before the initial program's PSP. The PSP +segment is the configured program-entry segment minus `0x10`; the initial MCB +is one paragraph before that PSP. DOS allocations, program PSPs, environments, +and resident programs use this chain. The default allocation strategy is first +fit; INT 21h/AH=58h can change the strategy and, when UMBs are available, the +memory searched. + +Programs that need conventional memory should allocate only what they use, +free unused blocks with INT 21h/AH=49h, and resize their own block with +INT 21h/AH=4Ah after loading. A resident program must keep its resident code +and data allocated. XMS and EMS provide separate memory; they do not increase +the free space in the conventional MCB chain. + +The configured program-entry segment determines the initial MCB location. With +the default entry segment `0x170`, the initial PSP is at `0x160` and its MCB +is at `0x15F`. This is separate from the root `COMMAND.COM` PSP at `0x0060`. +Its initial free block contains `0x9E9F` paragraphs (649,712 bytes). This MCB +capacity is distinct from the 640 KiB total conventional memory reported by +INT 12h. + +### Conventional-memory map + +The rows are not all separate allocations. In particular, the low-memory +structures are within the conventional RAM range. + +| Physical address | Segment:offset | Use | +| --- | --- | --- | +| `0x00000-0x003FF` | `0000:0000` | Interrupt Vector Table. Spice86 writes installed interrupt handler addresses here. | +| `0x00400-0x004FF` | `0040:0000` | BIOS Data Area, including conventional-memory size and keyboard state. | +| `0x00500-0x9FFFF` | `0050:0000` to `9FFF:000F` | Conventional RAM. `0x50` is the free-memory start constant, not a guarantee that all addresses above it are free. | +| `0x00600-0x006FF` | `0060:0000` | Root `COMMAND.COM` PSP. Its environment is placed at `0068:0000`, overlapping the PSP command-tail area. | +| `0x00700-0x0078B` | `0070:0000` | DOS SYSVARS structure (`0x8C` bytes). Its `FirstMCB` and `MemAllocScanStart` fields identify the actual initial MCB. | +| From `0x00B20` | `00B2:0000` | DOS Swappable Data Area (SDA), including the current PSP and InDOS state. | +| `0x01000-0x01007` | `0100:0000` | VGA static-functionality table written by Spice86. | +| `0x07C00-0x07DFF` | `07C0:0000` | Boot-sector load area, used when loading a boot sector. | +| From `initial PSP - 1` through segment `0x9FFF` | DOS MCB chain | Conventional free and allocated blocks. When UMBs are enabled, the block before `0x9FFF` ends at the bridge MCB there. | + +SYSVARS `FirstMCB` and `MemAllocScanStart` are initialized to `initial PSP - 1` +(at `0x15F` with the default program entry segment). They identify the first +MCB in the memory manager's conventional chain. + +## Upper memory blocks + +UMB support is enabled by default with `--Umb true`. It requires XMS; disabling +XMS also disables UMBs. The UMB chain starts with an MCB at segment `0xD000`. +When EMS is enabled, the UMB covers `0xD0000-0xDFFFF`; without EMS, it extends +through `0xEFFFF`. The EMS page frame occupies `0xE0000-0xEFFFF`. + +The conventional and UMB chains are unlinked at startup. INT 21h/AH=58h, +AL=02h queries the link state and AL=03h changes it. Allocation strategies +40h-42h search UMBs only; 80h-82h search UMBs before conventional memory. +XMS 3.0 functions 10h-12h also request, release, and resize UMBs. + +## BIOS, DOS and emulator-owned memory + +The `0xA0000-0xFFFFF` range contains video memory and emulator-owned regions. +Some of it is available to DOS as UMBs as described above; the remaining +regions are reserved for video, DOS tables, EMS, BIOS code, interrupt stubs, +and device headers. + +| Physical address | Segment | Use | +| --- | --- | --- | +| `0xA0000-0xAFFFF` | `A000` | VGA graphics memory window. | +| `0xB0000-0xB7FFF` | `B000` | Monochrome text video memory. | +| `0xB8000-0xBFFFF` | `B800` | Color text video memory. | +| From `0xC0000` | `C000` | VGA ROM data mapping. Spice86 stores VGA font data here; it is not a complete VGA BIOS ROM. | +| `0xC8000-0xCFFFF` | `C800-CFFF` | DOS private table arena. DOS reserves entries here for internal tables; XMS reserves a paragraph for its guest entry stub. | +| `0xD0000-0xDFFFF` | `D000-DFFF` | UMB range when UMB support is enabled. The MCB is at `D000:0000`; usable memory starts at `D001:0000`. | +| `0xE0000-0xEFFFF` | `E000-EFFF` | EMS page frame when EMS is enabled; otherwise an additional UMB range when UMB support is enabled. | +| From `0xF0000` | `F000` | Supplied BIOS image area and default start for Spice86's interrupt and assembly stubs. | +| `0xF1000` | `F100` | EMS device header and INT 67h stub are both written at this address. | +| From `0xF8000` | `F800` | DOS device header area. XMS registers its `XMSXXXX0` header at `F800:0000`, also used as the first default device-header address. | + +The interrupt and assembly stubs are written by a shared `MemoryAsmWriter`. +The start segment is `ProvidedAsmHandlersSegment`, defaulting to `0xF000`. +Installed handlers advance the writer's address as they are emitted. The +actual offsets and amount of memory used depend on the installed handlers. +The supplied system BIOS image is also loaded at `F000:0000`, with its reset +entry at `F000:FFF0`; the default stub area and BIOS image therefore share the +same segment. + +Interrupt stubs call C# code with callback instructions written inline in +emulated memory. A callback instruction contains `FE 38` and a callback +number. The callback registry maps that number to a C# action; it does not +store the handler's C# code in guest memory. Interrupt callbacks can use their +interrupt vector as the number. Automatically allocated callback numbers +start at `0x100`. + +When DOS services are installed, the memory dump replaces +callback instructions with INT/NOP bytes, so tools like Ghidra don't have to deal with that noise. + +## XMS + +The XMS manager implements XMS 3.0 services. DOS programs find the XMS entry +point through INT 2Fh, AX=4300h/4310h, then call the returned entry point to +request XMS functions. + +The extended-memory pool is a separate RAM object of 15 MiB. XMS allocates +blocks from this pool by handle; these allocations do not consume conventional +MCB space. The High Memory Area begins at `0x100000` and ends at `0x10FFEF`. +XMS manages HMA allocation and the A20 gate. XMS also exposes UMB allocation; +those blocks come from the DOS UMB chain described above. + +The XMS guest entry stub is placed in the DOS private table arena. The XMS DOS +device header is registered at `F800:0000`. + +## EMS + +The EMS manager provides services through INT 67h and registers the DOS device +`EMMXXXX0`. It has 8 MiB of separate backing memory, divided into 512 logical +pages of 16 KiB. + +Four pages form the EMS page frame at `E000:0000-E000:FFFF`. The EMS system +handle allocates these pages. Programs map logical pages into the frame to +access them. EMS backing memory does not use conventional MCB space. + +The implementation supports core EMS 3.2 functions and selected EMS 4.0 +functions. VCPI and LIM EMS OS-specific functions are not implemented. The +INT 67h stub is written at `F100:0000`, the same address as the EMS device +header. + +## Related code + +- [MemoryMap.cs](../Memory/MemoryMap.cs): PC memory-map constants. +- [DosMemoryManager.cs](./DosMemoryManager.cs): conventional MCB chain. +- [DosMemoryAllocationStrategy.cs](./Enums/DosMemoryAllocationStrategy.cs): DOS allocation strategy values. +- [DosInt21Handler.cs](../InterruptHandlers/Dos/DosInt21Handler.cs): INT 21h memory services. +- [DosSysVars.cs](./Structures/DosSysVars.cs): SYSVARS structure and initial `FirstMCB`. +- [DosTables.cs](./Structures/DosTables.cs): DOS private table arena. +- [DosProcessManager.cs](./DosProcessManager.cs): root PSP and process setup. +- [Configuration.cs](../../CLI/Configuration.cs): DOS UMB command-line option. +- [ExtendedMemoryManager.cs](../InterruptHandlers/Dos/Xms/ExtendedMemoryManager.cs): XMS and HMA. +- [ExpandedMemoryManager.cs](../InterruptHandlers/Dos/Ems/ExpandedMemoryManager.cs): EMS services and page frame. +- [EmmMemory.cs](../InterruptHandlers/Dos/Ems/EmmMemory.cs): EMS backing memory size. +- [MemoryAsmWriter.cs](../InterruptHandlers/Common/MemoryWriter/MemoryAsmWriter.cs): assembly stubs and callback instructions. +- [CallbackHandler.cs](../InterruptHandlers/Common/Callback/CallbackHandler.cs): callback registration and dispatch. +- [InterruptInstaller.cs](../InterruptHandlers/Common/RoutineInstall/InterruptInstaller.cs): interrupt stubs and IVT entries. diff --git a/src/Spice86.Core/Emulator/OperatingSystem/Structures/DosMemoryControlBlock.cs b/src/Spice86.Core/Emulator/OperatingSystem/Structures/DosMemoryControlBlock.cs index d93b50bafa..65ad859571 100644 --- a/src/Spice86.Core/Emulator/OperatingSystem/Structures/DosMemoryControlBlock.cs +++ b/src/Spice86.Core/Emulator/OperatingSystem/Structures/DosMemoryControlBlock.cs @@ -59,6 +59,22 @@ public string Owner { set => SetZeroTerminatedString(FilenameFieldOffset, value, FilenameFieldSize); } + /// + /// Writes the fixed-width, space-padded DOS MCB owner-name field. + /// + /// The owner name, up to eight ASCII characters. + public void SetOwnerName(string ownerName) { + ArgumentNullException.ThrowIfNull(ownerName); + if (ownerName.Length > FilenameFieldSize) { + throw new ArgumentOutOfRangeException(nameof(ownerName)); + } + for (int index = 0; index < FilenameFieldSize; index++) { + UInt8[FilenameFieldOffset + index] = index < ownerName.Length + ? (byte)ownerName[index] + : (byte)' '; + } + } + /// /// Gets or sets the PSP segment associated with the MCB. /// diff --git a/src/Spice86.Core/Emulator/OperatingSystem/Structures/DosSysVars.cs b/src/Spice86.Core/Emulator/OperatingSystem/Structures/DosSysVars.cs index df12d159a2..6df6323d99 100644 --- a/src/Spice86.Core/Emulator/OperatingSystem/Structures/DosSysVars.cs +++ b/src/Spice86.Core/Emulator/OperatingSystem/Structures/DosSysVars.cs @@ -23,7 +23,6 @@ public class DosSysVars : MemoryBasedDataStructure { public const ushort SizeInBytes = 0x8C; public const ushort SizeInParagraphs = SizeInBytes / ParagraphSizeInBytes; public const int Segment = DosProcessManager.CommandComSegment + DosProgramSegmentPrefix.PspSizeInParagraphs; - public const int FirstMcbSegment = Segment + SizeInParagraphs; private readonly DosDeviceHeader _nullDeviceHeader; @@ -50,7 +49,6 @@ public DosSysVars(DosOptions options, NullDevice nullDevice, } MinMemForExec = 0x0; A20GateFixRoutineOffset = 0x0; - MemAllocScanStart = FirstMcbSegment; MaxSectorLength = 0x200; RegCXfrom5e = 0x0; CountLRUCache = 0x0; @@ -58,7 +56,6 @@ public DosSysVars(DosOptions options, NullDevice nullDevice, SharingCount = 0x0; SharingDelay = 0x0; PtrCONInput = 0x0; - FirstMCB = FirstMcbSegment; DirtyDiskBuffers = 0x0; LookaheadBufPt = SegmentedAddress.ZERO; LookaheadBufNumber = 0x0; diff --git a/src/Spice86/Spice86DependencyInjection.cs b/src/Spice86/Spice86DependencyInjection.cs index 74510da2a0..f4b29f1ec0 100644 --- a/src/Spice86/Spice86DependencyInjection.cs +++ b/src/Spice86/Spice86DependencyInjection.cs @@ -389,21 +389,8 @@ internal Spice86DependencyInjection(Configuration configuration, MainWindow? mai configuration.ProvidedAsmHandlersSegment, 0), callbackHandler); - ExtendedMemoryManager? xms = null; - - DosTables dosTables = new(memory); - SharedMouseData sharedMouseData = new(); - if (configuration.Xms is not false) { - xms = new(memory, state, a20Gate, memoryAsmWriter, dosTables, loggerService); - } - - if (configuration.Xms is not false && loggerService.IsEnabled( - LogLevel.Information)) { - loggerService.LogInformation("DOS XMS driver created..."); - } - SystemBiosInt15Handler systemBiosInt15Handler = new(configuration, memory, cfgCpu, stack, state, a20Gate, biosDataArea, emulationLoopScheduler, ioPortDispatcher, loggerService, configuration.InitializeDOS is not false); @@ -479,20 +466,16 @@ internal Spice86DependencyInjection(Configuration configuration, MainWindow? mai loggerService.LogInformation("Emulator state serializer created..."); } - ExpandedMemoryManager? mcpEmsManager = null; - if (configuration.Ems is not false || xms != null) { - mcpEmsManager = new ExpandedMemoryManager(memory, cfgCpu, stack, state, loggerService); - } - - EmulatorMcpServices emulatorMcpServices = new(memory, state, functionCatalogue, cfgCpu, - ioPortDispatcher, vgaRenderer, pauseHandler, mcpEmsManager, xms, - emulatorBreakpointsManager, cfgBlocksJsonExporter, loggerService); - BiosKeyboardBuffer biosKeyboardBuffer = new BiosKeyboardBuffer(memory, biosDataArea); KeyboardInt16Handler keyboardInt16Handler = new( memory, ioPortDispatcher, biosDataArea, cfgCpu, stack, state, loggerService, biosKeyboardBuffer); + ExpandedMemoryManager? emsManager = null; + if (configuration.Ems is not false || configuration.Xms is not false) { + emsManager = new ExpandedMemoryManager(memory, cfgCpu, stack, state, loggerService); + } + DosOptions dosOptions = RuntimeOptionsMapper.ToDosOptions(configuration, dosRuntimeState); Dos dos = new Dos(dosOptions, memory, cfgCpu, stack, state, biosKeyboardBuffer, @@ -500,7 +483,15 @@ internal Spice86DependencyInjection(Configuration configuration, MainWindow? mai new Dictionary { { "BLASTER", soundBlaster.BlasterString } }, ioPortDispatcher, loggerService, floppyDiskTimingService, - mixer, driveActivityNotifier, xms); + mixer, driveActivityNotifier, a20Gate, memoryAsmWriter, emsManager); + + if (configuration.Xms is not false && loggerService.IsEnabled(LogLevel.Information)) { + loggerService.LogInformation("DOS XMS driver created..."); + } + + EmulatorMcpServices emulatorMcpServices = new(memory, state, functionCatalogue, cfgCpu, + ioPortDispatcher, vgaRenderer, pauseHandler, emsManager, dos.Xms, + emulatorBreakpointsManager, cfgBlocksJsonExporter, loggerService); DmaChannel fdcDmaChannel = dmaSystem.GetChannel(2) ?? throw new InvalidOperationException("DMA channel 2 unavailable for Floppy Disk Controller."); diff --git a/src/Spice86/ViewModels/DataModels/DosMcbBarItem.cs b/src/Spice86/ViewModels/DataModels/DosMcbBarItem.cs index 029f67d353..90b504403d 100644 --- a/src/Spice86/ViewModels/DataModels/DosMcbBarItem.cs +++ b/src/Spice86/ViewModels/DataModels/DosMcbBarItem.cs @@ -1,7 +1,7 @@ namespace Spice86.ViewModels.DataModels; /// -/// One block in the conventional memory bar: a single MCB sized by . +/// One block in the DOS memory bar: a single MCB sized by . /// /// MCB header segment as a hex string (e.g. 0x0800). /// Total size of the block (header excluded) in bytes. diff --git a/src/Spice86/ViewModels/DosMcbsViewModel.cs b/src/Spice86/ViewModels/DosMcbsViewModel.cs index 23b60e2824..7911aa17c8 100644 --- a/src/Spice86/ViewModels/DosMcbsViewModel.cs +++ b/src/Spice86/ViewModels/DosMcbsViewModel.cs @@ -47,9 +47,28 @@ public sealed partial class DosMcbsViewModel : TimerRefreshViewModelBase { [ObservableProperty] private int _freeMcbCount; + [ObservableProperty] + private long _totalUpperMemoryBytes; + + [ObservableProperty] + private long _freeUpperMemoryBytes; + + [ObservableProperty] + private long _usedUpperMemoryBytes; + + [ObservableProperty] + private long _largestFreeUpperMemoryBlockBytes; + + [ObservableProperty] + private int _totalUpperMemoryMcbCount; + + [ObservableProperty] + private int _freeUpperMemoryMcbCount; + /// One-line layout summary suitable for a status header. public string LayoutSummary => - $"Total: {TotalConventionalBytes:N0} bytes / Used: {UsedConventionalBytes:N0} / Free: {FreeConventionalBytes:N0} / Largest free MCB: {LargestFreeBlockBytes:N0} / MCBs: {TotalMcbCount} (free: {FreeMcbCount})"; + $"Conventional: Total {TotalConventionalBytes:N0} bytes / Used {UsedConventionalBytes:N0} / Free {FreeConventionalBytes:N0} / Largest free MCB {LargestFreeBlockBytes:N0} / MCBs {TotalMcbCount} (free: {FreeMcbCount})" + + $" / UMB: Total {TotalUpperMemoryBytes:N0} bytes / Used {UsedUpperMemoryBytes:N0} / Free {FreeUpperMemoryBytes:N0} / Largest free MCB {LargestFreeUpperMemoryBlockBytes:N0} / MCBs {TotalUpperMemoryMcbCount} (free: {FreeUpperMemoryMcbCount})"; /// Initializes a new . public DosMcbsViewModel(DosMemoryManager memoryManager, IPauseHandler pauseHandler) : base(400, pauseHandler) { @@ -60,18 +79,20 @@ public DosMcbsViewModel(DosMemoryManager memoryManager, IPauseHandler pauseHandl protected override void RefreshCore() { List built = new(); List blocks = new(); + HashSet displayedHeaderSegments = new(); long total = 0; long free = 0; long used = 0; long largestFree = 0; + int conventionalCount = 0; int freeCount = 0; - foreach (DosMemoryControlBlock block in _memoryManager.EnumerateBlocks()) { - DosMcbInfo info = new(); - block.CopyToDosMcbInfo(info); - built.Add(info); + foreach (DosMemoryControlBlock block in _memoryManager.EnumerateConventionalBlocks()) { + AddBlockToDisplay(block, built, blocks, displayedHeaderSegments); if (block.IsValid) { + DosMcbInfo info = built[^1]; long sizeBytes = info.SizeBytes; total += sizeBytes; + conventionalCount++; if (info.IsFree) { free += sizeBytes; freeCount++; @@ -81,16 +102,34 @@ protected override void RefreshCore() { } else { used += sizeBytes; } - string ownerLabel; + } + if (!block.IsValid) { + break; + } + } + + long upperMemoryTotal = 0; + long upperMemoryFree = 0; + long upperMemoryUsed = 0; + long largestUpperMemoryFree = 0; + int upperMemoryCount = 0; + int upperMemoryFreeCount = 0; + foreach (DosMemoryControlBlock block in _memoryManager.EnumerateUpperMemoryBlocks()) { + AddBlockToDisplay(block, built, blocks, displayedHeaderSegments); + if (block.IsValid) { + DosMcbInfo info = built[^1]; + long sizeBytes = info.SizeBytes; + upperMemoryTotal += sizeBytes; + upperMemoryCount++; if (info.IsFree) { - ownerLabel = "(free)"; - } else if (string.IsNullOrEmpty(info.OwnerName)) { - ownerLabel = info.OwnerPspSegment; + upperMemoryFree += sizeBytes; + upperMemoryFreeCount++; + if (sizeBytes > largestUpperMemoryFree) { + largestUpperMemoryFree = sizeBytes; + } } else { - ownerLabel = info.OwnerName; + upperMemoryUsed += sizeBytes; } - string tip = $"seg:{info.HeaderSegment} size={sizeBytes:N0} bytes owner={ownerLabel}"; - blocks.Add(new DosMcbBarItem(info.HeaderSegment, sizeBytes, info.IsFree, info.IsLast, info.OwnerName, tip)); } if (!block.IsValid) { break; @@ -109,8 +148,14 @@ protected override void RefreshCore() { FreeConventionalBytes = free; UsedConventionalBytes = used; LargestFreeBlockBytes = largestFree; - TotalMcbCount = blocks.Count; + TotalMcbCount = conventionalCount; FreeMcbCount = freeCount; + TotalUpperMemoryBytes = upperMemoryTotal; + FreeUpperMemoryBytes = upperMemoryFree; + UsedUpperMemoryBytes = upperMemoryUsed; + LargestFreeUpperMemoryBlockBytes = largestUpperMemoryFree; + TotalUpperMemoryMcbCount = upperMemoryCount; + FreeUpperMemoryMcbCount = upperMemoryFreeCount; OnPropertyChanged(nameof(LayoutSummary)); if (previouslySelected is null) { if (Items.Count > 0) { @@ -129,4 +174,30 @@ protected override void RefreshCore() { } } } + + private static void AddBlockToDisplay(DosMemoryControlBlock block, List items, + List blocks, HashSet displayedHeaderSegments) { + DosMcbInfo info = new(); + block.CopyToDosMcbInfo(info); + if (!displayedHeaderSegments.Add(info.HeaderSegment)) { + return; + } + + items.Add(info); + if (!block.IsValid) { + return; + } + + string ownerLabel; + if (info.IsFree) { + ownerLabel = "(free)"; + } else if (string.IsNullOrEmpty(info.OwnerName)) { + ownerLabel = info.OwnerPspSegment; + } else { + ownerLabel = info.OwnerName; + } + string tip = $"seg:{info.HeaderSegment} size={info.SizeBytes:N0} bytes owner={ownerLabel}"; + blocks.Add(new DosMcbBarItem(info.HeaderSegment, info.SizeBytes, info.IsFree, info.IsLast, + info.OwnerName, tip)); + } } diff --git a/src/Spice86/ViewModels/DosMemoryOverviewViewModel.cs b/src/Spice86/ViewModels/DosMemoryOverviewViewModel.cs index efeabfd561..d91b9736d5 100644 --- a/src/Spice86/ViewModels/DosMemoryOverviewViewModel.cs +++ b/src/Spice86/ViewModels/DosMemoryOverviewViewModel.cs @@ -103,6 +103,7 @@ public DosMemoryOverviewViewModel(DosMemoryManager memoryManager, /// protected override void RefreshCore() { RefreshConventional(out long convTotal, out long convUsed, out long convFree); + RefreshUpperMemory(out long umbTotal, out long umbUsed, out long umbFree); long xmsTotal = 0; long xmsFree = 0; @@ -132,12 +133,19 @@ protected override void RefreshCore() { Rows.Clear(); Rows.Add(new MemoryReportRow { - Category = "Conventional", + Category = "Conventional memory", TotalBytes = convTotal, UsedBytes = convUsed, FreeBytes = convFree, Notes = $"Largest free MCB: {LargestFreeConventionalBytes:N0} bytes" }); + Rows.Add(new MemoryReportRow { + Category = "Upper memory (UMB)", + TotalBytes = umbTotal, + UsedBytes = umbUsed, + FreeBytes = umbFree, + Notes = "MCBs excluded from conventional memory" + }); long hmaUsed; long hmaFree; @@ -196,14 +204,14 @@ protected override void RefreshCore() { Notes = emsNotes }); - long allTotal = convTotal + hmaTotal + xmsTotal + emsTotal; - long allFree = convFree + hmaFree + xmsFree + emsFree; + long allTotal = convTotal + umbTotal + hmaTotal + xmsTotal + emsTotal; + long allFree = convFree + umbFree + hmaFree + xmsFree + emsFree; Rows.Add(new MemoryReportRow { - Category = "Total", + Category = "System total", TotalBytes = allTotal, UsedBytes = allTotal - allFree, FreeBytes = allFree, - Notes = string.Empty + Notes = "Includes conventional, UMB, HMA, XMS, and EMS" }); XmsLoaded = xmsEnabled; @@ -223,7 +231,7 @@ private void RefreshConventional(out long total, out long used, out long free) { int mcbs = 0; int frees = 0; - foreach (DosMemoryControlBlock block in _memoryManager.EnumerateBlocks()) { + foreach (DosMemoryControlBlock block in _memoryManager.EnumerateConventionalBlocks()) { if (!block.IsValid) { break; } @@ -265,4 +273,21 @@ private void RefreshConventional(out long total, out long used, out long free) { $"MCBs: {mcbs} ({frees} free) / Total: {total:N0} B / Used: {used:N0} B / Free: {free:N0} B / Largest free: {largestFree:N0} B"; SegmentsVersion++; } + + private void RefreshUpperMemory(out long total, out long used, out long free) { + total = 0; + free = 0; + foreach (DosMemoryControlBlock block in _memoryManager.EnumerateUpperMemoryBlocks()) { + if (!block.IsValid) { + break; + } + + long sizeBytes = block.AllocationSizeInBytes; + total += sizeBytes; + if (block.IsFree) { + free += sizeBytes; + } + } + used = total - free; + } } diff --git a/src/Spice86/ViewModels/MemoryReportRow.cs b/src/Spice86/ViewModels/MemoryReportRow.cs index 6d46e5fa96..05fb51213f 100644 --- a/src/Spice86/ViewModels/MemoryReportRow.cs +++ b/src/Spice86/ViewModels/MemoryReportRow.cs @@ -1,7 +1,7 @@ namespace Spice86.ViewModels; /// -/// One row of the DOS memory summary table (Conventional / HMA / XMS / EMS / total). +/// One row of the DOS memory summary table (conventional, UMB, HMA, XMS, EMS, or system total). /// public sealed class MemoryReportRow { /// Memory category label (e.g. "Conventional"). @@ -10,12 +10,21 @@ public sealed class MemoryReportRow { /// Total bytes available for the category. public long TotalBytes { get; init; } + /// Total kilobytes available for the category. + public double TotalKilobytes => TotalBytes / 1024.0; + /// Used bytes. public long UsedBytes { get; init; } + /// Used kilobytes. + public double UsedKilobytes => UsedBytes / 1024.0; + /// Free bytes. public long FreeBytes { get; init; } + /// Free kilobytes. + public double FreeKilobytes => FreeBytes / 1024.0; + /// Optional remark (driver status, HMA flag, etc.). public string Notes { get; init; } = string.Empty; } diff --git a/src/Spice86/ViewModels/PropertiesMappers/DosMapperExtensions.cs b/src/Spice86/ViewModels/PropertiesMappers/DosMapperExtensions.cs index c55fb61aa8..13fc5cd43b 100644 --- a/src/Spice86/ViewModels/PropertiesMappers/DosMapperExtensions.cs +++ b/src/Spice86/ViewModels/PropertiesMappers/DosMapperExtensions.cs @@ -29,7 +29,7 @@ public static void CopyToDosMemorySummaryInfo(this DosMemoryManager memoryManage long largestFreeBytes = 0; int mcbCount = 0; int freeMcbCount = 0; - foreach (DosMemoryControlBlock block in memoryManager.EnumerateBlocks()) { + foreach (DosMemoryControlBlock block in memoryManager.EnumerateConventionalBlocks()) { if (!block.IsValid) { break; } @@ -51,6 +51,33 @@ public static void CopyToDosMemorySummaryInfo(this DosMemoryManager memoryManage info.McbCount = mcbCount; info.FreeMcbCount = freeMcbCount; + long upperMemoryFreeBytes = 0; + long upperMemoryUsedBytes = 0; + long largestFreeUpperMemoryBlockBytes = 0; + int upperMemoryBlockCount = 0; + int freeUpperMemoryBlockCount = 0; + foreach (DosMemoryControlBlock block in memoryManager.EnumerateUpperMemoryBlocks()) { + if (!block.IsValid) { + break; + } + upperMemoryBlockCount++; + long sizeBytes = block.AllocationSizeInBytes; + if (block.IsFree) { + freeUpperMemoryBlockCount++; + upperMemoryFreeBytes += sizeBytes; + if (sizeBytes > largestFreeUpperMemoryBlockBytes) { + largestFreeUpperMemoryBlockBytes = sizeBytes; + } + } else { + upperMemoryUsedBytes += sizeBytes; + } + } + info.UpperMemoryFreeBytes = upperMemoryFreeBytes; + info.UpperMemoryUsedBytes = upperMemoryUsedBytes; + info.LargestFreeUpperMemoryBlockBytes = largestFreeUpperMemoryBlockBytes; + info.UpperMemoryBlockCount = upperMemoryBlockCount; + info.FreeUpperMemoryBlockCount = freeUpperMemoryBlockCount; + info.EmsEnabled = ems is not null; if (ems is not null) { info.EmsTotalPages = EmmMemory.TotalPages; diff --git a/src/Spice86/ViewModels/ValueViewModels/Debugging/DosMemorySummaryInfo.cs b/src/Spice86/ViewModels/ValueViewModels/Debugging/DosMemorySummaryInfo.cs index 00ef9502d6..45c0b5d964 100644 --- a/src/Spice86/ViewModels/ValueViewModels/Debugging/DosMemorySummaryInfo.cs +++ b/src/Spice86/ViewModels/ValueViewModels/Debugging/DosMemorySummaryInfo.cs @@ -38,6 +38,36 @@ public partial class DosMemorySummaryInfo : InspectorInfoBase { [property: DisplayName("Free MCBs")] private int _freeMcbCount; + [ObservableProperty] + [property: ReadOnly(true)] + [property: Category("UMB")] + [property: DisplayName("Free UMB (bytes)")] + private long _upperMemoryFreeBytes; + + [ObservableProperty] + [property: ReadOnly(true)] + [property: Category("UMB")] + [property: DisplayName("Used UMB (bytes)")] + private long _upperMemoryUsedBytes; + + [ObservableProperty] + [property: ReadOnly(true)] + [property: Category("UMB")] + [property: DisplayName("Largest free UMB (bytes)")] + private long _largestFreeUpperMemoryBlockBytes; + + [ObservableProperty] + [property: ReadOnly(true)] + [property: Category("UMB")] + [property: DisplayName("Total UMBs")] + private int _upperMemoryBlockCount; + + [ObservableProperty] + [property: ReadOnly(true)] + [property: Category("UMB")] + [property: DisplayName("Free UMBs")] + private int _freeUpperMemoryBlockCount; + [ObservableProperty] [property: ReadOnly(true)] [property: Category("EMS")] diff --git a/src/Spice86/Views/DosMemoryOverviewView.axaml b/src/Spice86/Views/DosMemoryOverviewView.axaml index d466649cf4..5a16be0c32 100644 --- a/src/Spice86/Views/DosMemoryOverviewView.axaml +++ b/src/Spice86/Views/DosMemoryOverviewView.axaml @@ -78,9 +78,9 @@ HeadersVisibility="Column"> - - - + + + diff --git a/tests/Spice86.Tests/CommandLineParserTests.cs b/tests/Spice86.Tests/CommandLineParserTests.cs index 6b4e3637fe..e5ed2bb7f2 100644 --- a/tests/Spice86.Tests/CommandLineParserTests.cs +++ b/tests/Spice86.Tests/CommandLineParserTests.cs @@ -67,6 +67,28 @@ public void ParseCommandLine_ShouldPreserveExeArgsAndNullableBooleans() { } } + [Fact] + public void ParseCommandLine_UmbDefaultsEnabledAndCanBeDisabled() { + // Arrange + string executablePath = CreateTemporaryExecutablePath(); + try { + string[] enabledArgs = ["-e", executablePath]; + string[] disabledArgs = ["-e", executablePath, "--Umb", "false"]; + + // Act + Configuration enabledConfiguration = Parse(enabledArgs) + ?? throw new InvalidOperationException("Configuration should be created with an executable path."); + Configuration disabledConfiguration = Parse(disabledArgs) + ?? throw new InvalidOperationException("Configuration should be created with an executable path."); + + // Assert + enabledConfiguration.Umb.Should().BeTrue(); + disabledConfiguration.Umb.Should().BeFalse(); + } finally { + File.Delete(executablePath); + } + } + [Fact] public void ParseCommandLine_ShouldApplyPostProcessingRules() { // Arrange diff --git a/tests/Spice86.Tests/Dos/DosInt21IntegrationTests.cs b/tests/Spice86.Tests/Dos/DosInt21IntegrationTests.cs index 6cab5a7e32..5f2a615223 100644 --- a/tests/Spice86.Tests/Dos/DosInt21IntegrationTests.cs +++ b/tests/Spice86.Tests/Dos/DosInt21IntegrationTests.cs @@ -35,6 +35,58 @@ public void GetDbcsLeadByteTable_WithAL0_ReturnsValidPointer() { AssertResourcePasses("dbcs_lead_byte_table_al0.com"); } + [Fact] + public void UmbChain_UsesSysVarsMcbChainHeadAndCanBeQueriedLinkedAndUnlinked() { + // Arrange + string resourceName = "umb_chain_link.com"; + + // Act + TestIoPortHandler testHandler = RunDosResource(resourceName, enableXms: true); + + // Assert + testHandler.Results.Should().Contain((byte)TestResult.Success); + testHandler.Results.Should().NotContain((byte)TestResult.Failure); + } + + [Fact] + public void UmbMcbSize_IsReducedWhenEmsIsEnabled() { + // Arrange + string resourceName = "umb_ems_size.com"; + + // Act + TestIoPortHandler testHandler = RunDosResource(resourceName, true, true, true); + + // Assert + testHandler.Results.Should().Contain((byte)TestResult.Success); + testHandler.Results.Should().NotContain((byte)TestResult.Failure); + } + + [Fact] + public void UmbDisabled_ReportsNoChainAndRejectsLinking() { + // Arrange + string resourceName = "umb_disabled.com"; + + // Act + TestIoPortHandler testHandler = RunDosResource(resourceName, true, false, false); + + // Assert + testHandler.Results.Should().Contain((byte)TestResult.Success); + testHandler.Results.Should().NotContain((byte)TestResult.Failure); + } + + [Fact] + public void UmbAllocation_UsesHighOnlyAndHighThenLowStrategies() { + // Arrange + string resourceName = "umb_alloc_strategy.com"; + + // Act + TestIoPortHandler testHandler = RunDosResource(resourceName, enableXms: true); + + // Assert + testHandler.Results.Should().Contain((byte)TestResult.Success); + testHandler.Results.Should().NotContain((byte)TestResult.Failure); + } + [Fact] public void GetDbcsLeadByteTable_WithInvalidAL_ReturnsError() { AssertResourcePasses("dbcs_lead_byte_table_invalid_al.com"); @@ -413,6 +465,26 @@ private static TestIoPortHandler RunDosResource(string resourceName, Action? keyInjectionAction = null, Action? preRunSetup = null, Action? fileSystemSetup = null) { + return RunDosResource(resourceName, keyInjectionAction, preRunSetup, fileSystemSetup, + enableXms: false, enableEms: false, enableUmb: true); + } + + private static TestIoPortHandler RunDosResource(string resourceName, bool enableXms) { + return RunDosResource(resourceName, null, null, null, enableXms, enableEms: false, enableUmb: true); + } + + private static TestIoPortHandler RunDosResource(string resourceName, bool enableXms, bool enableEms, + bool enableUmb) { + return RunDosResource(resourceName, null, null, null, enableXms, enableEms, enableUmb); + } + + private static TestIoPortHandler RunDosResource(string resourceName, + Action? keyInjectionAction, + Action? preRunSetup, + Action? fileSystemSetup, + bool enableXms, + bool enableEms, + bool enableUmb) { string resourcePath = Path.Join(AppContext.BaseDirectory, "Resources", "DosInt21Tests", resourceName); if (!string.Equals(Path.GetExtension(resourcePath), ".com", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("DOS INT 21h resource tests require a DOS COM program.", nameof(resourceName)); @@ -437,9 +509,12 @@ private static TestIoPortHandler RunDosResource(string resourceName, maxCycles: 100000L, installInterruptVectors: true, enableA20Gate: true, + enableEms: enableEms, + enableXms: enableXms, cDrive: cDrive, instructionTimeScale: instructionsPerSecond ); + creator.EnableUmb = enableUmb; using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); if (keyInjectionAction is not null) { diff --git a/tests/Spice86.Tests/Dos/DosMemoryManagerTest.cs b/tests/Spice86.Tests/Dos/DosMemoryManagerTest.cs index 6e0bdd087a..f744fbcdae 100644 --- a/tests/Spice86.Tests/Dos/DosMemoryManagerTest.cs +++ b/tests/Spice86.Tests/Dos/DosMemoryManagerTest.cs @@ -6,11 +6,13 @@ namespace Spice86.Tests.Dos; using NSubstitute; +using Spice86.Core.CLI.RuntimeOptions; using Spice86.Core.Emulator.LoadableFile.Dos; using Spice86.Core.Emulator.Memory; using Spice86.Core.Emulator.Memory.Mmu; using Spice86.Core.Emulator.Memory.ReaderWriter; using Spice86.Core.Emulator.OperatingSystem; +using Spice86.Core.Emulator.OperatingSystem.Devices; using Spice86.Core.Emulator.OperatingSystem.Enums; using Spice86.Core.Emulator.OperatingSystem.Structures; using Spice86.Core.Emulator.VM.Breakpoint; @@ -28,6 +30,7 @@ public class DosMemoryManagerTests { // Dependencies needed to construct DosMemoryManager private readonly ILogger _loggerService; private readonly IMemory _memory; + private readonly DosSysVars _dosSysVars; /// /// The initial PSP segment used for tests (PSP is 16 paragraphs before entry point). @@ -74,8 +77,13 @@ public DosMemoryManagerTests() { DosSwappableDataArea dosSwappableDataArea = new(_memory, MemoryUtils.ToPhysicalAddress(DosSwappableDataArea.BaseSegment, 0)); dosSwappableDataArea.CurrentProgramSegmentPrefix = _initialPspSegment; - // Arrange - _memoryManager = new DosMemoryManager(_memory, _initialPspSegment, _loggerService); + DosOptions options = new(null, string.Empty, programEntryPointSegment, false, false, false, + new DosRuntimeState(null)); + NullDevice nullDevice = new(_loggerService, _memory, 0); + _dosSysVars = new DosSysVars(options, nullDevice, _memory, + MemoryUtils.ToPhysicalAddress(DosSysVars.Segment, 0)); + _memoryManager = new DosMemoryManager(_memory, _initialPspSegment, _loggerService, + _dosSysVars, false, false); } /// @@ -158,6 +166,56 @@ public void FindLargestFreeAtCreation() { block.DataBlockSegment.Should().Be(0xFF0); block.Size.Should().Be(36880); block.AllocationSizeInBytes.Should().Be(590080); + _dosSysVars.FirstMCB.Should().Be(0xFEF); + _dosSysVars.MemAllocScanStart.Should().Be(0xFEF); + _dosSysVars.StartOfUMBChain.Should().Be(0xFFFF); + _dosSysVars.ChainingUMB.Should().Be(0); + } + + /// + /// Ensures that inspecting UMBs exposes allocatable upper-memory blocks without linking the + /// conventional chain or including its bridge MCB. + /// + [Fact] + public void EnumerateUpperMemoryBlocksExcludesBridgeWithoutLinkingChain() { + // Arrange + DosMemoryManager upperMemoryManager = new(_memory, _initialPspSegment, _loggerService, + _dosSysVars, true, false); + List upperMemoryBlocks = new(); + + // Act + foreach (DosMemoryControlBlock block in upperMemoryManager.EnumerateUpperMemoryBlocks()) { + upperMemoryBlocks.Add(block); + } + + // Assert + upperMemoryBlocks.Should().ContainSingle(); + DosMemoryControlBlock firstUpperMemoryBlock = upperMemoryBlocks[0]; + firstUpperMemoryBlock.DataBlockSegment.Should().Be(0xD001); + firstUpperMemoryBlock.IsFree.Should().BeTrue(); + firstUpperMemoryBlock.IsLast.Should().BeTrue(); + _dosSysVars.ChainingUMB.Should().Be(0); + } + + /// + /// Ensures conventional enumeration does not follow a linked UMB chain or include its bridge. + /// + [Fact] + public void EnumerateConventionalBlocksExcludesLinkedUmbChain() { + // Arrange + DosMemoryManager upperMemoryManager = new(_memory, _initialPspSegment, _loggerService, + _dosSysVars, true, false); + List unlinkedBlocks = upperMemoryManager.EnumerateConventionalBlocks().ToList(); + + // Act + upperMemoryManager.SetUmbChainLinkState(1).Should().BeTrue(); + List linkedBlocks = upperMemoryManager.EnumerateConventionalBlocks().ToList(); + + // Assert + unlinkedBlocks.Should().ContainSingle(); + linkedBlocks.Should().ContainSingle(); + linkedBlocks[0].DataBlockSegment.Should().Be(unlinkedBlocks[0].DataBlockSegment); + linkedBlocks[0].DataBlockSegment.Should().NotBe(0xA000); } /// @@ -1191,17 +1249,27 @@ public void InvalidAllocationStrategyBits2To5SetIsIgnored() { } /// - /// Ensures that setting an invalid allocation strategy (invalid high memory bits) is ignored. + /// Ensures that DOSBox-compatible high-memory strategy bits are accepted. /// [Fact] - public void InvalidAllocationStrategyHighMemBitsIsIgnored() { - // Arrange - DosMemoryAllocationStrategy originalStrategy = _memoryManager.AllocationStrategy; - - // Act - try to set invalid high memory bits (0xC0 - both bits 6 and 7 set) + public void AllocationStrategyHighMemBitsAreAccepted() { + // Act - both high-memory strategy bits are accepted by DOSBox. _memoryManager.AllocationStrategy = (DosMemoryAllocationStrategy)0xC0; - // Assert - should remain unchanged - _memoryManager.AllocationStrategy.Should().Be(originalStrategy); + // Assert + _memoryManager.AllocationStrategyValue.Should().Be((ushort)0xC0); + } + + /// + /// Ensures that upper allocation strategy bits are accepted and preserved. + /// + [Fact] + public void AllocationStrategyHighByteIsAcceptedAndPreserved() { + // Act + bool accepted = _memoryManager.TrySetAllocationStrategy(0xAB00); + + // Assert + accepted.Should().BeTrue(); + _memoryManager.AllocationStrategyValue.Should().Be((ushort)0xAB00); } } \ No newline at end of file diff --git a/tests/Spice86.Tests/Dos/DosProcessManagerTests.cs b/tests/Spice86.Tests/Dos/DosProcessManagerTests.cs index 15f5320292..f4feeae04b 100644 --- a/tests/Spice86.Tests/Dos/DosProcessManagerTests.cs +++ b/tests/Spice86.Tests/Dos/DosProcessManagerTests.cs @@ -387,7 +387,9 @@ private static DosProcessManagerTestContext CreateContext(ushort? programEntryPo VgaFunctionality vgaFunctionality = new(memory, interruptVectorTable, ioPortDispatcher, biosDataArea, vgaRom, true); DosDriveManager driveManager = DosTestHelpers.CreateDriveManager(loggerService, null); - DosMemoryManager memoryManager = new(memory, initialPspSegment, loggerService); + DosSysVars dosSysVars = DosTestHelpers.CreateDosSysVars(memory, loggerService, + configuration.ProgramEntryPointSegment); + DosMemoryManager memoryManager = new(memory, initialPspSegment, loggerService, dosSysVars, false, false); DosTables dosTables = new(memory); DosCodePageState dosCodePageState = DosCodePageState.Create(850, CountryId.UnitedStates); DosFileManager fileManager = new(memory, new DosStringDecoder(memory, state, dosCodePageState), driveManager, loggerService, new List()); diff --git a/tests/Spice86.Tests/Dos/DosTestHelpers.cs b/tests/Spice86.Tests/Dos/DosTestHelpers.cs index 4b2cfa12ec..3d85b62b65 100644 --- a/tests/Spice86.Tests/Dos/DosTestHelpers.cs +++ b/tests/Spice86.Tests/Dos/DosTestHelpers.cs @@ -4,6 +4,7 @@ namespace Spice86.Tests.Dos; using NSubstitute; +using Spice86.Core.CLI.RuntimeOptions; using Spice86.Core.Emulator.CPU; using Spice86.Core.Emulator.Devices.Sound; using Spice86.Core.Emulator.InterruptHandlers.Mscdex; @@ -11,9 +12,11 @@ namespace Spice86.Tests.Dos; using Spice86.Core.Emulator.Memory.Mmu; using Spice86.Core.Emulator.Memory.ReaderWriter; using Spice86.Core.Emulator.OperatingSystem; +using Spice86.Core.Emulator.OperatingSystem.Devices; using Spice86.Core.Emulator.OperatingSystem.Structures; using Spice86.Core.Emulator.VM; using Spice86.Shared.Interfaces; +using Spice86.Shared.Utils; /// /// Factory helpers shared across DOS unit tests. @@ -44,4 +47,15 @@ internal static DosDriveManager CreateDriveManager(ILogger logger, string? cDriv internal static DosDriveManager CreateDriveManager(string? cDrive, string? exe = null) { return CreateDriveManager(Substitute.For(), cDrive, exe); } + + /// + /// Creates DOS system variables for a memory manager test setup. + /// + internal static DosSysVars CreateDosSysVars(IMemory memory, ILogger logger, ushort programEntryPointSegment) { + DosOptions options = new(null, string.Empty, programEntryPointSegment, false, false, false, + new DosRuntimeState(null)); + NullDevice nullDevice = new(logger, memory, 0); + return new DosSysVars(options, nullDevice, memory, + MemoryUtils.ToPhysicalAddress(DosSysVars.Segment, 0)); + } } diff --git a/tests/Spice86.Tests/Dos/Xms/Xms32BitUnitTests.cs b/tests/Spice86.Tests/Dos/Xms/Xms32BitUnitTests.cs index 0704bd5c1e..7eaa9282e7 100644 --- a/tests/Spice86.Tests/Dos/Xms/Xms32BitUnitTests.cs +++ b/tests/Spice86.Tests/Dos/Xms/Xms32BitUnitTests.cs @@ -12,8 +12,10 @@ using Spice86.Core.Emulator.InterruptHandlers.Dos.Xms; using Spice86.Core.Emulator.Memory; using Spice86.Core.Emulator.Memory.Mmu; +using Spice86.Core.Emulator.OperatingSystem; using Spice86.Core.Emulator.OperatingSystem.Structures; using Spice86.Shared.Interfaces; +using Spice86.Tests.Dos; using Xunit; @@ -40,10 +42,12 @@ public Xms32BitFunctionsTests() { _callbackHandler = new CallbackHandler(_state, _loggerService); _dosTables = new DosTables(_memory); _asmWriter = new MemoryAsmWriter(_memory, new(0, 0), _callbackHandler); + DosSysVars dosSysVars = DosTestHelpers.CreateDosSysVars(_memory, _loggerService, 0x1000); + DosMemoryManager dosMemoryManager = new(_memory, 0x1000, _loggerService, dosSysVars, false, false); // Create XMS manager _xms = new ExtendedMemoryManager(_memory, _state, _a20Gate, _asmWriter, - _dosTables, _loggerService); + _dosTables, dosMemoryManager, _loggerService); } [Fact] diff --git a/tests/Spice86.Tests/Dos/Xms/XmsIntegrationTests.cs b/tests/Spice86.Tests/Dos/Xms/XmsIntegrationTests.cs index 59f8c47581..2997b46f7f 100644 --- a/tests/Spice86.Tests/Dos/Xms/XmsIntegrationTests.cs +++ b/tests/Spice86.Tests/Dos/Xms/XmsIntegrationTests.cs @@ -11,10 +11,8 @@ namespace Spice86.Tests.Dos.Xms; /// Integration tests for XMS functionality that run machine code through the emulation stack, /// similar to how real programs like HITEST.ASM interact with the XMS driver. /// -public class XmsIntegrationTests -{ - enum TestResult : byte - { +public class XmsIntegrationTests { + enum TestResult : byte { Success = 0x00, Failure = 0xFF } @@ -23,24 +21,56 @@ enum TestResult : byte /// Tests XMS installation check via INT 2Fh, AH=43h, AL=00h /// [Fact] - public void XmsInstallationCheck_ShouldBeInstalled() - { - AssertXmsResourcePasses("xms_installation_check.com"); + public void XmsInstallationCheck_ShouldBeInstalled() { + // Arrange + string resourceName = "xms_installation_check.com"; + + // Act + TestIoPortHandler testHandler = RunXmsResource(resourceName, false); + + // Assert + testHandler.Results.Should().Contain((byte)TestResult.Success); + testHandler.Results.Should().NotContain((byte)TestResult.Failure); } /// /// Tests XMS entry point retrieval via INT 2Fh, AH=43h, AL=10h /// [Fact] - public void GetXmsEntryPoint_ShouldReturnValidAddress() - { - AssertXmsResourcePasses("xms_entry_point.com"); + public void GetXmsEntryPoint_ShouldReturnValidAddress() { + // Arrange + string resourceName = "xms_entry_point.com"; + + // Act + TestIoPortHandler testHandler = RunXmsResource(resourceName, false); + + // Assert + testHandler.Results.Should().Contain((byte)TestResult.Success); + testHandler.Results.Should().NotContain((byte)TestResult.Failure); } - private void AssertXmsResourcePasses(string resourceName) - { - TestIoPortHandler testHandler = RunXmsResource(resourceName, enableA20Gate: false); + [Fact] + public void RequestAndReleaseUmb_RestoresDosAllocationState() { + // Arrange + string resourceName = "xms_request_release_umb.com"; + + // Act + TestIoPortHandler testHandler = RunXmsResource(resourceName, false); + // Assert + testHandler.Results.Should().Contain((byte)TestResult.Success); + testHandler.Results.Should().NotContain((byte)TestResult.Failure); + } + + [Fact] + public void RequestUmb_WhenDisabled_ReturnsNotImplemented() { + // Arrange + string resourceName = "xms_umb_disabled.com"; + + // Act + TestIoPortHandler testHandler = RunXmsResource(resourceName, false, false, false); + + // Assert testHandler.Results.Should().Contain((byte)TestResult.Success); testHandler.Results.Should().NotContain((byte)TestResult.Failure); } @@ -48,8 +78,12 @@ private void AssertXmsResourcePasses(string resourceName) /// /// Runs the XMS test program and returns a test handler with results /// - private TestIoPortHandler RunXmsResource(string resourceName, bool enableA20Gate) - { + private TestIoPortHandler RunXmsResource(string resourceName, bool enableA20Gate) { + return RunXmsResource(resourceName, enableA20Gate, false, true); + } + + private TestIoPortHandler RunXmsResource(string resourceName, bool enableA20Gate, bool enableEms, + bool enableUmb) { string filePath = Path.Join(AppContext.BaseDirectory, "Resources", "XmsTests", resourceName); if (!string.Equals(Path.GetExtension(filePath), ".com", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("XMS resource tests require a DOS COM program.", nameof(resourceName)); @@ -61,8 +95,10 @@ private TestIoPortHandler RunXmsResource(string resourceName, bool enableA20Gate maxCycles: 100000L, installInterruptVectors: true, enableA20Gate: enableA20Gate, + enableEms: enableEms, enableXms: true ); + creator.EnableUmb = enableUmb; using Spice86DependencyInjection spice86DependencyInjection = creator.Create(); TestIoPortHandler testHandler = new( diff --git a/tests/Spice86.Tests/Dos/Xms/XmsUnitTests.cs b/tests/Spice86.Tests/Dos/Xms/XmsUnitTests.cs index 2d1bcf3ec3..f895a0c2e9 100644 --- a/tests/Spice86.Tests/Dos/Xms/XmsUnitTests.cs +++ b/tests/Spice86.Tests/Dos/Xms/XmsUnitTests.cs @@ -12,9 +12,11 @@ using Spice86.Core.Emulator.InterruptHandlers.Dos.Xms; using Spice86.Core.Emulator.Memory; using Spice86.Core.Emulator.Memory.Mmu; +using Spice86.Core.Emulator.OperatingSystem; using Spice86.Core.Emulator.OperatingSystem.Structures; using Spice86.Shared.Interfaces; using Spice86.Shared.Utils; +using Spice86.Tests.Dos; using Xunit; @@ -42,10 +44,12 @@ public XmsUnitTests() { _callbackHandler = new CallbackHandler(_state, _loggerService); _dosTables = new DosTables(_memory); _asmWriter = new MemoryAsmWriter(_memory, new(0, 0), _callbackHandler); + DosSysVars dosSysVars = DosTestHelpers.CreateDosSysVars(_memory, _loggerService, 0x1000); + DosMemoryManager dosMemoryManager = new(_memory, 0x1000, _loggerService, dosSysVars, false, false); // Create XMS manager _xms = new ExtendedMemoryManager(_memory, _state, _a20Gate, _asmWriter, - _dosTables, _loggerService); + _dosTables, dosMemoryManager, _loggerService); } [Fact] @@ -144,7 +148,10 @@ public void A20AlreadyEnabledAtStartup_ShouldPreventDisabling() { CallbackHandler callbackHandler = new CallbackHandler(state, loggerService); DosTables dosTables = new DosTables(memory); MemoryAsmWriter asmWriter = new MemoryAsmWriter(memory, new(0, 0), callbackHandler); - ExtendedMemoryManager xms = new ExtendedMemoryManager(memory, state, a20Gate, asmWriter, dosTables, loggerService); + DosSysVars dosSysVars = DosTestHelpers.CreateDosSysVars(memory, loggerService, 0x1000); + DosMemoryManager dosMemoryManager = new(memory, 0x1000, loggerService, dosSysVars, false, false); + ExtendedMemoryManager xms = new ExtendedMemoryManager(memory, state, a20Gate, + asmWriter, dosTables, dosMemoryManager, loggerService); // Verify initial state a20Gate.IsEnabled.Should().BeTrue("A20 gate should be enabled at startup"); diff --git a/tests/Spice86.Tests/McpServerToolStateTests.cs b/tests/Spice86.Tests/McpServerToolStateTests.cs index 5346dd2039..40ee590675 100644 --- a/tests/Spice86.Tests/McpServerToolStateTests.cs +++ b/tests/Spice86.Tests/McpServerToolStateTests.cs @@ -19,6 +19,25 @@ namespace Spice86.Tests; public class McpServerToolStateTests { private const string TestProgramName = "add"; + [Fact] + public async Task DosAndMcpServices_ShouldShareEmsAndXmsManagersAsync() { + // Arrange + await using McpIntegrationContext context = await McpIntegrationContext.CreateAsync( + TestProgramName, enableXms: true, enableEms: true, initializeDos: true); + + // Act + Spice86.Core.Emulator.OperatingSystem.Dos dos = context.Services.Dos + ?? throw new InvalidOperationException("DOS services were not initialized."); + ExpandedMemoryManager? emsManager = context.Services.EmsManager; + ExtendedMemoryManager? xmsManager = context.Services.XmsManager; + + // Assert + context.Services.EmsManager.Should().NotBeNull(); + emsManager.Should().BeSameAs(dos.Ems); + context.Services.XmsManager.Should().NotBeNull(); + xmsManager.Should().BeSameAs(dos.Xms); + } + [Fact] public async Task McpAbout_ShouldExposeDiscoverabilityMetadataAsync() { await using McpIntegrationContext context = await McpIntegrationContext.CreateAsync(TestProgramName); diff --git a/tests/Spice86.Tests/Resources/DosInt21Tests/umb_alloc_strategy.asm b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_alloc_strategy.asm new file mode 100644 index 0000000000..35e8d5000d --- /dev/null +++ b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_alloc_strategy.asm @@ -0,0 +1,98 @@ +; Build from WSL: nasm -f bin umb_alloc_strategy.asm -o umb_alloc_strategy.com +bits 16 +org 100h + +result_port equ 0999h +success equ 00h +failure equ 0FFh + +start: + push cs + pop es + mov ax, 4A00h + mov bx, 1000h + int 21h + jc failed + + mov ax, 5801h + mov bx, 0040h + int 21h + jc failed + + mov ax, 4800h + mov bx, 2000h + int 21h + jnc failed + cmp bx, 1FFFh + jne failed + + mov ax, 4800h + mov bx, 1FFFh + int 21h + jc failed + cmp ax, 0D001h + jne failed + mov [umb_segment], ax + + mov ax, 5801h + mov bx, 0080h + int 21h + jc failed + + mov ax, 4800h + mov bx, 2000h + int 21h + jc failed + cmp ax, 0A000h + jae failed + mov [low_segment], ax + + mov ax, 5801h + mov bx, 00C3h + int 21h + jnc failed + cmp ax, 0001h + jne failed + + mov ax, 5801h + mov bx, 00C0h + int 21h + jc failed + + mov ax, 4800h + mov bx, 0100h + int 21h + jc failed + cmp ax, 0A000h + jae failed + mov [fallback_segment], ax + + mov ax, 4900h + mov es, [fallback_segment] + int 21h + jc failed + + mov ax, 4900h + mov es, [low_segment] + int 21h + jc failed + + mov ax, 4900h + mov es, [umb_segment] + int 21h + jc failed + + mov al, success + jmp report + +failed: + mov al, failure + +report: + mov dx, result_port + out dx, al + hlt + +umb_segment dw 0 +low_segment dw 0 +fallback_segment dw 0 diff --git a/tests/Spice86.Tests/Resources/DosInt21Tests/umb_alloc_strategy.com b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_alloc_strategy.com new file mode 100644 index 0000000000..c9fb32575c Binary files /dev/null and b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_alloc_strategy.com differ diff --git a/tests/Spice86.Tests/Resources/DosInt21Tests/umb_chain_link.asm b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_chain_link.asm new file mode 100644 index 0000000000..ac49c30d6e --- /dev/null +++ b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_chain_link.asm @@ -0,0 +1,126 @@ +; Build from WSL: nasm -f bin umb_chain_link.asm -o umb_chain_link.com +bits 16 +org 100h + +result_port equ 0999h +success equ 00h +failure equ 0FFh + +start: + mov ax, 0040h + mov es, ax + cmp word [es:0013h], 0280h + jne failed + + mov ax, 5200h + int 21h + mov dx, [es:bx - 2] + cmp dx, 015Fh + jne failed + cmp word [es:bx + 68h], dx + jne failed + mov es, dx + cmp byte [es:0], 4Dh + je sysvars_mcb_is_valid + cmp byte [es:0], 5Ah + jne failed + +sysvars_mcb_is_valid: + + mov ax, 5200h + int 21h + cmp word [es:bx + 66h], 9FFFh + jne failed + test byte [es:bx + 63h], 01h + jnz failed + + mov ax, 9FFFh + mov es, ax + cmp byte [es:0], 4Dh + jne failed + cmp word [es:1], 0008h + jne failed + cmp word [es:3], 3000h + jne failed + cmp byte [es:8], 'S' + jne failed + cmp byte [es:9], 'C' + jne failed + cmp byte [es:15], ' ' + jne failed + + mov ax, 0D000h + mov es, ax + cmp byte [es:0], 5Ah + jne failed + cmp word [es:1], 0000h + jne failed + cmp word [es:3], 1FFFh + jne failed + + mov ax, 5800h + int 21h + cmp ax, 0000h + jne failed + + mov ax, 5802h + int 21h + jc failed + cmp al, 00h + jne failed + + mov ax, 5803h + mov bx, 0001h + int 21h + jc failed + + mov ax, 5200h + int 21h + mov ax, [es:bx - 2] + mov es, ax + mov cx, 20h + +find_umb_bridge: + mov ax, es + cmp ax, 9FFFh + je found_umb_bridge + cmp byte [es:0], 4Dh + jne failed + mov dx, es + add dx, [es:3] + inc dx + mov es, dx + loop find_umb_bridge + jmp failed + +found_umb_bridge: + cmp byte [es:0], 4Dh + jne failed + + mov ax, 5802h + int 21h + jc failed + cmp al, 01h + jne failed + + mov ax, 5803h + xor bx, bx + int 21h + jc failed + + mov ax, 5802h + int 21h + jc failed + cmp al, 00h + jne failed + + mov al, success + jmp report + +failed: + mov al, failure + +report: + mov dx, result_port + out dx, al + hlt \ No newline at end of file diff --git a/tests/Spice86.Tests/Resources/DosInt21Tests/umb_chain_link.com b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_chain_link.com new file mode 100644 index 0000000000..c6838731fd Binary files /dev/null and b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_chain_link.com differ diff --git a/tests/Spice86.Tests/Resources/DosInt21Tests/umb_disabled.asm b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_disabled.asm new file mode 100644 index 0000000000..fdc99f868c --- /dev/null +++ b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_disabled.asm @@ -0,0 +1,39 @@ +; Build from WSL: nasm -f bin umb_disabled.asm -o umb_disabled.com +bits 16 +org 100h + +result_port equ 0999h +success equ 00h +failure equ 0FFh + +start: + mov ax, 5200h + int 21h + cmp word [es:bx + 66h], 0FFFFh + jne failed + test byte [es:bx + 63h], 01h + jnz failed + + mov ax, 5803h + mov bx, 0001h + int 21h + jnc failed + cmp ax, 0001h + jne failed + + mov ax, 5802h + int 21h + jc failed + cmp al, 00h + jne failed + + mov al, success + jmp report + +failed: + mov al, failure + +report: + mov dx, result_port + out dx, al + hlt \ No newline at end of file diff --git a/tests/Spice86.Tests/Resources/DosInt21Tests/umb_disabled.com b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_disabled.com new file mode 100644 index 0000000000..b29f351ec4 Binary files /dev/null and b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_disabled.com differ diff --git a/tests/Spice86.Tests/Resources/DosInt21Tests/umb_ems_size.asm b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_ems_size.asm new file mode 100644 index 0000000000..48ab2cabac --- /dev/null +++ b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_ems_size.asm @@ -0,0 +1,31 @@ +; Build from WSL: nasm -f bin umb_ems_size.asm -o umb_ems_size.com +bits 16 +org 100h + +result_port equ 0999h +success equ 00h +failure equ 0FFh + +start: + mov ax, 5200h + int 21h + cmp word [es:bx + 66h], 9FFFh + jne failed + + mov ax, 0D000h + mov es, ax + cmp byte [es:0], 5Ah + jne failed + cmp word [es:3], 0FFFh + jne failed + + mov al, success + jmp report + +failed: + mov al, failure + +report: + mov dx, result_port + out dx, al + hlt \ No newline at end of file diff --git a/tests/Spice86.Tests/Resources/DosInt21Tests/umb_ems_size.com b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_ems_size.com new file mode 100644 index 0000000000..0d6938ad2b Binary files /dev/null and b/tests/Spice86.Tests/Resources/DosInt21Tests/umb_ems_size.com differ diff --git a/tests/Spice86.Tests/Resources/XmsTests/xms_request_release_umb.asm b/tests/Spice86.Tests/Resources/XmsTests/xms_request_release_umb.asm new file mode 100644 index 0000000000..e46a9b3566 --- /dev/null +++ b/tests/Spice86.Tests/Resources/XmsTests/xms_request_release_umb.asm @@ -0,0 +1,127 @@ +; Build from WSL: nasm -f bin xms_request_release_umb.asm -o xms_request_release_umb.com +bits 16 +org 100h + +result_port equ 0999h +success equ 00h +failure equ 0FFh + +start: + mov byte [stage], 1 + mov ax, 4310h + int 2Fh + mov [xms_entry], bx + mov [xms_entry + 2], es + + mov ah, 10h + mov dx, 2000h + call far [xms_entry] + cmp ax, 0000h + jne failed + cmp bl, 0B0h + jne failed + cmp dx, 1FFFh + jne failed + + mov byte [stage], 2 + mov ax, 5800h + int 21h + cmp ax, 0000h + jne failed + mov ax, 5802h + int 21h + jc failed + cmp al, 00h + jne failed + + mov byte [stage], 3 + mov ah, 10h + mov dx, 1FFFh + call far [xms_entry] + cmp ax, 0001h + jne failed + mov [umb_segment], bx + cmp bx, 0D001h + jne failed + + mov ah, 10h + mov dx, 0001h + call far [xms_entry] + cmp ax, 0000h + jne failed + cmp bl, 0B1h + jne failed + cmp dx, 0000h + jne failed + + mov byte [stage], 4 + mov ax, 5800h + int 21h + cmp ax, 0000h + jne failed + mov ax, 5802h + int 21h + jc failed + cmp al, 00h + jne failed + + mov byte [stage], 5 + mov ah, 11h + mov dx, [umb_segment] + call far [xms_entry] + cmp ax, 0001h + jne failed + + mov ax, 5800h + int 21h + cmp ax, 0000h + jne failed + mov ax, 5802h + int 21h + jc failed + cmp al, 00h + jne failed + + mov ah, 11h + mov dx, [umb_segment] + call far [xms_entry] + cmp ax, 0000h + jne failed + cmp bl, 0B2h + jne failed + + mov byte [stage], 6 + mov ah, 12h + mov bx, 0100h + mov dx, [umb_segment] + call far [xms_entry] + cmp ax, 0000h + jne failed + cmp bl, 80h + jne failed + + mov byte [stage], 7 + mov ax, 5800h + int 21h + cmp ax, 0000h + jne failed + mov ax, 5802h + int 21h + jc failed + cmp al, 00h + jne failed + + mov al, success + jmp report + +failed: + mov al, [stage] + +report: + mov dx, result_port + out dx, al + hlt + +xms_entry dw 0, 0 +umb_segment dw 0 +stage db 0 \ No newline at end of file diff --git a/tests/Spice86.Tests/Resources/XmsTests/xms_request_release_umb.com b/tests/Spice86.Tests/Resources/XmsTests/xms_request_release_umb.com new file mode 100644 index 0000000000..a1cccbf901 Binary files /dev/null and b/tests/Spice86.Tests/Resources/XmsTests/xms_request_release_umb.com differ diff --git a/tests/Spice86.Tests/Resources/XmsTests/xms_umb_disabled.asm b/tests/Spice86.Tests/Resources/XmsTests/xms_umb_disabled.asm new file mode 100644 index 0000000000..5a5a0924de --- /dev/null +++ b/tests/Spice86.Tests/Resources/XmsTests/xms_umb_disabled.asm @@ -0,0 +1,34 @@ +; Build from WSL: nasm -f bin xms_umb_disabled.asm -o xms_umb_disabled.com +bits 16 +org 100h + +result_port equ 0999h +success equ 00h +failure equ 0FFh + +start: + mov ax, 4310h + int 2Fh + mov [xms_entry], bx + mov [xms_entry + 2], es + + mov ah, 10h + mov dx, 0100h + call far [xms_entry] + cmp ax, 0000h + jne failed + cmp bl, 80h + jne failed + + mov al, success + jmp report + +failed: + mov al, failure + +report: + mov dx, result_port + out dx, al + hlt + +xms_entry dw 0, 0 \ No newline at end of file diff --git a/tests/Spice86.Tests/Resources/XmsTests/xms_umb_disabled.com b/tests/Spice86.Tests/Resources/XmsTests/xms_umb_disabled.com new file mode 100644 index 0000000000..827c795dc4 Binary files /dev/null and b/tests/Spice86.Tests/Resources/XmsTests/xms_umb_disabled.com differ diff --git a/tests/Spice86.Tests/Spice86Creator.cs b/tests/Spice86.Tests/Spice86Creator.cs index 2785f32ab7..a610243a4a 100644 --- a/tests/Spice86.Tests/Spice86Creator.cs +++ b/tests/Spice86.Tests/Spice86Creator.cs @@ -19,6 +19,8 @@ public sealed class Spice86Creator : IDisposable { private readonly string _exportFolder; private readonly bool _ownsExportFolder; + public bool EnableUmb { get; set; } = true; + public Spice86Creator(string binName, bool enablePit = false, long maxCycles = 100000, bool installInterruptVectors = false, bool failOnUnhandledPort = false, bool enableA20Gate = false, bool enableXms = false, bool enableEms = false, string? overrideSupplierClassName = null, string? cDrive = null, @@ -90,6 +92,7 @@ public Spice86Creator(string binName, bool enablePit = false, } public Spice86DependencyInjection Create() { + _configuration.Umb = EnableUmb; Spice86DependencyInjection res = new(_configuration); MachineLeakTracker.Track(res.Machine); res.Machine.CpuState.Flags.CpuModel = CpuModel.ZET_86; diff --git a/tests/Spice86.Tests/UI/DosMemoryUiTests.cs b/tests/Spice86.Tests/UI/DosMemoryUiTests.cs new file mode 100644 index 0000000000..b52c97a0f5 --- /dev/null +++ b/tests/Spice86.Tests/UI/DosMemoryUiTests.cs @@ -0,0 +1,119 @@ +namespace Spice86.Tests.UI; + +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using Avalonia.VisualTree; + +using FluentAssertions; + +using Spice86.Core.CLI.RuntimeOptions; +using Spice86.Core.Emulator.Memory; +using Spice86.Core.Emulator.OperatingSystem; +using Spice86.Core.Emulator.OperatingSystem.Devices; +using Spice86.Core.Emulator.OperatingSystem.Structures; +using Spice86.Core.Emulator.VM; +using Spice86.Shared.Interfaces; +using Spice86.Shared.Utils; +using Spice86.ViewModels; +using Spice86.Views; + +/// +/// Verifies DOS memory views render conventional and upper-memory data independently. +/// +public class DosMemoryUiTests : BreakpointUiTestBase { + /// + /// Verifies the MCB inspector renders allocatable UMBs once for either UMB chain state and the + /// system memory table keeps conventional and system totals distinct while displaying KB units. + /// + [AvaloniaFact] + public void DosMemoryViewsSeparateConventionalAndUpperMemory() { + // Arrange + ILogger loggerService = CreateMockLoggerService(); + DosMemoryManager memoryManager = CreateUpperMemoryManager(loggerService); + PauseHandler pauseHandler = CreatePauseHandler(loggerService); + pauseHandler.RequestPause(); + + // Act & Assert + AssertMcbView(memoryManager, pauseHandler); + memoryManager.SetUmbChainLinkState(1).Should().BeTrue(); + AssertMcbView(memoryManager, pauseHandler); + AssertMemoryOverview(memoryManager, pauseHandler); + } + + private static DosMemoryManager CreateUpperMemoryManager(ILogger loggerService) { + (Memory memory, _, _) = CreateMemory(); + const ushort programEntryPointSegment = 0x1000; + const ushort initialPspSegment = programEntryPointSegment - 0x10; + DosSwappableDataArea swappableDataArea = new(memory, + MemoryUtils.ToPhysicalAddress(DosSwappableDataArea.BaseSegment, 0)); + swappableDataArea.CurrentProgramSegmentPrefix = initialPspSegment; + DosOptions options = new(null, string.Empty, programEntryPointSegment, false, false, false, + new DosRuntimeState(null)); + NullDevice nullDevice = new(loggerService, memory, 0); + DosSysVars dosSysVars = new(options, nullDevice, memory, + MemoryUtils.ToPhysicalAddress(DosSysVars.Segment, 0)); + return new DosMemoryManager(memory, initialPspSegment, loggerService, dosSysVars, true, false); + } + + private static void AssertMcbView(DosMemoryManager memoryManager, PauseHandler pauseHandler) { + using DosMcbsViewModel viewModel = new(memoryManager, pauseHandler); + DosMcbsView view = new() { + DataContext = viewModel + }; + Window window = new() { + Width = 900, + Height = 600, + Content = view + }; + + ShowWindowAndWait(window); + + viewModel.Items.Should().HaveCount(2); + viewModel.Items.Select(static item => item.HeaderSegment).Should().NotContain("0x9FFF"); + viewModel.Blocks.Should().HaveCount(2); + viewModel.TotalMcbCount.Should().Be(1); + viewModel.TotalUpperMemoryMcbCount.Should().Be(1); + viewModel.LayoutSummary.Should().Contain("Conventional:"); + viewModel.LayoutSummary.Should().Contain("UMB:"); + Grid? barHost = view.FindControl("BarHost"); + barHost.Should().NotBeNull(); + if (barHost is null) { + throw new InvalidOperationException("BarHost was not found in the DOS MCB view."); + } + barHost.Children.Should().HaveCount(2); + + CloseWindowAndWait(window); + } + + private static void AssertMemoryOverview(DosMemoryManager memoryManager, PauseHandler pauseHandler) { + using DosMemoryOverviewViewModel viewModel = new(memoryManager, null, null, pauseHandler); + DosMemoryOverviewView view = new() { + DataContext = viewModel + }; + Window window = new() { + Width = 1000, + Height = 800, + Content = view + }; + + ShowWindowAndWait(window); + + MemoryReportRow conventionalMemory = viewModel.Rows.Single( + static row => row.Category == "Conventional memory"); + MemoryReportRow upperMemory = viewModel.Rows.Single( + static row => row.Category == "Upper memory (UMB)"); + MemoryReportRow systemTotal = viewModel.Rows.Single( + static row => row.Category == "System total"); + conventionalMemory.TotalKilobytes.Should().Be( + conventionalMemory.TotalBytes / 1024.0); + systemTotal.TotalBytes.Should().Be( + conventionalMemory.TotalBytes + upperMemory.TotalBytes); + DataGrid memorySummaryGrid = view.GetVisualDescendants().OfType().Single(); + IEnumerable headers = memorySummaryGrid.Columns.Select(static column => column.Header); + headers.Should().Contain("Total (KB)"); + headers.Should().Contain("Used (KB)"); + headers.Should().Contain("Free (KB)"); + + CloseWindowAndWait(window); + } +}