2017-06-18 13:02:54 +00:00
|
|
|
|
using BizHawk.Common;
|
|
|
|
|
using BizHawk.Common.BizInvoke;
|
|
|
|
|
using BizHawk.Common.BufferExtensions;
|
|
|
|
|
using BizHawk.Emulation.Common;
|
|
|
|
|
using BizHawk.Emulation.Cores.Waterbox;
|
|
|
|
|
using BizHawk.Emulation.DiscSystem;
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.ComponentModel;
|
|
|
|
|
using System.ComponentModel.DataAnnotations;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Runtime.InteropServices;
|
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
namespace BizHawk.Emulation.Cores.Consoles.Sega.Saturn
|
|
|
|
|
{
|
2017-07-13 19:31:06 +00:00
|
|
|
|
[Core("Saturnus", "Mednafen Team", true, true, "0.9.44.1",
|
2017-06-18 13:02:54 +00:00
|
|
|
|
"https://mednafen.github.io/releases/", false)]
|
|
|
|
|
public class Saturnus : WaterboxCore,
|
|
|
|
|
IDriveLight, IRegionable,
|
|
|
|
|
ISettable<Saturnus.Settings, Saturnus.SyncSettings>
|
|
|
|
|
{
|
|
|
|
|
private static readonly DiscSectorReaderPolicy _diskPolicy = new DiscSectorReaderPolicy
|
|
|
|
|
{
|
|
|
|
|
DeinterleavedSubcode = false
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private LibSaturnus _core;
|
|
|
|
|
private Disc[] _disks;
|
|
|
|
|
private DiscSectorReader[] _diskReaders;
|
|
|
|
|
private bool _isPal;
|
|
|
|
|
private SaturnusControllerDeck _controllerDeck;
|
|
|
|
|
private int _activeDisk;
|
|
|
|
|
private bool _prevDiskSignal;
|
|
|
|
|
private bool _nextDiskSignal;
|
|
|
|
|
|
|
|
|
|
private static bool CheckDisks(IEnumerable<DiscSectorReader> readers)
|
|
|
|
|
{
|
|
|
|
|
var buff = new byte[2048 * 16];
|
|
|
|
|
foreach (var r in readers)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < 16; i++)
|
|
|
|
|
{
|
|
|
|
|
if (r.ReadLBA_2048(i, buff, 2048 * i) != 2048)
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Encoding.ASCII.GetString(buff, 0, 16) != "SEGA SEGASATURN ")
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
using (var sha256 = SHA256.Create())
|
|
|
|
|
{
|
|
|
|
|
sha256.ComputeHash(buff, 0x100, 0xd00);
|
|
|
|
|
if (sha256.Hash.BytesToHexString() != "96B8EA48819CFA589F24C40AA149C224C420DCCF38B730F00156EFE25C9BBC8F")
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-08 22:22:40 +00:00
|
|
|
|
[CoreConstructor("SAT")]
|
|
|
|
|
public Saturnus(CoreComm comm, byte[] rom)
|
|
|
|
|
:base(comm, new Configuration())
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("To load a Saturn game, please load the CUE file and not the BIN file.");
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public Saturnus(CoreComm comm, IEnumerable<Disc> disks, bool deterministic, Settings settings,
|
|
|
|
|
SyncSettings syncSettings)
|
2017-06-17 19:30:03 +00:00
|
|
|
|
:base(comm, new Configuration
|
|
|
|
|
{
|
|
|
|
|
MaxSamples = 8192,
|
|
|
|
|
DefaultWidth = 320,
|
|
|
|
|
DefaultHeight = 240,
|
|
|
|
|
MaxWidth = 1024,
|
|
|
|
|
MaxHeight = 1024,
|
|
|
|
|
SystemId = "SAT"
|
2017-06-18 13:02:54 +00:00
|
|
|
|
})
|
|
|
|
|
{
|
|
|
|
|
settings = settings ?? new Settings();
|
|
|
|
|
syncSettings = syncSettings ?? new SyncSettings();
|
|
|
|
|
|
|
|
|
|
_disks = disks.ToArray();
|
|
|
|
|
_diskReaders = disks.Select(d => new DiscSectorReader(d) { Policy = _diskPolicy }).ToArray();
|
|
|
|
|
if (!CheckDisks(_diskReaders))
|
|
|
|
|
throw new InvalidOperationException("Some disks are not valid");
|
|
|
|
|
InitCallbacks();
|
|
|
|
|
|
|
|
|
|
_core = PreInit<LibSaturnus>(new PeRunnerOptions
|
|
|
|
|
{
|
|
|
|
|
Filename = "ss.wbx",
|
|
|
|
|
SbrkHeapSizeKB = 128,
|
2017-06-17 19:30:03 +00:00
|
|
|
|
SealedHeapSizeKB = 4096, // 512KB of bios, 2MB of kof95/ultraman carts
|
|
|
|
|
InvisibleHeapSizeKB = 8 * 1024, // 4MB of framebuffer
|
|
|
|
|
MmapHeapSizeKB = 0, // not used?
|
|
|
|
|
PlainHeapSizeKB = 24 * 1024, // up to 16MB of cart ram
|
2017-06-18 13:02:54 +00:00
|
|
|
|
StartAddress = LibSaturnus.StartAddress
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
SetFirmwareCallbacks();
|
|
|
|
|
SetCdCallbacks();
|
|
|
|
|
|
|
|
|
|
if (!_core.Init(_disks.Length, syncSettings.ExpansionCart, syncSettings.Region, syncSettings.RegionAutodetect))
|
|
|
|
|
throw new InvalidOperationException("Core rejected the disks!");
|
|
|
|
|
ClearAllCallbacks();
|
|
|
|
|
|
|
|
|
|
_controllerDeck = new SaturnusControllerDeck(new[]
|
|
|
|
|
{
|
|
|
|
|
syncSettings.Port1Multitap,
|
|
|
|
|
syncSettings.Port2Multitap
|
|
|
|
|
}, new[]
|
|
|
|
|
{
|
|
|
|
|
syncSettings.Port1,
|
|
|
|
|
syncSettings.Port2,
|
|
|
|
|
syncSettings.Port3,
|
|
|
|
|
syncSettings.Port4,
|
|
|
|
|
syncSettings.Port5,
|
|
|
|
|
syncSettings.Port6,
|
|
|
|
|
syncSettings.Port7,
|
|
|
|
|
syncSettings.Port8,
|
|
|
|
|
syncSettings.Port9,
|
|
|
|
|
syncSettings.Port10,
|
|
|
|
|
syncSettings.Port11,
|
|
|
|
|
syncSettings.Port12
|
|
|
|
|
}, _core);
|
|
|
|
|
ControllerDefinition = _controllerDeck.Definition;
|
|
|
|
|
ControllerDefinition.Name = "Saturn Controller";
|
|
|
|
|
ControllerDefinition.BoolButtons.AddRange(new[]
|
|
|
|
|
{
|
|
|
|
|
"Power", "Reset", "Previous Disk", "Next Disk"
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_core.SetRtc((long)syncSettings.InitialTime.Subtract(new DateTime(1970, 1, 1)).TotalSeconds,
|
|
|
|
|
syncSettings.Language);
|
|
|
|
|
|
|
|
|
|
PostInit();
|
|
|
|
|
SetCdCallbacks();
|
|
|
|
|
_core.SetDisk(0, false);
|
|
|
|
|
PutSettings(settings);
|
|
|
|
|
_syncSettings = syncSettings;
|
|
|
|
|
DeterministicEmulation = deterministic || !_syncSettings.UseRealTime;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-17 19:30:03 +00:00
|
|
|
|
protected override LibWaterboxCore.FrameInfo FrameAdvancePrep(IController controller, bool render, bool rendersound)
|
|
|
|
|
{
|
2017-06-18 13:02:54 +00:00
|
|
|
|
var prevDiskSignal = controller.IsPressed("Previous Disk");
|
|
|
|
|
var nextDiskSignal = controller.IsPressed("Next Disk");
|
|
|
|
|
var newDisk = _activeDisk;
|
|
|
|
|
if (prevDiskSignal && !_prevDiskSignal)
|
|
|
|
|
newDisk--;
|
|
|
|
|
if (nextDiskSignal && !_nextDiskSignal)
|
|
|
|
|
newDisk++;
|
|
|
|
|
_prevDiskSignal = prevDiskSignal;
|
|
|
|
|
_nextDiskSignal = nextDiskSignal;
|
|
|
|
|
if (newDisk < -1)
|
|
|
|
|
newDisk = -1;
|
|
|
|
|
if (newDisk >= _disks.Length)
|
|
|
|
|
newDisk = _disks.Length - 1;
|
|
|
|
|
if (newDisk != _activeDisk)
|
|
|
|
|
{
|
|
|
|
|
_core.SetDisk(newDisk == -1 ? 0 : newDisk, newDisk == -1);
|
|
|
|
|
_activeDisk = newDisk;
|
2017-06-17 19:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if not reset, the core will maintain its own deterministic increasing time each frame
|
2017-06-18 13:02:54 +00:00
|
|
|
|
if (!DeterministicEmulation)
|
|
|
|
|
{
|
|
|
|
|
_core.SetRtc((long)DateTime.Now.Subtract(new DateTime(1970, 1, 1)).TotalSeconds,
|
|
|
|
|
_syncSettings.Language);
|
2017-06-17 19:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
DriveLightOn = false;
|
|
|
|
|
if (controller.IsPressed("Power"))
|
2017-06-17 19:30:03 +00:00
|
|
|
|
_core.HardReset();
|
|
|
|
|
|
|
|
|
|
_core.SetControllerData(_controllerDeck.Poll(controller));
|
|
|
|
|
|
2017-06-29 09:21:16 +00:00
|
|
|
|
SetVideoParameters();
|
|
|
|
|
|
2017-06-17 19:30:03 +00:00
|
|
|
|
return new LibSaturnus.FrameInfo { ResetPushed = controller.IsPressed("Reset") ? 1 : 0 };
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public DisplayType Region => _isPal ? DisplayType.PAL : DisplayType.NTSC;
|
|
|
|
|
|
|
|
|
|
#region ISettable
|
|
|
|
|
|
|
|
|
|
public class Settings
|
|
|
|
|
{
|
2017-06-29 09:21:16 +00:00
|
|
|
|
public enum ResolutionModeTypes
|
|
|
|
|
{
|
|
|
|
|
[Display(Name = "Pixel Pro")]
|
|
|
|
|
PixelPro,
|
|
|
|
|
[Display(Name = "Hardcode Debug")]
|
|
|
|
|
HardcoreDebug,
|
|
|
|
|
[Display(Name = "Mednafen (5:4 AR)")]
|
|
|
|
|
Mednafen,
|
|
|
|
|
[Display(Name = "Tweaked Mednafen (5:4 AR)")]
|
|
|
|
|
TweakedMednafen,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[DisplayName("Resolution Mode")]
|
|
|
|
|
[DefaultValue(ResolutionModeTypes.PixelPro)]
|
|
|
|
|
[Description("Method for managing varying resolutions")]
|
|
|
|
|
public ResolutionModeTypes ResolutionMode { get; set; }
|
2017-06-18 13:02:54 +00:00
|
|
|
|
|
|
|
|
|
// extern bool setting_ss_h_blend;
|
|
|
|
|
[DisplayName("Horizontal Blend")]
|
|
|
|
|
[DefaultValue(false)]
|
|
|
|
|
[Description("Use horizontal blend filter")]
|
|
|
|
|
public bool HBlend { get; set; }
|
|
|
|
|
|
|
|
|
|
// extern bool setting_ss_h_overscan;
|
|
|
|
|
[DisplayName("Horizontal Overscan")]
|
|
|
|
|
[DefaultValue(true)]
|
|
|
|
|
[Description("Show horiziontal overscan area")]
|
|
|
|
|
public bool HOverscan { get; set; }
|
|
|
|
|
|
|
|
|
|
// extern int setting_ss_slstart;
|
|
|
|
|
[DefaultValue(0)]
|
|
|
|
|
[Description("First scanline to display in NTSC mode")]
|
|
|
|
|
[Range(0, 239)]
|
|
|
|
|
public int ScanlineStartNtsc { get; set; }
|
|
|
|
|
// extern int setting_ss_slend;
|
|
|
|
|
[DefaultValue(239)]
|
|
|
|
|
[Description("Last scanline to display in NTSC mode")]
|
|
|
|
|
[Range(0, 239)]
|
|
|
|
|
public int ScanlineEndNtsc { get; set; }
|
|
|
|
|
// extern int setting_ss_slstartp;
|
|
|
|
|
[DefaultValue(0)]
|
|
|
|
|
[Description("First scanline to display in PAL mode")]
|
|
|
|
|
[Range(-16, 271)]
|
|
|
|
|
public int ScanlineStartPal { get; set; }
|
|
|
|
|
// extern int setting_ss_slendp;
|
|
|
|
|
[DefaultValue(255)]
|
|
|
|
|
[Description("Last scanline to display in PAL mode")]
|
|
|
|
|
[Range(-16, 271)]
|
|
|
|
|
public int ScanlineEndPal { get; set; }
|
|
|
|
|
|
|
|
|
|
public Settings()
|
|
|
|
|
{
|
|
|
|
|
SettingsUtil.SetDefaultValues(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Settings Clone()
|
|
|
|
|
{
|
|
|
|
|
return (Settings)MemberwiseClone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static bool NeedsReboot(Settings x, Settings y)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class SyncSettings
|
|
|
|
|
{
|
|
|
|
|
public enum CartType
|
|
|
|
|
{
|
|
|
|
|
[Display(Name = "Autodetect. Will use Backup Ram Cart if no others are appropriate")]
|
|
|
|
|
Autodetect = -1,
|
|
|
|
|
[Display(Name = "None")]
|
|
|
|
|
None,
|
|
|
|
|
[Display(Name = "Backup Ram Cart")]
|
|
|
|
|
Backup,
|
|
|
|
|
[Display(Name = "1 Meg Ram Cart")]
|
|
|
|
|
Ram1Meg,
|
|
|
|
|
[Display(Name = "4 Meg Ram Cart")]
|
|
|
|
|
Ram4Meg,
|
|
|
|
|
[Display(Name = "King of Fighters 95 Rom Cart")]
|
|
|
|
|
Kof95,
|
|
|
|
|
[Display(Name = "Ultraman Rom Cart")]
|
|
|
|
|
Ultraman,
|
|
|
|
|
[Display(Name = "CS1 16 Meg Ram Cart")]
|
|
|
|
|
Ram16Meg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// extern int setting_ss_cart;
|
|
|
|
|
[DefaultValue(CartType.Autodetect)]
|
|
|
|
|
[Description("What to plug into the Saturn expansion slot")]
|
|
|
|
|
public CartType ExpansionCart { get; set; }
|
|
|
|
|
|
|
|
|
|
[DisplayName("Port 1 Device")]
|
|
|
|
|
[DefaultValue(SaturnusControllerDeck.Device.Gamepad)]
|
2017-06-20 12:51:49 +00:00
|
|
|
|
[TypeConverter(typeof(DescribableEnumConverter))]
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public SaturnusControllerDeck.Device Port1 { get; set; }
|
2017-06-20 12:51:49 +00:00
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
[DisplayName("Port 2 Device")]
|
|
|
|
|
[DefaultValue(SaturnusControllerDeck.Device.Gamepad)]
|
2017-06-20 12:51:49 +00:00
|
|
|
|
[TypeConverter(typeof(DescribableEnumConverter))]
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public SaturnusControllerDeck.Device Port2 { get; set; }
|
2017-06-20 12:51:49 +00:00
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
[DisplayName("Port 3 Device")]
|
|
|
|
|
[DefaultValue(SaturnusControllerDeck.Device.None)]
|
|
|
|
|
[Description("Only used if one or both multitaps are set")]
|
2017-06-20 12:51:49 +00:00
|
|
|
|
[TypeConverter(typeof(DescribableEnumConverter))]
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public SaturnusControllerDeck.Device Port3 { get; set; }
|
2017-06-20 12:51:49 +00:00
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
[DisplayName("Port 4 Device")]
|
|
|
|
|
[DefaultValue(SaturnusControllerDeck.Device.None)]
|
|
|
|
|
[Description("Only used if one or both multitaps are set")]
|
2017-06-20 12:51:49 +00:00
|
|
|
|
[TypeConverter(typeof(DescribableEnumConverter))]
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public SaturnusControllerDeck.Device Port4 { get; set; }
|
2017-06-20 12:51:49 +00:00
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
[DisplayName("Port 5 Device")]
|
|
|
|
|
[DefaultValue(SaturnusControllerDeck.Device.None)]
|
|
|
|
|
[Description("Only used if one or both multitaps are set")]
|
2017-06-20 12:51:49 +00:00
|
|
|
|
[TypeConverter(typeof(DescribableEnumConverter))]
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public SaturnusControllerDeck.Device Port5 { get; set; }
|
2017-06-20 12:51:49 +00:00
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
[DisplayName("Port 6 Device")]
|
|
|
|
|
[DefaultValue(SaturnusControllerDeck.Device.None)]
|
|
|
|
|
[Description("Only used if one or both multitaps are set")]
|
2017-06-20 12:51:49 +00:00
|
|
|
|
[TypeConverter(typeof(DescribableEnumConverter))]
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public SaturnusControllerDeck.Device Port6 { get; set; }
|
2017-06-20 12:51:49 +00:00
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
[DisplayName("Port 7 Device")]
|
|
|
|
|
[DefaultValue(SaturnusControllerDeck.Device.None)]
|
|
|
|
|
[Description("Only used if one or both multitaps are set")]
|
2017-06-20 12:51:49 +00:00
|
|
|
|
[TypeConverter(typeof(DescribableEnumConverter))]
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public SaturnusControllerDeck.Device Port7 { get; set; }
|
2017-06-20 12:51:49 +00:00
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
[DisplayName("Port 8 Device")]
|
|
|
|
|
[DefaultValue(SaturnusControllerDeck.Device.None)]
|
|
|
|
|
[Description("Only used if both multitaps are set")]
|
2017-06-20 12:51:49 +00:00
|
|
|
|
[TypeConverter(typeof(DescribableEnumConverter))]
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public SaturnusControllerDeck.Device Port8 { get; set; }
|
2017-06-20 12:51:49 +00:00
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
[DisplayName("Port 9 Device")]
|
|
|
|
|
[DefaultValue(SaturnusControllerDeck.Device.None)]
|
|
|
|
|
[Description("Only used if both multitaps are set")]
|
2017-06-20 12:51:49 +00:00
|
|
|
|
[TypeConverter(typeof(DescribableEnumConverter))]
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public SaturnusControllerDeck.Device Port9 { get; set; }
|
2017-06-20 12:51:49 +00:00
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
[DisplayName("Port 10 Device")]
|
|
|
|
|
[DefaultValue(SaturnusControllerDeck.Device.None)]
|
|
|
|
|
[Description("Only used if both multitaps are set")]
|
2017-06-20 12:51:49 +00:00
|
|
|
|
[TypeConverter(typeof(DescribableEnumConverter))]
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public SaturnusControllerDeck.Device Port10 { get; set; }
|
2017-06-20 12:51:49 +00:00
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
[DisplayName("Port 11 Device")]
|
|
|
|
|
[DefaultValue(SaturnusControllerDeck.Device.None)]
|
|
|
|
|
[Description("Only used if both multitaps are set")]
|
2017-06-20 12:51:49 +00:00
|
|
|
|
[TypeConverter(typeof(DescribableEnumConverter))]
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public SaturnusControllerDeck.Device Port11 { get; set; }
|
2017-06-20 12:51:49 +00:00
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
[DisplayName("Port 12 Device")]
|
|
|
|
|
[DefaultValue(SaturnusControllerDeck.Device.None)]
|
|
|
|
|
[Description("Only used if both multitaps are set")]
|
2017-06-20 12:51:49 +00:00
|
|
|
|
[TypeConverter(typeof(DescribableEnumConverter))]
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public SaturnusControllerDeck.Device Port12 { get; set; }
|
2017-06-20 12:51:49 +00:00
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
[DisplayName("Port 1 Multitap")]
|
|
|
|
|
[Description("If true, Ports 1-6 will be used for multitap")]
|
|
|
|
|
[DefaultValue(false)]
|
|
|
|
|
public bool Port1Multitap { get; set; }
|
|
|
|
|
[DisplayName("Port 2 Multitap")]
|
|
|
|
|
[Description("If true, Ports 2-7 (or 7-12, depending on Port 1 Multitap) will be used for multitap")]
|
|
|
|
|
[DefaultValue(false)]
|
|
|
|
|
public bool Port2Multitap { get; set; }
|
|
|
|
|
|
|
|
|
|
// extern bool setting_ss_region_autodetect;
|
|
|
|
|
[DisplayName("Region Autodetect")]
|
|
|
|
|
[Description("Attempt to guess the game region")]
|
|
|
|
|
[DefaultValue(true)]
|
|
|
|
|
public bool RegionAutodetect { get; set; }
|
|
|
|
|
|
|
|
|
|
public enum RegionType
|
|
|
|
|
{
|
|
|
|
|
[Display(Name = "Japan")]
|
|
|
|
|
Japan = 1,
|
|
|
|
|
[Display(Name = "Asia NTSC (Taiwan, Phillipines)")]
|
|
|
|
|
Taiwan = 2,
|
|
|
|
|
[Display(Name = "North America")]
|
|
|
|
|
Murka = 4,
|
|
|
|
|
[Display(Name = "Latin America NTSC (Brazil)")]
|
|
|
|
|
Brazil = 5,
|
|
|
|
|
[Display(Name = "South Korea")]
|
|
|
|
|
Korea = 6,
|
|
|
|
|
[Display(Name = "Asia PAL (China, Middle East)")]
|
|
|
|
|
China = 10,
|
|
|
|
|
[Display(Name = "Europe")]
|
|
|
|
|
Yurop = 12,
|
|
|
|
|
[Display(Name = "Latin America PAL")]
|
|
|
|
|
SouthAmerica = 13
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// extern int setting_ss_region_default;
|
|
|
|
|
[DisplayName("Default Region")]
|
|
|
|
|
[Description("Used when Region Autodetect is disabled or fails")]
|
|
|
|
|
[DefaultValue(RegionType.Japan)]
|
2017-06-20 01:23:20 +00:00
|
|
|
|
[TypeConverter(typeof(DescribableEnumConverter))]
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public RegionType Region { get; set; }
|
|
|
|
|
|
|
|
|
|
public enum LanguageType
|
|
|
|
|
{
|
|
|
|
|
English = 0,
|
|
|
|
|
German = 1,
|
|
|
|
|
French = 2,
|
|
|
|
|
Spanish = 3,
|
|
|
|
|
Italian = 4,
|
|
|
|
|
Japanese = 5,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[DisplayName("Language")]
|
|
|
|
|
[Description("Language of the system. Only affects some games in some regions.")]
|
|
|
|
|
[DefaultValue(LanguageType.Japanese)]
|
|
|
|
|
public LanguageType Language { get; set; }
|
|
|
|
|
|
|
|
|
|
[DisplayName("Initial Time")]
|
|
|
|
|
[Description("Initial time of emulation. Only relevant when UseRealTime is false.")]
|
|
|
|
|
[DefaultValue(typeof(DateTime), "2010-01-01")]
|
|
|
|
|
public DateTime InitialTime { get; set; }
|
|
|
|
|
|
|
|
|
|
[DisplayName("Use RealTime")]
|
|
|
|
|
[Description("If true, RTC clock will be based off of real time instead of emulated time. Ignored (set to false) when recording a movie.")]
|
|
|
|
|
[DefaultValue(false)]
|
|
|
|
|
public bool UseRealTime { get; set; }
|
|
|
|
|
|
|
|
|
|
public SyncSettings()
|
|
|
|
|
{
|
|
|
|
|
SettingsUtil.SetDefaultValues(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public SyncSettings Clone()
|
|
|
|
|
{
|
|
|
|
|
return (SyncSettings)MemberwiseClone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static bool NeedsReboot(SyncSettings x, SyncSettings y)
|
|
|
|
|
{
|
|
|
|
|
return !DeepEquality.DeepEquals(x, y);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Settings _settings;
|
|
|
|
|
private SyncSettings _syncSettings;
|
2017-06-17 19:30:03 +00:00
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
public Settings GetSettings()
|
|
|
|
|
{
|
|
|
|
|
return _settings.Clone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool PutSettings(Settings s)
|
|
|
|
|
{
|
|
|
|
|
var ret = Settings.NeedsReboot(_settings, s);
|
|
|
|
|
_settings = s;
|
2017-06-29 09:21:16 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//todo natt - is this safe? this is now called before every frameadvance
|
|
|
|
|
//(the correct aspect ratio is no longer an option for other reasons)
|
|
|
|
|
//_core.SetVideoParameters(s.CorrectAspectRatio, s.HBlend, s.HOverscan, sls, sle);
|
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public SyncSettings GetSyncSettings()
|
|
|
|
|
{
|
|
|
|
|
return _syncSettings.Clone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool PutSyncSettings(SyncSettings s)
|
|
|
|
|
{
|
|
|
|
|
var ret = SyncSettings.NeedsReboot(_syncSettings, s);
|
|
|
|
|
_syncSettings = s;
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-29 09:21:16 +00:00
|
|
|
|
private void SetVideoParameters()
|
|
|
|
|
{
|
|
|
|
|
var s = _settings;
|
|
|
|
|
var sls = _isPal ? s.ScanlineStartPal + 16 : s.ScanlineStartNtsc;
|
|
|
|
|
var sle = _isPal ? s.ScanlineEndPal + 16 : s.ScanlineEndNtsc;
|
|
|
|
|
|
|
|
|
|
bool correctAspect = true;
|
|
|
|
|
if (_settings.ResolutionMode == Settings.ResolutionModeTypes.PixelPro)
|
|
|
|
|
correctAspect = false;
|
|
|
|
|
|
|
|
|
|
_core.SetVideoParameters(correctAspect, _settings.HBlend, _settings.HOverscan, sls, sle);
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-18 13:02:54 +00:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region IStatable
|
2017-06-17 19:30:03 +00:00
|
|
|
|
|
|
|
|
|
protected override void SaveStateBinaryInternal(BinaryWriter writer)
|
|
|
|
|
{
|
2017-06-18 13:02:54 +00:00
|
|
|
|
writer.Write(_activeDisk);
|
|
|
|
|
writer.Write(_prevDiskSignal);
|
2017-06-17 19:30:03 +00:00
|
|
|
|
writer.Write(_nextDiskSignal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override void LoadStateBinaryInternal(BinaryReader reader)
|
|
|
|
|
{
|
2017-06-18 13:02:54 +00:00
|
|
|
|
_activeDisk = reader.ReadInt32();
|
|
|
|
|
_prevDiskSignal = reader.ReadBoolean();
|
2017-06-17 19:30:03 +00:00
|
|
|
|
_nextDiskSignal = reader.ReadBoolean();
|
|
|
|
|
// any managed pointers that we sent to the core need to be resent now!
|
|
|
|
|
SetCdCallbacks();
|
2017-06-29 09:21:16 +00:00
|
|
|
|
|
|
|
|
|
//todo natt: evaluate the philosophy of this.
|
|
|
|
|
//i think it's sound: loadstate will replace the last values set by frontend; so this should re-assert them.
|
|
|
|
|
//or we could make sure values from the frontend are stored in a segment designed with the appropriate waterboxing rules, but that seems tricky...
|
|
|
|
|
//anyway, in this case, I did it before frameadvance instead, so that's just as good (probably?)
|
|
|
|
|
//PutSettings(_settings);
|
2017-06-18 13:02:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Callbacks
|
|
|
|
|
|
|
|
|
|
private LibSaturnus.FirmwareSizeCallback _firmwareSizeCallback;
|
|
|
|
|
private LibSaturnus.FirmwareDataCallback _firmwareDataCallback;
|
|
|
|
|
private LibSaturnus.CDTOCCallback _cdTocCallback;
|
|
|
|
|
private LibSaturnus.CDSectorCallback _cdSectorCallback;
|
|
|
|
|
|
|
|
|
|
private void InitCallbacks()
|
|
|
|
|
{
|
|
|
|
|
_firmwareSizeCallback = FirmwareSize;
|
|
|
|
|
_firmwareDataCallback = FirmwareData;
|
|
|
|
|
_cdTocCallback = CDTOCCallback;
|
|
|
|
|
_cdSectorCallback = CDSectorCallback;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SetFirmwareCallbacks()
|
|
|
|
|
{
|
|
|
|
|
_core.SetFirmwareCallbacks(_firmwareSizeCallback, _firmwareDataCallback);
|
|
|
|
|
}
|
|
|
|
|
private void SetCdCallbacks()
|
|
|
|
|
{
|
|
|
|
|
_core.SetCDCallbacks(_cdTocCallback, _cdSectorCallback);
|
|
|
|
|
}
|
|
|
|
|
private void ClearAllCallbacks()
|
|
|
|
|
{
|
|
|
|
|
_core.SetFirmwareCallbacks(null, null);
|
|
|
|
|
_core.SetCDCallbacks(null, null);
|
|
|
|
|
_core.SetInputCallback(null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string TranslateFirmwareName(string filename)
|
|
|
|
|
{
|
|
|
|
|
switch (filename)
|
|
|
|
|
{
|
|
|
|
|
case "ss.cart.kof95_path":
|
|
|
|
|
return "KOF95";
|
|
|
|
|
case "ss.cart.ultraman_path":
|
|
|
|
|
return "ULTRAMAN";
|
|
|
|
|
case "BIOS_J":
|
|
|
|
|
case "BIOS_A":
|
|
|
|
|
return "J";
|
|
|
|
|
case "BIOS_E":
|
|
|
|
|
_isPal = true;
|
|
|
|
|
return "E";
|
|
|
|
|
case "BIOS_U":
|
|
|
|
|
return "U";
|
|
|
|
|
default:
|
|
|
|
|
throw new InvalidOperationException("Unknown BIOS file");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private byte[] GetFirmware(string filename)
|
|
|
|
|
{
|
|
|
|
|
return CoreComm.CoreFileProvider.GetFirmware("SAT", TranslateFirmwareName(filename), true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private int FirmwareSize(string filename)
|
|
|
|
|
{
|
|
|
|
|
return GetFirmware(filename).Length;
|
|
|
|
|
}
|
|
|
|
|
private void FirmwareData(string filename, IntPtr dest)
|
|
|
|
|
{
|
|
|
|
|
var data = GetFirmware(filename);
|
|
|
|
|
Marshal.Copy(data, 0, dest, data.Length);
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-09 15:17:42 +00:00
|
|
|
|
public static void SetupTOC(LibSaturnus.TOC t, DiscTOC tin)
|
2017-06-18 13:02:54 +00:00
|
|
|
|
{
|
|
|
|
|
// everything that's not commented, we're sure about
|
|
|
|
|
t.FirstTrack = tin.FirstRecordedTrackNumber;
|
|
|
|
|
t.LastTrack = tin.LastRecordedTrackNumber;
|
|
|
|
|
t.DiskType = (int)tin.Session1Format;
|
|
|
|
|
for (int i = 0; i < 101; i++)
|
|
|
|
|
{
|
|
|
|
|
t.Tracks[i].Adr = tin.TOCItems[i].Exists ? 1 : 0; // ????
|
|
|
|
|
t.Tracks[i].Lba = tin.TOCItems[i].LBA;
|
|
|
|
|
t.Tracks[i].Control = (int)tin.TOCItems[i].Control;
|
|
|
|
|
t.Tracks[i].Valid = tin.TOCItems[i].Exists ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-07-09 15:17:42 +00:00
|
|
|
|
|
|
|
|
|
private void CDTOCCallback(int disk, [In, Out]LibSaturnus.TOC t)
|
|
|
|
|
{
|
|
|
|
|
SetupTOC(t, _disks[disk].TOC);
|
|
|
|
|
}
|
2017-06-18 13:02:54 +00:00
|
|
|
|
private void CDSectorCallback(int disk, int lba, IntPtr dest)
|
|
|
|
|
{
|
|
|
|
|
var buff = new byte[2448];
|
|
|
|
|
_diskReaders[disk].ReadLBA_2448(lba, buff, 0);
|
|
|
|
|
Marshal.Copy(buff, 0, dest, 2448);
|
|
|
|
|
DriveLightOn = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool DriveLightEnabled => true;
|
|
|
|
|
public bool DriveLightOn { get; private set; }
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
private const int PalFpsNum = 1734687500;
|
|
|
|
|
private const int PalFpsDen = 61 * 455 * 1251;
|
|
|
|
|
private const int NtscFpsNum = 1746818182; // 1746818181.8
|
|
|
|
|
private const int NtscFpsDen = 61 * 455 * 1051;
|
|
|
|
|
public override int VsyncNumerator => _isPal ? PalFpsNum : NtscFpsNum;
|
|
|
|
|
public override int VsyncDenominator => _isPal ? PalFpsDen : NtscFpsDen;
|
2017-06-27 02:20:55 +00:00
|
|
|
|
|
|
|
|
|
private int _virtualWidth;
|
|
|
|
|
private int _virtualHeight;
|
|
|
|
|
public override int VirtualWidth => _virtualWidth;
|
|
|
|
|
public override int VirtualHeight => _virtualHeight;
|
|
|
|
|
|
2017-07-15 07:53:33 +00:00
|
|
|
|
bool _useResizedBuffer;
|
|
|
|
|
int[] _resizedBuffer;
|
|
|
|
|
|
|
|
|
|
public override int[] GetVideoBuffer()
|
|
|
|
|
{
|
|
|
|
|
if (_useResizedBuffer) return _resizedBuffer;
|
|
|
|
|
else return _videoBuffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AllocResizedBuffer(int width, int height)
|
|
|
|
|
{
|
|
|
|
|
_useResizedBuffer = true;
|
|
|
|
|
if (_resizedBuffer == null || width * height != _resizedBuffer.Length)
|
|
|
|
|
_resizedBuffer = new int[width * height];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected unsafe override void FrameAdvancePost()
|
2017-06-27 02:20:55 +00:00
|
|
|
|
{
|
2017-06-29 09:21:16 +00:00
|
|
|
|
//TODO: can we force the videoprovider to add a prescale instead of having to do it in software here?
|
|
|
|
|
//TODO: if not, actually do it in software, instead of relying on the virtual sizes
|
|
|
|
|
//TODO: find a reason why relying on the virtual sizes is actually not good enough?
|
2017-07-15 07:53:33 +00:00
|
|
|
|
|
2017-06-29 09:21:16 +00:00
|
|
|
|
//TODO: export VDP2 display area width from emulator and add option to aggressively crop overscan - OR - add that logic to core
|
|
|
|
|
|
|
|
|
|
//mednafen, for reference:
|
|
|
|
|
//if (PAL)
|
|
|
|
|
//{
|
|
|
|
|
// gi->nominal_width = (ShowHOverscan ? 365 : 354);
|
|
|
|
|
// gi->fb_height = 576;
|
|
|
|
|
//}
|
|
|
|
|
//else
|
|
|
|
|
//{
|
|
|
|
|
// gi->nominal_width = (ShowHOverscan ? 302 : 292);
|
|
|
|
|
// gi->fb_height = 480;
|
|
|
|
|
//}
|
|
|
|
|
//gi->nominal_height = LineVisLast + 1 - LineVisFirst;
|
|
|
|
|
//gi->lcm_width = (ShowHOverscan ? 10560 : 10240);
|
|
|
|
|
//gi->lcm_height = (LineVisLast + 1 - LineVisFirst) * 2;
|
|
|
|
|
//if (!CorrectAspect)
|
|
|
|
|
//{
|
|
|
|
|
// gi->nominal_width = (ShowHOverscan ? 352 : 341);
|
|
|
|
|
// gi->lcm_width = gi->nominal_width * 2;
|
|
|
|
|
//}
|
|
|
|
|
|
2017-07-15 07:53:33 +00:00
|
|
|
|
_useResizedBuffer = false;
|
|
|
|
|
|
2017-06-29 09:21:16 +00:00
|
|
|
|
bool isHorz2x = false, isVert2x = false;
|
|
|
|
|
|
|
|
|
|
//note: these work with PAL also
|
|
|
|
|
//these are all with CorrectAR.
|
|
|
|
|
//with IncorrectAR, only 352 and 341 will show up
|
|
|
|
|
//that is, 330 is forced to 352 so mednafen's presentation can display it pristine no matter what mode it is
|
|
|
|
|
//(it's mednafen's equivalent of a more debuggish mode, I guess)
|
|
|
|
|
if (BufferWidth == 352) { } //a large basic framebuffer size
|
|
|
|
|
else if (BufferWidth == 341) { } //a large basic framebuffer size with overscan cropped
|
|
|
|
|
else if (BufferWidth == 330) { } //a small basic framebuffer
|
|
|
|
|
else if (BufferWidth == 320) { } //a small basic framebuffer with overscan cropped
|
|
|
|
|
else isHorz2x = true;
|
|
|
|
|
|
|
|
|
|
int slStart = _isPal ? _settings.ScanlineStartPal : _settings.ScanlineStartNtsc;
|
|
|
|
|
int slEnd = _isPal ? _settings.ScanlineEndPal : _settings.ScanlineEndNtsc;
|
|
|
|
|
int slHeight = (slEnd - slStart) + 1;
|
|
|
|
|
|
|
|
|
|
if (BufferHeight == slHeight) { }
|
|
|
|
|
else isVert2x = true;
|
|
|
|
|
|
2017-07-15 07:53:33 +00:00
|
|
|
|
if (_settings.ResolutionMode == Settings.ResolutionModeTypes.PixelPro)
|
|
|
|
|
{
|
|
|
|
|
//this is the tricky one: need to adapt the framebuffer size
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-29 09:21:16 +00:00
|
|
|
|
switch (_settings.ResolutionMode)
|
2017-06-29 00:25:08 +00:00
|
|
|
|
{
|
2017-06-29 09:21:16 +00:00
|
|
|
|
case Settings.ResolutionModeTypes.HardcoreDebug:
|
|
|
|
|
_virtualWidth = BufferWidth;
|
|
|
|
|
_virtualHeight = BufferHeight;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case Settings.ResolutionModeTypes.Mednafen:
|
|
|
|
|
//this is mednafen's "correct AR" case.
|
|
|
|
|
//note that this will shrink a width from 330 to 302 (that's the nature of mednafen)
|
|
|
|
|
//that makes the bios saturn orb get stretched vertically
|
|
|
|
|
//note: these are never high resolution (use a 2x window size if you want to see high resolution content)
|
|
|
|
|
if (_isPal)
|
|
|
|
|
{
|
|
|
|
|
_virtualWidth = (_settings.HOverscan ? 365 : 354);
|
|
|
|
|
_virtualHeight = slHeight;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_virtualWidth = (_settings.HOverscan ? 302 : 292);
|
|
|
|
|
_virtualHeight = slHeight;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case Settings.ResolutionModeTypes.TweakedMednafen:
|
|
|
|
|
//same as mednafen but stretch up
|
|
|
|
|
//base case is 330x254
|
|
|
|
|
if (_isPal)
|
|
|
|
|
{
|
|
|
|
|
//this can be the same as Mednafen mode? we don't shrink down in any case, so it must be OK
|
|
|
|
|
_virtualWidth = (_settings.HOverscan ? 365 : 354);
|
|
|
|
|
_virtualHeight = slHeight;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
//ideally we want a height of 254, but we may have changed the overscan settings
|
|
|
|
|
_virtualWidth = (_settings.HOverscan ? 330 : 320);
|
|
|
|
|
_virtualHeight = BufferHeight * 254 / 240;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case Settings.ResolutionModeTypes.PixelPro:
|
|
|
|
|
//mednafen makes sure we always get 352 or 341 (if overscan cropped)
|
|
|
|
|
//really the only thing we do
|
|
|
|
|
//(that's not the best solution for us [not what psx does], but it will do for now)
|
|
|
|
|
_virtualWidth = BufferWidth * (isHorz2x ? 1 : 2); //not a mistake, we scale to make it bigger if it isn't already
|
|
|
|
|
_virtualHeight = BufferHeight * (isVert2x ? 1 : 2); //not a mistake
|
2017-07-15 07:53:33 +00:00
|
|
|
|
|
|
|
|
|
if (isHorz2x && isVert2x) { } //nothing to do
|
|
|
|
|
else if (isHorz2x && !isVert2x)
|
|
|
|
|
{
|
|
|
|
|
//needs double sizing vertically
|
|
|
|
|
AllocResizedBuffer(_virtualWidth, _virtualHeight);
|
|
|
|
|
for (int y = 0; y < BufferHeight; y++)
|
|
|
|
|
{
|
|
|
|
|
Buffer.BlockCopy(_videoBuffer, BufferWidth * y * 4, _resizedBuffer, BufferWidth * (y * 2 + 0) * 4, BufferWidth * 4);
|
|
|
|
|
Buffer.BlockCopy(_videoBuffer, BufferWidth * y * 4, _resizedBuffer, BufferWidth * (y * 2 + 1) * 4, BufferWidth * 4);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (!isHorz2x && isVert2x)
|
|
|
|
|
{
|
|
|
|
|
//needs double sizing horizontally
|
|
|
|
|
AllocResizedBuffer(_virtualWidth, _virtualHeight);
|
|
|
|
|
fixed (int* _pdst = &_resizedBuffer[0])
|
|
|
|
|
{
|
|
|
|
|
fixed (int* _psrc = &_videoBuffer[0])
|
|
|
|
|
{
|
|
|
|
|
int* psrc = _psrc;
|
|
|
|
|
int* pdst = _pdst;
|
|
|
|
|
for (int y = 0; y < BufferHeight; y++)
|
|
|
|
|
{
|
|
|
|
|
for (int x = 0; x < BufferWidth; x++) { *pdst++ = *psrc; *pdst++ = *psrc++; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
//needs double sizing horizontally and vertically
|
|
|
|
|
AllocResizedBuffer(_virtualWidth, _virtualHeight);
|
|
|
|
|
fixed (int* _pdst = &_resizedBuffer[0])
|
|
|
|
|
{
|
|
|
|
|
fixed (int* _psrc = &_videoBuffer[0])
|
|
|
|
|
{
|
|
|
|
|
int* psrc = _psrc;
|
|
|
|
|
int* pdst = _pdst;
|
|
|
|
|
for (int y = 0; y < BufferHeight; y++)
|
|
|
|
|
{
|
|
|
|
|
for (int x = 0; x < BufferWidth; x++) { *pdst++ = psrc[x]; *pdst++ = psrc[x]; }
|
|
|
|
|
for (int x = 0; x < BufferWidth; x++) { *pdst++ = psrc[x]; *pdst++ = psrc[x]; }
|
|
|
|
|
psrc += BufferWidth;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BufferWidth = _virtualWidth;
|
|
|
|
|
BufferHeight = _virtualHeight;
|
|
|
|
|
|
2017-06-29 09:21:16 +00:00
|
|
|
|
break;
|
2017-06-29 00:49:57 +00:00
|
|
|
|
}
|
2017-06-29 09:21:16 +00:00
|
|
|
|
|
2017-06-27 02:20:55 +00:00
|
|
|
|
}
|
2017-06-18 13:02:54 +00:00
|
|
|
|
}
|
|
|
|
|
}
|