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 { [Core("Saturnus", "Mednafen Team", true, true, "0.9.44.1", "https://mednafen.github.io/releases/", false)] public class Saturnus : WaterboxCore, IDriveLight, IRegionable, ISettable { 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 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; } [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."); } public Saturnus(CoreComm comm, IEnumerable disks, bool deterministic, Settings settings, SyncSettings syncSettings) :base(comm, new Configuration { MaxSamples = 8192, DefaultWidth = 320, DefaultHeight = 240, MaxWidth = 1024, MaxHeight = 1024, SystemId = "SAT" }) { 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(new PeRunnerOptions { Filename = "ss.wbx", SbrkHeapSizeKB = 128, 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 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; } protected override LibWaterboxCore.FrameInfo FrameAdvancePrep(IController controller, bool render, bool rendersound) { 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; } // if not reset, the core will maintain its own deterministic increasing time each frame if (!DeterministicEmulation) { _core.SetRtc((long)DateTime.Now.Subtract(new DateTime(1970, 1, 1)).TotalSeconds, _syncSettings.Language); } DriveLightOn = false; if (controller.IsPressed("Power")) _core.HardReset(); _core.SetControllerData(_controllerDeck.Poll(controller)); SetVideoParameters(); return new LibSaturnus.FrameInfo { ResetPushed = controller.IsPressed("Reset") ? 1 : 0 }; } public DisplayType Region => _isPal ? DisplayType.PAL : DisplayType.NTSC; #region ISettable public class Settings { 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; } // 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)] [TypeConverter(typeof(DescribableEnumConverter))] public SaturnusControllerDeck.Device Port1 { get; set; } [DisplayName("Port 2 Device")] [DefaultValue(SaturnusControllerDeck.Device.Gamepad)] [TypeConverter(typeof(DescribableEnumConverter))] public SaturnusControllerDeck.Device Port2 { get; set; } [DisplayName("Port 3 Device")] [DefaultValue(SaturnusControllerDeck.Device.None)] [Description("Only used if one or both multitaps are set")] [TypeConverter(typeof(DescribableEnumConverter))] public SaturnusControllerDeck.Device Port3 { get; set; } [DisplayName("Port 4 Device")] [DefaultValue(SaturnusControllerDeck.Device.None)] [Description("Only used if one or both multitaps are set")] [TypeConverter(typeof(DescribableEnumConverter))] public SaturnusControllerDeck.Device Port4 { get; set; } [DisplayName("Port 5 Device")] [DefaultValue(SaturnusControllerDeck.Device.None)] [Description("Only used if one or both multitaps are set")] [TypeConverter(typeof(DescribableEnumConverter))] public SaturnusControllerDeck.Device Port5 { get; set; } [DisplayName("Port 6 Device")] [DefaultValue(SaturnusControllerDeck.Device.None)] [Description("Only used if one or both multitaps are set")] [TypeConverter(typeof(DescribableEnumConverter))] public SaturnusControllerDeck.Device Port6 { get; set; } [DisplayName("Port 7 Device")] [DefaultValue(SaturnusControllerDeck.Device.None)] [Description("Only used if one or both multitaps are set")] [TypeConverter(typeof(DescribableEnumConverter))] public SaturnusControllerDeck.Device Port7 { get; set; } [DisplayName("Port 8 Device")] [DefaultValue(SaturnusControllerDeck.Device.None)] [Description("Only used if both multitaps are set")] [TypeConverter(typeof(DescribableEnumConverter))] public SaturnusControllerDeck.Device Port8 { get; set; } [DisplayName("Port 9 Device")] [DefaultValue(SaturnusControllerDeck.Device.None)] [Description("Only used if both multitaps are set")] [TypeConverter(typeof(DescribableEnumConverter))] public SaturnusControllerDeck.Device Port9 { get; set; } [DisplayName("Port 10 Device")] [DefaultValue(SaturnusControllerDeck.Device.None)] [Description("Only used if both multitaps are set")] [TypeConverter(typeof(DescribableEnumConverter))] public SaturnusControllerDeck.Device Port10 { get; set; } [DisplayName("Port 11 Device")] [DefaultValue(SaturnusControllerDeck.Device.None)] [Description("Only used if both multitaps are set")] [TypeConverter(typeof(DescribableEnumConverter))] public SaturnusControllerDeck.Device Port11 { get; set; } [DisplayName("Port 12 Device")] [DefaultValue(SaturnusControllerDeck.Device.None)] [Description("Only used if both multitaps are set")] [TypeConverter(typeof(DescribableEnumConverter))] public SaturnusControllerDeck.Device Port12 { get; set; } [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)] [TypeConverter(typeof(DescribableEnumConverter))] 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; public Settings GetSettings() { return _settings.Clone(); } public bool PutSettings(Settings s) { var ret = Settings.NeedsReboot(_settings, s); _settings = s; //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); return ret; } public SyncSettings GetSyncSettings() { return _syncSettings.Clone(); } public bool PutSyncSettings(SyncSettings s) { var ret = SyncSettings.NeedsReboot(_syncSettings, s); _syncSettings = s; return ret; } 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); } #endregion #region IStatable protected override void SaveStateBinaryInternal(BinaryWriter writer) { writer.Write(_activeDisk); writer.Write(_prevDiskSignal); writer.Write(_nextDiskSignal); } protected override void LoadStateBinaryInternal(BinaryReader reader) { _activeDisk = reader.ReadInt32(); _prevDiskSignal = reader.ReadBoolean(); _nextDiskSignal = reader.ReadBoolean(); // any managed pointers that we sent to the core need to be resent now! SetCdCallbacks(); //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); } #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); } public static void SetupTOC(LibSaturnus.TOC t, DiscTOC tin) { // 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; } } private void CDTOCCallback(int disk, [In, Out]LibSaturnus.TOC t) { SetupTOC(t, _disks[disk].TOC); } 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; private int _virtualWidth; private int _virtualHeight; public override int VirtualWidth => _virtualWidth; public override int VirtualHeight => _virtualHeight; 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() { //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? //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; //} _useResizedBuffer = false; 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; if (_settings.ResolutionMode == Settings.ResolutionModeTypes.PixelPro) { //this is the tricky one: need to adapt the framebuffer size } switch (_settings.ResolutionMode) { 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 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; break; } } } }