BizHawk/src/BizHawk.Emulation.Cores/Computers/DOS/DOSBox.cs

505 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using BizHawk.Common;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores.Properties;
using BizHawk.Emulation.Cores.Waterbox;
using BizHawk.Emulation.DiscSystem;
namespace BizHawk.Emulation.Cores.Computers.DOS
{
[PortedCore(
name: CoreNames.DOSBox,
author: "Jonathan Campbell et al.",
portedVersion: "2025.02.01 (324193b)",
portedUrl: "https://github.com/joncampbell123/dosbox-x",
isReleased: false)]
public partial class DOSBox : WaterboxCore
{
private static readonly Configuration DefaultConfig = new Configuration
{
SystemId = VSystemID.Raw.DOS,
MaxSamples = 8 * 1024,
DefaultWidth = LibDOSBox.VGA_MAX_WIDTH,
DefaultHeight = LibDOSBox.VGA_MAX_HEIGHT,
MaxWidth = LibDOSBox.SVGA_MAX_WIDTH,
MaxHeight = LibDOSBox.SVGA_MAX_HEIGHT,
DefaultFpsNumerator = LibDOSBox.VIDEO_NUMERATOR_DOS,
DefaultFpsDenominator = LibDOSBox.VIDEO_DENOMINATOR_DOS
};
private readonly List<IRomAsset> _floppyDiskAssets;
private readonly List<IDiscAsset> _discAssets;
// Drive management variables
private bool _nextFloppyDiskPressed = false;
private bool _nextCDROMPressed = false;
private List<IRomAsset> _floppyDiskImageFiles = new List<IRomAsset>();
private int _floppyDiskCount = 0;
private int _currentFloppyDisk = 0;
private int _currentCDROM = 0;
// VGA Refresh rate info
private ulong _VGARefreshRateNumerator = LibDOSBox.VIDEO_NUMERATOR_DOS;
private ulong _VGARefreshRateDenominator = LibDOSBox.VIDEO_DENOMINATOR_DOS;
private string GetFullName(IRomAsset rom) => rom.Game.Name + rom.Extension;
public override int VirtualWidth => BufferHeight * 4 / 3;
private LibDOSBox _libDOSBox;
// Image selection / swapping variables
[CoreConstructor(VSystemID.Raw.DOS)]
public DOSBox(CoreLoadParameters<object, SyncSettings> lp)
: base(lp.Comm, DefaultConfig)
{
_floppyDiskAssets = lp.Roms;
_discAssets = lp.Discs;
_syncSettings = lp.SyncSettings ?? new();
VsyncNumerator = (int) _syncSettings.FPSNumerator;
VsyncDenominator = (int) _syncSettings.FPSDenominator;
DriveLightEnabled = false;
ControllerDefinition = CreateControllerDefinition(_syncSettings, _floppyDiskAssets.Count, _discAssets.Count);
// Parsing input files
var ConfigFiles = new List<IRomAsset>();
// Parsing rom files
foreach (var file in _floppyDiskAssets)
{
var ext = Path.GetExtension(file.RomPath);
bool recognized = false;
// Checking for supported floppy disk extensions
if (ext is ".ima" or ".img" or ".xdf" or ".dmf" or ".fdd" or ".fdi" or ".nfd" or ".d88")
{
_floppyDiskImageFiles.Add(file);
recognized = true;
}
// Checking for DOSBox-x config files
else if (ext is ".conf")
{
ConfigFiles.Add(file);
recognized = true;
}
if (!recognized) throw new Exception($"Unrecognized input file provided: '{file.RomPath}'");
}
var writableHDDImageFileSize = (ulong) _syncSettings.WriteableHardDisk;
_CDReadCallback = CDRead;
_libDOSBox = PreInit<LibDOSBox>(new WaterboxOptions
{
Filename = "dosbox.wbx",
SbrkHeapSizeKB = 32 * 1024,
SealedHeapSizeKB = 32 * 1024,
InvisibleHeapSizeKB = 1024,
PlainHeapSizeKB = 32 * 1024,
MmapHeapSizeKB = 256 * 1024 + (uint) (writableHDDImageFileSize / 1024ul),
SkipCoreConsistencyCheck = lp.Comm.CorePreferences.HasFlag(CoreComm.CorePreferencesFlags.WaterboxCoreConsistencyCheck),
SkipMemoryConsistencyCheck = lp.Comm.CorePreferences.HasFlag(CoreComm.CorePreferencesFlags.WaterboxMemoryConsistencyCheck),
}, new Delegate[] { _CDReadCallback });
////// CD Loading Logic Start
_libDOSBox.SetCdCallbacks(_CDReadCallback);
// Processing each disc
int curDiscIndex = 0;
for (var discIdx = 0; discIdx < _discAssets.Count; discIdx++)
{
// Creating file name to pass to dosbox
var cdRomFileName = _discAssets[discIdx].DiscName + ".cdrom";
// Getting disc data structure
var CDDataStruct = GetCDDataStruct(_discAssets[discIdx].DiscData);
Console.WriteLine($"[CD] Adding Disc {discIdx}: '{_discAssets[discIdx].DiscName}' as '{cdRomFileName}' with sector count: {CDDataStruct.end}, track count: {CDDataStruct.last}.");
// Adding file name to list
_cdRomFileNames.Add(cdRomFileName);
// Creating reader
var discSectorReader = new DiscSectorReader(_discAssets[discIdx].DiscData);
// Adding reader to map
_cdRomFileToReaderMap[cdRomFileName] = discSectorReader;
// Passing CD Data to the core
_libDOSBox.pushCDData(curDiscIndex, CDDataStruct.end, CDDataStruct.last);
// Passing track data to the core
for (var trackIdx = 0; trackIdx < CDDataStruct.last; trackIdx++) _libDOSBox.pushTrackData(curDiscIndex, trackIdx, CDDataStruct.tracks[trackIdx]);
}
////// CD Loading Logic End
// Getting base config file
var configString = Encoding.UTF8.GetString(Resources.DOSBOX_CONF_BASE.Value);
configString += "\n";
// Getting selected machine preset config file
configString += Encoding.UTF8.GetString(_syncSettings.ConfigurationPreset switch
{
ConfigurationPreset._1981_IBM_5150 => Resources.DOSBOX_CONF_1981_IBM_5150.Value,
ConfigurationPreset._1983_IBM_5160 => Resources.DOSBOX_CONF_1983_IBM_5160.Value,
ConfigurationPreset._1986_IBM_5162 => Resources.DOSBOX_CONF_1986_IBM_5162.Value,
ConfigurationPreset._1987_IBM_PS2_25 => Resources.DOSBOX_CONF_1987_IBM_PS2_25.Value,
ConfigurationPreset._1990_IBM_PS2_25_286 => Resources.DOSBOX_CONF_1990_IBM_PS2_25_286.Value,
ConfigurationPreset._1991_IBM_PS2_25_386 => Resources.DOSBOX_CONF_1991_IBM_PS2_25_386.Value,
ConfigurationPreset._1993_IBM_PS2_53_SLC2_486 => Resources.DOSBOX_CONF_1993_IBM_PS2_53_SLC2_486.Value,
ConfigurationPreset._1994_IBM_PS2_76i_SLC2_486 => Resources.DOSBOX_CONF_1994_IBM_PS2_76i_SLC2_486.Value,
ConfigurationPreset._1997_IBM_APTIVA_2140 => Resources.DOSBOX_CONF_1997_IBM_APTIVA_2140.Value,
ConfigurationPreset._1999_IBM_THINKPAD_240 => Resources.DOSBOX_CONF_1999_IBM_THINKPAD_240.Value,
_ => [ ]
});
configString += "\n";
// Adding joystick configuration
configString += "[joystick]\n";
if (_syncSettings.EnableJoystick1 || _syncSettings.EnableJoystick2) configString += "joysticktype = 2axis\n";
else configString += "joysticktype = none\n";
// Adding PC Speaker
configString += "[speaker]\n";
if (_syncSettings.PCSpeaker != PCSpeaker.Auto) configString += $"pcspeaker = {_syncSettings.PCSpeaker}\n";
configString += "\n";
// Adding sound blaser configuration
configString += "[sblaster]\n";
if (_syncSettings.SoundBlasterModel != SoundBlasterModel.Auto) configString += $"sbtype = {_syncSettings.SoundBlasterModel}\n";
if (_syncSettings.SoundBlasterIRQ != -1) configString += $"irq = {_syncSettings.SoundBlasterIRQ}\n";
configString += "\n";
// Adding memory size configuration
configString += "[dosbox]\n";
if (_syncSettings.RAMSize != -1) configString += $"memsize = {_syncSettings.RAMSize}\n";
configString += "\n";
// Adding autoexec line
configString += "[autoexec]\n";
configString += "@echo off\n";
////// Floppy disks: Mounting and appending mounting lines
string floppyMountLine = "imgmount a ";
foreach (var file in _floppyDiskImageFiles)
{
string floppyNewName = FileNames.FD + _floppyDiskCount.ToString() + Path.GetExtension(file.RomPath);
_exe.AddReadonlyFile(file.FileData, floppyNewName);
floppyMountLine += floppyNewName + " ";
_floppyDiskCount++;
}
if (_floppyDiskCount > 0) configString += floppyMountLine + "\n";
////// CD-ROMs: Mounting and appending mounting lines
string cdromMountLine = "imgmount d ";
foreach (var file in _cdRomFileNames) cdromMountLine += file + " ";
if (_cdRomFileNames.Count > 0) configString += cdromMountLine + "\n";
//// Hard Disk mounting
if (_syncSettings.WriteableHardDisk != WriteableHardDiskOptions.None)
{
var writableHDDImageFile = _syncSettings.WriteableHardDisk switch
{
WriteableHardDiskOptions.FAT16_21Mb => Resources.DOSBOX_HDD_IMAGE_FAT16_21MB.Value,
WriteableHardDiskOptions.FAT16_41Mb => Resources.DOSBOX_HDD_IMAGE_FAT16_41MB.Value,
WriteableHardDiskOptions.FAT16_241Mb => Resources.DOSBOX_HDD_IMAGE_FAT16_241MB.Value,
WriteableHardDiskOptions.FAT16_504Mb => Resources.DOSBOX_HDD_IMAGE_FAT16_504MB.Value,
WriteableHardDiskOptions.FAT16_2014Mb => Resources.DOSBOX_HDD_IMAGE_FAT16_2014MB.Value,
WriteableHardDiskOptions.FAT32_4091Mb => Resources.DOSBOX_HDD_IMAGE_FAT32_4091MB.Value,
_ => Resources.DOSBOX_HDD_IMAGE_FAT16_21MB.Value
};
var writableHDDImageData = Zstd.DecompressZstdStream(new MemoryStream(writableHDDImageFile)).ToArray();
_exe.AddReadonlyFile(writableHDDImageData, FileNames.WHD);
configString += "imgmount c " + FileNames.WHD + ".img\n";
}
//// CPU (core) configuration
configString += "[cpu]\n";
if (_syncSettings.CPUCycles != -1) configString += $"cycles = {_syncSettings.CPUCycles}";
//// DOSBox-x configuration
configString += "[dosbox]\n";
if (_syncSettings.MachineType != MachineType.Auto)
{
var machineName = Enum.GetName(typeof(MachineType), _syncSettings.MachineType)!.Replace('P', '+');
configString += $"machine = {machineName}\n";
}
/////////////// Configuration End: Adding single config file to the wbx
// Reconverting config to byte array
IEnumerable<byte> configData = Encoding.UTF8.GetBytes(configString);
// Adding EOL
configString += "@echo on\n";
configString += "\n";
/////// Appending any additional user-provided config files
foreach (var file in ConfigFiles)
{
// Forcing a new line
configData = configData.Concat("\n"u8.ToArray());
configData = configData.Concat(file.FileData);
}
_exe.AddReadonlyFile(configData.ToArray(), FileNames.DOSBOX_CONF);
Console.WriteLine("Configuration: {0}", System.Text.Encoding.Default.GetString(configData.ToArray()));
////////////// Initializing Core
if (!_libDOSBox.Init(new LibDOSBox.InitSettings() {
joystick1Enabled = _syncSettings.EnableJoystick1 ? 1 : 0,
joystick2Enabled = _syncSettings.EnableJoystick2 ? 1 : 0,
hardDiskDriveSize = writableHDDImageFileSize,
preserveHardDiskContents = _syncSettings.PreserveHardDiskContents ? 1 : 0,
fpsNumerator = _syncSettings.FPSNumerator,
fpsDenominator = _syncSettings.FPSDenominator }))
{
throw new InvalidOperationException("Core rejected the rom!");
}
PostInit();
DriveLightEnabled = false;
if (_syncSettings.WriteableHardDisk != WriteableHardDiskOptions.None) DriveLightEnabled = true;
if (_floppyDiskCount > 0) DriveLightEnabled = true;
if (_cdRomFileNames.Count > 0) DriveLightEnabled = true;
}
// CD Handling logic
private List<string> _cdRomFileNames = new List<string>();
private Dictionary<string, DiscSectorReader> _cdRomFileToReaderMap = new Dictionary<string, DiscSectorReader>();
private readonly LibDOSBox.CDReadCallback _CDReadCallback;
public static LibDOSBox.CDData GetCDDataStruct(Disc cd)
{
var ret = new LibDOSBox.CDData();
var ses = cd.Session1;
var ntrack = ses.InformationTrackCount;
for (var i = 0; i < LibDOSBox.CD_MAX_TRACKS; i++)
{
ret.tracks[i] = new();
ret.tracks[i].offset = 0;
ret.tracks[i].loopEnabled = 0;
ret.tracks[i].loopOffset = 0;
if (i < ntrack)
{
ret.tracks[i].start = ses.Tracks[i + 1].LBA;
ret.tracks[i].end = ses.Tracks[i + 2].LBA;
ret.tracks[i].mode = ses.Tracks[i + 1].Mode;
if (i == ntrack - 1)
{
ret.end = ret.tracks[i].end;
ret.last = ntrack;
}
}
else
{
ret.tracks[i].start = 0;
ret.tracks[i].end = 0;
ret.tracks[i].mode = 0;
}
}
return ret;
}
private void CDRead(string cdRomName, int lba, IntPtr dest, int sectorSize)
{
// Console.WriteLine($"Reading from {cdRomName} : {lba} : {sectorSize}");
if (!_cdRomFileToReaderMap.TryGetValue(cdRomName, out var cdRomReader)) throw new InvalidOperationException($"Unrecognized CD File with name: {cdRomName}");
byte[] sectorBuffer = new byte[4096];
switch (sectorSize)
{
case 2048:
cdRomReader.ReadLBA_2048(lba, sectorBuffer, 0);
Marshal.Copy(sectorBuffer, 0, dest, 2048);
break;
case 2352:
cdRomReader.ReadLBA_2352(lba, sectorBuffer, 0);
Marshal.Copy(sectorBuffer, 0, dest, 2352);
break;
case 2448:
cdRomReader.ReadLBA_2448(lba, sectorBuffer, 0);
Marshal.Copy(sectorBuffer, 0, dest, 2448);
break;
default:
throw new InvalidOperationException($"Unsupported CD sector size: {sectorSize}");
}
DriveLightOn = true;
}
protected override LibWaterboxCore.FrameInfo FrameAdvancePrep(IController controller, bool render, bool rendersound)
{
DriveLightOn = false;
var fi = new LibDOSBox.FrameInfo();
// Setting joystick inputs
if (_syncSettings.EnableJoystick1)
{
fi.joystick1.up = controller.IsPressed($"P1 {Inputs.Joystick} {JoystickButtons.Up}") ? 1 : 0;
fi.joystick1.down = controller.IsPressed($"P1 {Inputs.Joystick} {JoystickButtons.Down}") ? 1 : 0;
fi.joystick1.left = controller.IsPressed($"P1 {Inputs.Joystick} {JoystickButtons.Left}") ? 1 : 0;
fi.joystick1.right = controller.IsPressed($"P1 {Inputs.Joystick} {JoystickButtons.Right}") ? 1 : 0;
fi.joystick1.button1 = controller.IsPressed($"P1 {Inputs.Joystick} {JoystickButtons.Button1}") ? 1 : 0;
fi.joystick1.button2 = controller.IsPressed($"P1 {Inputs.Joystick} {JoystickButtons.Button2}") ? 1 : 0;
}
if (_syncSettings.EnableJoystick2)
{
fi.joystick2.up = controller.IsPressed($"P2 {Inputs.Joystick} {JoystickButtons.Up}") ? 1 : 0;
fi.joystick2.down = controller.IsPressed($"P2 {Inputs.Joystick} {JoystickButtons.Down}") ? 1 : 0;
fi.joystick2.left = controller.IsPressed($"P2 {Inputs.Joystick} {JoystickButtons.Left}") ? 1 : 0;
fi.joystick2.right = controller.IsPressed($"P2 {Inputs.Joystick} {JoystickButtons.Right}") ? 1 : 0;
fi.joystick2.button1 = controller.IsPressed($"P2 {Inputs.Joystick} {JoystickButtons.Button1}") ? 1 : 0;
fi.joystick2.button2 = controller.IsPressed($"P2 {Inputs.Joystick} {JoystickButtons.Button2}") ? 1 : 0;
}
// Setting mouse inputs
if (_syncSettings.EnableMouse)
{
var deltaX = controller.AxisValue($"{Inputs.Mouse} {MouseInputs.SpeedX}");
var deltaY = controller.AxisValue($"{Inputs.Mouse} {MouseInputs.SpeedY}");
fi.mouse.posX = controller.AxisValue($"{Inputs.Mouse} {MouseInputs.PosX}");
fi.mouse.posY = controller.AxisValue($"{Inputs.Mouse} { MouseInputs.PosY}");
fi.mouse.dX = deltaX != 0 ? deltaX : fi.mouse.posX - _mouseState.posX;
fi.mouse.dY = deltaY != 0 ? deltaY : fi.mouse.posY - _mouseState.posY;
// Button pressed criteria:
bool isMouseLeftButtonPressed = controller.IsPressed($"{Inputs.Mouse} {MouseInputs.LeftButton}");
bool isMouseMiddleButtonPressed = controller.IsPressed($"{Inputs.Mouse} {MouseInputs.MiddleButton}");
bool isMouseRightButtonPressed = controller.IsPressed($"{Inputs.Mouse} {MouseInputs.RightButton}");
// If the input is made in this frame and the button is not held from before
fi.mouse.leftButtonPressed = isMouseLeftButtonPressed && !_mouseState.leftButtonHeld ? 1 : 0;
fi.mouse.middleButtonPressed = isMouseMiddleButtonPressed && !_mouseState.middleButtonHeld ? 1 : 0;
fi.mouse.rightButtonPressed = isMouseRightButtonPressed && !_mouseState.rightButtonHeld ? 1 : 0;
// Button released criteria:
// If the input is not pressed in this frame and the button is held from before
fi.mouse.leftButtonReleased = !isMouseLeftButtonPressed && _mouseState.leftButtonHeld ? 1 : 0;
fi.mouse.middleButtonReleased = !isMouseMiddleButtonPressed && _mouseState.middleButtonHeld ? 1 : 0;
fi.mouse.rightButtonReleased = !isMouseRightButtonPressed && _mouseState.rightButtonHeld ? 1 : 0;
fi.mouse.sensitivity = _syncSettings.MouseSensitivity;
// Getting new mouse state values
var nextState = new DOSBox.MouseState();
nextState.posX = fi.mouse.posX;
nextState.posY = fi.mouse.posY;
nextState.leftButtonHeld = isMouseLeftButtonPressed;
nextState.middleButtonHeld = isMouseMiddleButtonPressed;
nextState.rightButtonHeld = isMouseRightButtonPressed;
// Updating mouse state
_mouseState = nextState;
}
// Processing floppy disks swaps
fi.driveActions.insertFloppyDisk = -1;
var nextFloppyDiskWasPressed = _nextFloppyDiskPressed;
_nextFloppyDiskPressed = controller.IsPressed(Inputs.NextFloppyDisk);
if (!nextFloppyDiskWasPressed && _nextFloppyDiskPressed && _floppyDiskCount >= 2)
{
_currentFloppyDisk = (_currentFloppyDisk + 1) % _floppyDiskCount;
fi.driveActions.insertFloppyDisk = _currentFloppyDisk;
CoreComm.Notify($"Insterted FloppyDisk {_currentFloppyDisk}: {GetFullName(_floppyDiskImageFiles[_currentFloppyDisk])} into drive A:", null);
}
// Processing CDROM swaps
fi.driveActions.insertCDROM = -1;
var nextCDROMWasPressed = _nextCDROMPressed;
_nextCDROMPressed = controller.IsPressed(Inputs.NextCDROM);
if (!nextCDROMWasPressed && _nextCDROMPressed && _cdRomFileNames.Count >= 2)
{
_currentCDROM = (_currentCDROM + 1) % _cdRomFileNames.Count;
fi.driveActions.insertCDROM = _currentCDROM;
CoreComm.Notify($"Insterted CDROM {_currentCDROM}: {_cdRomFileNames[_currentCDROM]} into drive D:", null);
}
// Processing keyboard inputs
foreach (var (name, key) in _keyboardMap)
{
if (controller.IsPressed(name))
{
unsafe
{
fi.Keys.Buffer[(int) key] = 1;
}
}
}
return fi;
}
protected override void FrameAdvancePost()
{
DriveLightOn = _libDOSBox.getDriveActivityFlag();
// Checking on VGA refresh rate updates
var currentVGARefreshRateNumerator = _VGARefreshRateNumerator;
var currentVGARefreshRateDenominator = _VGARefreshRateDenominator;
_VGARefreshRateNumerator = _libDOSBox.getVGARefreshRateNumerator();
_VGARefreshRateDenominator = _libDOSBox.getVGARefreshRateDenominator();
// If it changed, notify now
if (currentVGARefreshRateNumerator != _VGARefreshRateNumerator || currentVGARefreshRateDenominator != _VGARefreshRateDenominator)
{
double newVGARefreshRate = (double) _VGARefreshRateNumerator / _VGARefreshRateDenominator;
CoreComm.Notify($"VGA Refresh Rate changed to: {_VGARefreshRateNumerator} / {_VGARefreshRateDenominator} = {newVGARefreshRate} Hz", null);
Console.WriteLine($"VGA Refresh Rate changed to: {_VGARefreshRateNumerator} / {_VGARefreshRateDenominator} = {newVGARefreshRate} Hz", null);
}
}
protected override void SaveStateBinaryInternal(BinaryWriter writer)
{
writer.Write(_nextFloppyDiskPressed);
writer.Write(_nextCDROMPressed);
writer.Write(_currentFloppyDisk);
writer.Write(_currentCDROM);
writer.Write(_mouseState.posX);
writer.Write(_mouseState.posY);
writer.Write(_mouseState.leftButtonHeld);
writer.Write(_mouseState.middleButtonHeld);
writer.Write(_mouseState.rightButtonHeld);
}
protected override void LoadStateBinaryInternal(BinaryReader reader)
{
_nextFloppyDiskPressed = reader.ReadBoolean();
_nextCDROMPressed = reader.ReadBoolean();
_currentFloppyDisk = reader.ReadInt32();
_currentCDROM = reader.ReadInt32();
_mouseState.posX = reader.ReadInt32();
_mouseState.posY = reader.ReadInt32();
_mouseState.leftButtonHeld = reader.ReadBoolean();
_mouseState.middleButtonHeld = reader.ReadBoolean();
_mouseState.rightButtonHeld = reader.ReadBoolean();
}
private static class FileNames
{
public const string DOSBOX_CONF = "dosbox-x.conf";
public const string FD = "FloppyDisk";
public const string CD = "CompactDisk";
public const string WHD = "__WritableHardDiskDrive";
}
}
}