From 962d96c33b8e2941108abbe4e4c732e4bc2524ca Mon Sep 17 00:00:00 2001 From: Shane Lynch Date: Sat, 24 Feb 2024 23:00:51 -0500 Subject: [PATCH] Initial source commit for MiSTerCast. --- .gitignore | 398 +++++++++ External/.gitignore | 1 + External/README.txt | 1 + FrontEnd/App.config | 6 + FrontEnd/App.xaml | 8 + FrontEnd/App.xaml.cs | 11 + FrontEnd/HelpWindow.xaml | 14 + FrontEnd/HelpWindow.xaml.cs | 48 + FrontEnd/MainWindow.xaml | 265 ++++++ FrontEnd/MainWindow.xaml.cs | 718 +++++++++++++++ FrontEnd/MiSTerCast.csproj | 129 +++ FrontEnd/MiSTerCast.sln | 51 ++ FrontEnd/MiSTerCastInterop.cs | 69 ++ FrontEnd/Properties/AssemblyInfo.cs | 55 ++ FrontEnd/Properties/Resources.Designer.cs | 71 ++ FrontEnd/Properties/Resources.resx | 117 +++ FrontEnd/Properties/Settings.Designer.cs | 30 + FrontEnd/Properties/Settings.settings | 7 + FrontEnd/README.txt | 24 + FrontEnd/app.ico | Bin 0 -> 67646 bytes FrontEnd/modelines.dat | 9 + Library/MiSTerCastLib/AudioCapture.h | 145 ++++ Library/MiSTerCastLib/MiSTerCast.cpp | 106 +++ Library/MiSTerCastLib/MiSTerCastLib.cpp | 247 ++++++ Library/MiSTerCastLib/MiSTerCastLib.h | 99 +++ Library/MiSTerCastLib/MiSTerCastLib.vcxproj | 190 ++++ .../MiSTerCastLib.vcxproj.filters | 48 + Library/MiSTerCastLib/VideoCapture.h | 337 +++++++ Library/MiSTerCastLib/cpp.hint | 2 + Library/MiSTerCastLib/dllmain.cpp | 18 + Library/MiSTerCastLib/pch.cpp | 1 + Library/MiSTerCastLib/pch.h | 51 ++ Library/MiSTerCastLib/renderer_nogpu.h | 821 ++++++++++++++++++ 33 files changed, 4097 insertions(+) create mode 100644 .gitignore create mode 100644 External/.gitignore create mode 100644 External/README.txt create mode 100644 FrontEnd/App.config create mode 100644 FrontEnd/App.xaml create mode 100644 FrontEnd/App.xaml.cs create mode 100644 FrontEnd/HelpWindow.xaml create mode 100644 FrontEnd/HelpWindow.xaml.cs create mode 100644 FrontEnd/MainWindow.xaml create mode 100644 FrontEnd/MainWindow.xaml.cs create mode 100644 FrontEnd/MiSTerCast.csproj create mode 100644 FrontEnd/MiSTerCast.sln create mode 100644 FrontEnd/MiSTerCastInterop.cs create mode 100644 FrontEnd/Properties/AssemblyInfo.cs create mode 100644 FrontEnd/Properties/Resources.Designer.cs create mode 100644 FrontEnd/Properties/Resources.resx create mode 100644 FrontEnd/Properties/Settings.Designer.cs create mode 100644 FrontEnd/Properties/Settings.settings create mode 100644 FrontEnd/README.txt create mode 100644 FrontEnd/app.ico create mode 100644 FrontEnd/modelines.dat create mode 100644 Library/MiSTerCastLib/AudioCapture.h create mode 100644 Library/MiSTerCastLib/MiSTerCast.cpp create mode 100644 Library/MiSTerCastLib/MiSTerCastLib.cpp create mode 100644 Library/MiSTerCastLib/MiSTerCastLib.h create mode 100644 Library/MiSTerCastLib/MiSTerCastLib.vcxproj create mode 100644 Library/MiSTerCastLib/MiSTerCastLib.vcxproj.filters create mode 100644 Library/MiSTerCastLib/VideoCapture.h create mode 100644 Library/MiSTerCastLib/cpp.hint create mode 100644 Library/MiSTerCastLib/dllmain.cpp create mode 100644 Library/MiSTerCastLib/pch.cpp create mode 100644 Library/MiSTerCastLib/pch.h create mode 100644 Library/MiSTerCastLib/renderer_nogpu.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8dd4607 --- /dev/null +++ b/.gitignore @@ -0,0 +1,398 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml \ No newline at end of file diff --git a/External/.gitignore b/External/.gitignore new file mode 100644 index 0000000..e41d74f --- /dev/null +++ b/External/.gitignore @@ -0,0 +1 @@ +lz4/ \ No newline at end of file diff --git a/External/README.txt b/External/README.txt new file mode 100644 index 0000000..d771213 --- /dev/null +++ b/External/README.txt @@ -0,0 +1 @@ +Download https://github.com/lz4/lz4/releases/download/v1.9.4/lz4_win32_v1_9_4.zip and extract to External/lz4 \ No newline at end of file diff --git a/FrontEnd/App.config b/FrontEnd/App.config new file mode 100644 index 0000000..731f6de --- /dev/null +++ b/FrontEnd/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/FrontEnd/App.xaml b/FrontEnd/App.xaml new file mode 100644 index 0000000..9c87016 --- /dev/null +++ b/FrontEnd/App.xaml @@ -0,0 +1,8 @@ + + + + diff --git a/FrontEnd/App.xaml.cs b/FrontEnd/App.xaml.cs new file mode 100644 index 0000000..6d0c292 --- /dev/null +++ b/FrontEnd/App.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows; + +namespace MiSTerCast +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/FrontEnd/HelpWindow.xaml b/FrontEnd/HelpWindow.xaml new file mode 100644 index 0000000..615893b --- /dev/null +++ b/FrontEnd/HelpWindow.xaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/FrontEnd/HelpWindow.xaml.cs b/FrontEnd/HelpWindow.xaml.cs new file mode 100644 index 0000000..124d707 --- /dev/null +++ b/FrontEnd/HelpWindow.xaml.cs @@ -0,0 +1,48 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Markup; + +namespace MiSTerCast +{ + /// + /// Interaction logic for HelpWindow.xaml + /// + public partial class HelpWindow : Window + { + public HelpWindow() + { + InitializeComponent(); + TextBlock textBlock = (TextBlock)XamlReader.Parse( + "" + + File.ReadAllText("README.txt") + ""); + + foreach(Inline inline in textBlock.Inlines) + { + Hyperlink link = inline as Hyperlink; + if (link != null) + { + link.Click += Link_Click; + } + } + InfoScrollView.Content = textBlock; + } + + private void Link_Click(object sender, RoutedEventArgs e) + { + Hyperlink hl = sender as Hyperlink; + Uri uri = new Uri(((Run)hl.Inlines.FirstInline).Text); + Process.Start(new ProcessStartInfo(uri.AbsoluteUri)); + e.Handled = true; + } + + private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) + { + e.Cancel = true; + this.Hide(); + } + } +} diff --git a/FrontEnd/MainWindow.xaml b/FrontEnd/MainWindow.xaml new file mode 100644 index 0000000..7672e40 --- /dev/null +++ b/FrontEnd/MainWindow.xaml @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 192.168.1.2 + + + + + + + + + + + + + + + + 320x240 NTSC (60Hz) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Display 1 + Display 2 + Display 3 + Display 4 + + + Centered + TopLeft + Top + TopRight + Right + BottomRight + Bottom + BottomLeft + Left + + + No Rotate + 90° CCW + 90° CW + 180° + + Enable Audio + Enable Preview + + + + + + + + Custom Size + 1X Crop + 2X Crop + 3X Crop + 4X Crop + 5X Crop + Full 4:3 Crop + Full 5:4 Crop + + + + + + + + + + + + + + 320 + 240 + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FrontEnd/MainWindow.xaml.cs b/FrontEnd/MainWindow.xaml.cs new file mode 100644 index 0000000..7bfe57c --- /dev/null +++ b/FrontEnd/MainWindow.xaml.cs @@ -0,0 +1,718 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.ComponentModel; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Win32; + +namespace MiSTerCast +{ + struct Modeline + { + public string name; + public Double pclock; + public UInt16 hactive; + public UInt16 hbegin; + public UInt16 hend; + public UInt16 htotal; + public UInt16 vactive; + public UInt16 vbegin; + public UInt16 vend; + public UInt16 vtotal; + public bool interlace; + } + + struct SourceOptions + { + public byte display; + public bool audio; + public bool preview; + public byte alignment; + public byte cropmode; + public UInt16 width; + public UInt16 height; + public Int16 xoffset; + public Int16 yoffset; + public byte rotation; + } + + public partial class MainWindow : Window + { + private bool isInitialized = false; + private bool isStreaming = false; + HelpWindow helpWindow = null; + const string lastSaveFilename = "lastsave.dat"; + string currentSaveFilename = null; + + private void InitializeMiSTerCast() + { + if (LogDelegate == null) + LogDelegate = new MiSTerCastInterop.LogDelegate(LogCallback); + if (CaptureImageDelegate == null) + CaptureImageDelegate = new MiSTerCastInterop.CaptureImageDelegate(CaptureImage); + isInitialized = MiSTerCastInterop.Initialize(LogDelegate, CaptureImageDelegate); + if (isInitialized) + { + OnModelineChanged(); + } + } + + public MainWindow() + { + InitializeComponent(); + ReadModelinesFile(); + PopulateModelineDropdown(); + InitializeMiSTerCast(); + } + + void MainWindow_Closing(object sender, CancelEventArgs e) + { + if (isStreaming) + MiSTerCastInterop.StopStream(); + MiSTerCastInterop.Shutdown(); + helpWindow.Close(); + } + + private void Window_Closed(object sender, EventArgs e) + { + Application.Current.Shutdown(); + } + + private void HelpButton_Click(object sender, RoutedEventArgs e) + { + helpWindow.Show(); + } + + private void Window_Activated(object sender, EventArgs e) + { + if (helpWindow == null) + { + helpWindow = new HelpWindow(); + if (!File.Exists(lastSaveFilename)) + { + // Show help the first time MiSTerCast is opened + helpWindow.Show(); + try + { + File.Create(lastSaveFilename); + } + catch (Exception exception) + { + Log("Creating last save file failed: " + exception.Message, true); + } + } + else + { + try + { + currentSaveFilename = null; + using (StreamReader sr = File.OpenText(lastSaveFilename)) + { + if (!sr.EndOfStream) + { + currentSaveFilename = sr.ReadLine(); + if (!File.Exists(currentSaveFilename)) + { + currentSaveFilename = null; + Log("Last save is missing.", true); + } + } + } + + if (currentSaveFilename != null) + { + Log("Auto loading settings: " + currentSaveFilename); + using (StreamReader sr = File.OpenText(currentSaveFilename)) + { + LoadSaveFileFromStream(sr); + } + } + } + catch (Exception exception) + { + Log("Reading last save file failed: " + exception.Message, true); + } + } + } + } + + private void ToggleStreamButton_Click(object sender, RoutedEventArgs e) + { + if (isStreaming) + { + if (MiSTerCastInterop.StopStream()) + { + isStreaming = false; + ToggleStreamButton.Content = "Start Stream"; + CaptureSourceBox.IsEnabled = true; + EnableAudioCheckBox.IsEnabled = true; + ApplyModelineButton.IsEnabled = false; + } + } + else + { + if (!isInitialized) + InitializeMiSTerCast(); + + if (isInitialized) + { + EnablePreviewCheckBox.IsChecked = false; + if (MiSTerCastInterop.StartStream(TargetIpAddresTextBox.Text)) + { + isStreaming = true; + ToggleStreamButton.Content = "Stop Stream"; + CaptureSourceBox.IsEnabled = false; + EnableAudioCheckBox.IsEnabled = false; + } + } + } + } + + #region Settings + + const int SettingsVersion = 1; + + private void SaveSettingsButton_Click(object sender, RoutedEventArgs e) + { + try + { + SaveFileDialog saveFileDialog = new SaveFileDialog(); + saveFileDialog.Filter = "Settings File|*.sav"; + saveFileDialog.Title = "Save MiSTerCast settings"; + if (currentSaveFilename != null) + { + saveFileDialog.InitialDirectory = Path.GetDirectoryName(currentSaveFilename); + saveFileDialog.FileName = Path.GetFileName(currentSaveFilename); + } + + if (saveFileDialog.ShowDialog().Value) + { + if (!String.IsNullOrWhiteSpace(saveFileDialog.FileName)) + { + using (System.IO.FileStream fs = (System.IO.FileStream)saveFileDialog.OpenFile()) + { + using (var sw = new StreamWriter(fs)) + { + sw.WriteLine(SettingsVersion); + + sw.WriteLine(TargetIpAddresTextBox.Text); + + sw.WriteLine(ModelinePresetsBox.SelectedIndex); + sw.WriteLine(pclockTextBox.Text); + sw.WriteLine(hactiveTextBox.Text); + sw.WriteLine(hbeginTextBox.Text); + sw.WriteLine(hendTextBox.Text); + sw.WriteLine(htotalTextBox.Text); + sw.WriteLine(vactiveTextBox.Text); + sw.WriteLine(vbeginTextBox.Text); + sw.WriteLine(vendTextBox.Text); + sw.WriteLine(vtotalTextBox.Text); + sw.WriteLine(interlacedCheckBox.IsChecked.Value ? 1 : 0); + + sw.WriteLine(CaptureSourceBox.SelectedIndex); + sw.WriteLine(RotateComboBox.SelectedIndex); + sw.WriteLine(EnableAudioCheckBox.IsChecked.Value ? 1 : 0); + sw.WriteLine(CropComboBox.SelectedIndex); + sw.WriteLine(CaptureWidth.Text); + sw.WriteLine(CaptureHeight.Text); + sw.WriteLine(CaptureXOffset.Text); + sw.WriteLine(CaptureYOffset.Text); + + Log("Settings saved."); + } + } + + UpdateLastSaveFile(saveFileDialog.FileName); + } + } + } + catch (Exception exception) + { + Log("Save settings failed: " + exception.Message, true); + } + } + + private void LoadSettingsButton_Click(object sender, RoutedEventArgs e) + { + try + { + OpenFileDialog openFileDialog = new OpenFileDialog(); + openFileDialog.Filter = "Settings File|*.sav"; + openFileDialog.Title = "Load MiSTerCast settings"; + if (currentSaveFilename != null) + { + openFileDialog.InitialDirectory = Path.GetDirectoryName(currentSaveFilename); + openFileDialog.FileName = Path.GetFileName(currentSaveFilename); + } + + if (openFileDialog.ShowDialog().Value) + { + if (!String.IsNullOrWhiteSpace(openFileDialog.FileName)) + { + using (System.IO.FileStream fs = (System.IO.FileStream)openFileDialog.OpenFile()) + { + using (var sr = new StreamReader(fs)) + { + LoadSaveFileFromStream(sr); + UpdateLastSaveFile(openFileDialog.FileName); + } + } + } + } + } + catch (Exception exception) + { + Log("Load settings failed: " + exception.Message, true); + } + } + + private void LoadSaveFileFromStream(StreamReader sr) + { + int settingsVersion = int.Parse(sr.ReadLine()); + if (settingsVersion > SettingsVersion) + { + Log("Unsupported save file version: " + settingsVersion, true); + return; + } + + TargetIpAddresTextBox.Text = sr.ReadLine(); + + ModelinePresetsBox.SelectedIndex = Math.Min(int.Parse(sr.ReadLine()), ModelinePresetsBox.Items.Count - 1); + pclockTextBox.Text = sr.ReadLine(); + hactiveTextBox.Text = sr.ReadLine(); + hbeginTextBox.Text = sr.ReadLine(); + hendTextBox.Text = sr.ReadLine(); + htotalTextBox.Text = sr.ReadLine(); + vactiveTextBox.Text = sr.ReadLine(); + vbeginTextBox.Text = sr.ReadLine(); + vendTextBox.Text = sr.ReadLine(); + vtotalTextBox.Text = sr.ReadLine(); + interlacedCheckBox.IsChecked = sr.ReadLine() == "1" ? true : false; + + CaptureSourceBox.SelectedIndex = Math.Min(int.Parse(sr.ReadLine()), CaptureSourceBox.Items.Count - 1); + RotateComboBox.SelectedIndex = Math.Min(int.Parse(sr.ReadLine()), RotateComboBox.Items.Count - 1); + EnableAudioCheckBox.IsChecked = sr.ReadLine() == "1" ? true : false; + CropComboBox.SelectedIndex = Math.Min(int.Parse(sr.ReadLine()), CropComboBox.Items.Count - 1); + CaptureWidth.Text = sr.ReadLine(); + CaptureHeight.Text = sr.ReadLine(); + CaptureXOffset.Text = sr.ReadLine(); + CaptureYOffset.Text = sr.ReadLine(); + + Log("Settings loaded."); + } + + private void UpdateLastSaveFile(string fileName) + { + currentSaveFilename = fileName; + try + { + using (FileStream fs = File.OpenWrite(lastSaveFilename)) + { + using (StreamWriter sw = new StreamWriter(fs)) + { + sw.WriteLine(fileName); + } + } + } + catch (Exception exception) + { + Log("Save last save file for autoload failed: " + exception.Message, true); + } + } + + #endregion Settings + + #region Number Entry Validation + + private void PositiveIntValidation(object sender, TextCompositionEventArgs e) + { + int result; + e.Handled = + !(int.TryParse(((TextBox)sender).Text + e.Text, out result) && + result >= 0); + } + + private void IntValidation(object sender, TextCompositionEventArgs e) + { + int result; + string fullText = ((TextBox)sender).Text.Insert(((TextBox)sender).CaretIndex, e.Text); + e.Handled = !int.TryParse(fullText, out result) && fullText != "-"; + } + + private void PositiveDoubleValidation(object sender, TextCompositionEventArgs e) + { + double result; + e.Handled = + !(double.TryParse(((TextBox)sender).Text + e.Text, out result) && + result >= 0); + } + + #endregion Number Entry Validation + + #region Logs + + private MiSTerCastInterop.LogDelegate LogDelegate; + + private void Log(string message, bool error = false) + { + this.Dispatcher.InvokeAsync(() => + { + TextBlock logText = new TextBlock() { Text = message }; + if (error) + logText.Background = Brushes.Pink; + LogPanel.Children.Add(logText);//.Insert(0, (logText)); + }); + } + + private void LogCallback(string message, bool error) + { + Log(message, error); + } + + private bool autoScrollLogs = true; + private void LogScrollView_ScrollChanged(object sender, ScrollChangedEventArgs e) + { + if (e.ExtentHeightChange == 0) + { + if (LogScrollView.VerticalOffset == LogScrollView.ScrollableHeight) + autoScrollLogs = true; + else + autoScrollLogs = false; + } + + if (autoScrollLogs && e.ExtentHeightChange != 0) + LogScrollView.ScrollToVerticalOffset(LogScrollView.ExtentHeight); + } + + #endregion Logs + + #region Capture Source + + private SourceOptions currentSourceOptions; + + private void OnCaptureSourceChanged() + { + currentSourceOptions.display = (byte)CaptureSourceBox.SelectedIndex; + currentSourceOptions.alignment = (byte)AlignmentBox.SelectedIndex; + currentSourceOptions.rotation = (byte)RotateComboBox.SelectedIndex; + currentSourceOptions.cropmode = (byte)CropComboBox.SelectedIndex; + CaptureWidth.IsEnabled = CropComboBox.SelectedIndex == 0; + CaptureHeight.IsEnabled = CropComboBox.SelectedIndex == 0; + currentSourceOptions.audio = EnableAudioCheckBox.IsChecked.Value; + currentSourceOptions.preview = EnablePreviewCheckBox.IsChecked.Value; + ushort.TryParse(CaptureWidth.Text, out currentSourceOptions.width); + ushort.TryParse(CaptureHeight.Text, out currentSourceOptions.height); + short.TryParse(CaptureXOffset.Text, out currentSourceOptions.xoffset); + short.TryParse(CaptureYOffset.Text, out currentSourceOptions.yoffset); + + PreviewImage.Visibility = currentSourceOptions.preview ? Visibility.Visible : Visibility.Hidden; + PreviewDisabledLabel.Visibility = currentSourceOptions.preview ? Visibility.Hidden : Visibility.Visible; + + if (currentSourceOptions.width > 0 && currentSourceOptions.height > 0) + { + MiSTerCastInterop.SetSource( + currentSourceOptions.display, + currentSourceOptions.audio, + currentSourceOptions.preview, + currentSourceOptions.alignment, + currentSourceOptions.cropmode, + currentSourceOptions.width, + currentSourceOptions.height, + currentSourceOptions.xoffset, + currentSourceOptions.yoffset, + currentSourceOptions.rotation); + } + } + + private void CaptureSource_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (isInitialized) + OnCaptureSourceChanged(); + } + + private void CaptureSource_Checked(object sender, RoutedEventArgs e) + { + if (isInitialized) + OnCaptureSourceChanged(); + } + + private void CaptureSource_TextChanged(object sender, TextChangedEventArgs e) + { + if (isInitialized) + { + OnCaptureSourceChanged(); + } + } + + private void UpdateCropSize() + { + CaptureWidth.TextChanged -= CaptureSource_TextChanged; + CaptureHeight.TextChanged -= CaptureSource_TextChanged; + switch (currentSourceOptions.cropmode) + { + case 1: + CaptureWidth.Text = (currentModeLine.hactive).ToString(); + CaptureHeight.Text = (currentModeLine.vactive).ToString(); + break; + case 2: + CaptureWidth.Text = (currentModeLine.hactive * 2).ToString(); + CaptureHeight.Text = (currentModeLine.vactive * 2).ToString(); + break; + case 3: + CaptureWidth.Text = (currentModeLine.hactive * 3).ToString(); + CaptureHeight.Text = (currentModeLine.vactive * 3).ToString(); + break; + case 4: + CaptureWidth.Text = (currentModeLine.hactive * 4).ToString(); + CaptureHeight.Text = (currentModeLine.vactive * 4).ToString(); + break; + case 5: + CaptureWidth.Text = (currentModeLine.hactive * 5).ToString(); + CaptureHeight.Text = (currentModeLine.vactive * 5).ToString(); + break; + default: + break; + } + CaptureWidth.TextChanged += CaptureSource_TextChanged; + CaptureHeight.TextChanged += CaptureSource_TextChanged; + } + + #endregion Capture Source + + #region Modelines + + private Modeline currentModeLine; + private List modelines; + + private void OnModelineChanged() + { + double.TryParse(pclockTextBox.Text, out currentModeLine.pclock); + ushort.TryParse(hactiveTextBox.Text, out currentModeLine.hactive); + ushort.TryParse(hbeginTextBox.Text, out currentModeLine.hbegin); + ushort.TryParse(hendTextBox.Text, out currentModeLine.hend); + ushort.TryParse(htotalTextBox.Text, out currentModeLine.htotal); + ushort.TryParse(vactiveTextBox.Text, out currentModeLine.vactive); + ushort.TryParse(vbeginTextBox.Text, out currentModeLine.vbegin); + ushort.TryParse(vendTextBox.Text, out currentModeLine.vend); + ushort.TryParse(vtotalTextBox.Text, out currentModeLine.vtotal); + currentModeLine.interlace = interlacedCheckBox.IsChecked.Value; + + if (isInitialized && currentModeLine.pclock > 0 && currentModeLine.hactive > 0 && currentModeLine.vactive > 0) + { + MiSTerCastInterop.SetModeline( + currentModeLine.pclock, + currentModeLine.hactive, + currentModeLine.hbegin, + currentModeLine.hend, + currentModeLine.htotal, + currentModeLine.vactive, + currentModeLine.vbegin, + currentModeLine.vend, + currentModeLine.vtotal, + currentModeLine.interlace); + + UpdateCropSize(); + OnCaptureSourceChanged(); + } + } + + private void ApplyModelineButton_Click(object sender, RoutedEventArgs e) + { + OnModelineChanged(); + ApplyModelineButton.IsEnabled = false; + } + + private void SetModelineUI(Modeline modeline) + { + ignoreModelineChange = true; + pclockTextBox.Text = modeline.pclock.ToString(); + hactiveTextBox.Text = modeline.hactive.ToString(); + hbeginTextBox.Text = modeline.hbegin.ToString(); + hendTextBox.Text = modeline.hend.ToString(); + htotalTextBox.Text = modeline.htotal.ToString(); + vactiveTextBox.Text = modeline.vactive.ToString(); + vbeginTextBox.Text = modeline.vbegin.ToString(); + vendTextBox.Text = modeline.vend.ToString(); + vtotalTextBox.Text = modeline.vtotal.ToString(); + interlacedCheckBox.IsChecked = modeline.interlace; + ignoreModelineChange = false; + } + + private void ModelinePresetsBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + + if (ModelinePresetsBox.SelectedIndex > 0 && modelines != null && ModelinePresetsBox.SelectedIndex <= modelines.Count) + { + SetModelineUI(modelines[ModelinePresetsBox.SelectedIndex - 1]); + OnModelineChanged(); + } + } + + void ReadModelinesFile() + { + List newModeLines = new List(); + try + { + List lines = new List(File.ReadAllLines("modelines.dat")); + + for (int i = 0; i < lines.Count; i++) + { + Modeline modeline = new Modeline(); + bool badLine = false; + string line = lines[i].Trim(); + if (line.Length == 0 || line[0] == ';') + { + badLine = true; + } + else + { + int nameStart = line.IndexOf('['); + int nameEnd = line.IndexOf(']'); + if (nameStart == -1 || nameEnd == -1 || nameEnd <= nameStart + 1) + { + Log("Invalid modeline name format: " + lines[i], true); + badLine = true; + } + else + { + modeline.name = line.Substring(nameStart + 1, nameEnd - nameStart - 1); + if (String.IsNullOrEmpty(modeline.name)) + { + Log("Invalid modeline name format: " + lines[i], true); + badLine = true; + } + else + { + string[] values = line.Remove(nameStart, nameEnd - nameStart + 1) + .Split() + .Select(p => p.Trim()) + .Where(p => !string.IsNullOrWhiteSpace(p)) + .ToArray(); + if (values.Length != 10) + { + Log("Invalid modeline values count: " + lines[i], true); + badLine = true; + } + else + { + UInt16 interlace; + if (!Double.TryParse(values[0], out modeline.pclock) || + !UInt16.TryParse(values[1], out modeline.hactive) || + !UInt16.TryParse(values[2], out modeline.hbegin) || + !UInt16.TryParse(values[3], out modeline.hend) || + !UInt16.TryParse(values[4], out modeline.htotal) || + !UInt16.TryParse(values[5], out modeline.vactive) || + !UInt16.TryParse(values[6], out modeline.vbegin) || + !UInt16.TryParse(values[7], out modeline.vend) || + !UInt16.TryParse(values[8], out modeline.vtotal) || + !UInt16.TryParse(values[9], out interlace)) + + { + Log("Invalid modeline values format: " + lines[i], true); + badLine = true; + } + else + { + modeline.interlace = interlace != 0; + newModeLines.Add(modeline); + } + } + } + } + } + + if (badLine) + { + lines.RemoveAt(i); + i--; + } + } + + if (newModeLines.Count == 0) + throw new Exception("No valid modelines."); + + modelines = newModeLines; + } + catch (Exception e) + { + Log("Failed to read modelines.dat. " + e.Message, true); + } + } + + void PopulateModelineDropdown() + { + ModelinePresetsBox.Items.Clear(); + ModelinePresetsBox.Items.Add("Custom"); + foreach (Modeline modeline in modelines) + { + ModelinePresetsBox.Items.Add(modeline.name); + } + + ModelinePresetsBox.SelectedIndex = modelines.Count > 0 ? 1 : 0; + } + + bool ignoreModelineChange = false; + private void ModeLineTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + OnManualModelineChange(); + } + + private void InterlacedCheckBox_Checked(object sender, RoutedEventArgs e) + { + OnManualModelineChange(); + } + + private void OnManualModelineChange() + { + if (!ignoreModelineChange) + { + ModelinePresetsBox.SelectedIndex = 0; + if (isStreaming) + ApplyModelineButton.IsEnabled = true; + } + } + + #endregion Modelines + + #region Preview + + private MiSTerCastInterop.CaptureImageDelegate CaptureImageDelegate; + private bool isPreviewEnabled = true; + + public void CaptureImage(int width, int height, IntPtr buffer) + { + if (isPreviewEnabled) + { + BitmapSource source = CreateBitmapSource(width, height, buffer); + source.Freeze(); + this.Dispatcher.InvokeAsync(() => + { + PreviewImage.Source = source; + }); + } + } + + [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)] + public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); + + public BitmapSource CreateBitmapSource(int width, int height, IntPtr buffer) + { + WriteableBitmap writableImg = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgra32, null); + + writableImg.Lock(); + CopyMemory(writableImg.BackBuffer, buffer, (uint)(4 * width * height)); + writableImg.AddDirtyRect(new Int32Rect(0, 0, width, height)); + writableImg.Unlock(); + + return writableImg; + } + + #endregion Preview + } +} diff --git a/FrontEnd/MiSTerCast.csproj b/FrontEnd/MiSTerCast.csproj new file mode 100644 index 0000000..ba2ab6e --- /dev/null +++ b/FrontEnd/MiSTerCast.csproj @@ -0,0 +1,129 @@ + + + + + Debug + AnyCPU + {9F56815A-0D3B-49AE-95DF-8A5124B2FF6B} + WinExe + MiSTerCast + MiSTerCast + v4.6.1 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + app.ico + + + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + HelpWindow.xaml + + + MainWindow.xaml + Code + + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + PreserveNewest + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + + PreserveNewest + + + + + + + + + xcopy "$(SolutionDir)..\External\lz4\dll\msys-lz4-1.dll" "$(SolutionDir)$(OutDir)msys-lz4-1.dll*" /y +if $(ConfigurationName) == Release xcopy "$(SolutionDir)$(ProjDir)Release\MISTERCASTLIB.dll" "$(SolutionDir)$(OutDir)MISTERCASTLIB.dll*" /y +if $(ConfigurationName) == Debug xcopy "$(SolutionDir)$(ProjDir)Debug\MISTERCASTLIB.dll" "$(SolutionDir)$(OutDir)MISTERCASTLIB.dll*" /y +if $(ConfigurationName) == Debug xcopy "$(SolutionDir)$(ProjDir)Debug\MISTERCASTLIB.pdb" "$(SolutionDir)$(OutDir)MISTERCASTLIB.pdb*" /y + + \ No newline at end of file diff --git a/FrontEnd/MiSTerCast.sln b/FrontEnd/MiSTerCast.sln new file mode 100644 index 0000000..c8f4eff --- /dev/null +++ b/FrontEnd/MiSTerCast.sln @@ -0,0 +1,51 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.1705 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiSTerCast", "MiSTerCast.csproj", "{9F56815A-0D3B-49AE-95DF-8A5124B2FF6B}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MiSTerCastLib", "..\Library\MiSTerCastLib\MiSTerCastLib.vcxproj", "{3F2B9AA9-BCC6-475B-8B39-3562BFE8CEB7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9F56815A-0D3B-49AE-95DF-8A5124B2FF6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F56815A-0D3B-49AE-95DF-8A5124B2FF6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F56815A-0D3B-49AE-95DF-8A5124B2FF6B}.Debug|x64.ActiveCfg = Debug|Any CPU + {9F56815A-0D3B-49AE-95DF-8A5124B2FF6B}.Debug|x64.Build.0 = Debug|Any CPU + {9F56815A-0D3B-49AE-95DF-8A5124B2FF6B}.Debug|x86.ActiveCfg = Debug|Any CPU + {9F56815A-0D3B-49AE-95DF-8A5124B2FF6B}.Debug|x86.Build.0 = Debug|Any CPU + {9F56815A-0D3B-49AE-95DF-8A5124B2FF6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F56815A-0D3B-49AE-95DF-8A5124B2FF6B}.Release|Any CPU.Build.0 = Release|Any CPU + {9F56815A-0D3B-49AE-95DF-8A5124B2FF6B}.Release|x64.ActiveCfg = Release|Any CPU + {9F56815A-0D3B-49AE-95DF-8A5124B2FF6B}.Release|x64.Build.0 = Release|Any CPU + {9F56815A-0D3B-49AE-95DF-8A5124B2FF6B}.Release|x86.ActiveCfg = Release|Any CPU + {9F56815A-0D3B-49AE-95DF-8A5124B2FF6B}.Release|x86.Build.0 = Release|Any CPU + {3F2B9AA9-BCC6-475B-8B39-3562BFE8CEB7}.Debug|Any CPU.ActiveCfg = Debug|Win32 + {3F2B9AA9-BCC6-475B-8B39-3562BFE8CEB7}.Debug|Any CPU.Build.0 = Debug|Win32 + {3F2B9AA9-BCC6-475B-8B39-3562BFE8CEB7}.Debug|x64.ActiveCfg = Debug|x64 + {3F2B9AA9-BCC6-475B-8B39-3562BFE8CEB7}.Debug|x64.Build.0 = Debug|x64 + {3F2B9AA9-BCC6-475B-8B39-3562BFE8CEB7}.Debug|x86.ActiveCfg = Debug|Win32 + {3F2B9AA9-BCC6-475B-8B39-3562BFE8CEB7}.Debug|x86.Build.0 = Debug|Win32 + {3F2B9AA9-BCC6-475B-8B39-3562BFE8CEB7}.Release|Any CPU.ActiveCfg = Release|Win32 + {3F2B9AA9-BCC6-475B-8B39-3562BFE8CEB7}.Release|Any CPU.Build.0 = Release|Win32 + {3F2B9AA9-BCC6-475B-8B39-3562BFE8CEB7}.Release|x64.ActiveCfg = Release|x64 + {3F2B9AA9-BCC6-475B-8B39-3562BFE8CEB7}.Release|x64.Build.0 = Release|x64 + {3F2B9AA9-BCC6-475B-8B39-3562BFE8CEB7}.Release|x86.ActiveCfg = Release|Win32 + {3F2B9AA9-BCC6-475B-8B39-3562BFE8CEB7}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2661CD4A-FB24-45C8-8AEF-12ECFF8D2975} + EndGlobalSection +EndGlobal diff --git a/FrontEnd/MiSTerCastInterop.cs b/FrontEnd/MiSTerCastInterop.cs new file mode 100644 index 0000000..c47f795 --- /dev/null +++ b/FrontEnd/MiSTerCastInterop.cs @@ -0,0 +1,69 @@ +using System; +using System.Runtime.InteropServices; + +namespace MiSTerCast +{ + class MiSTerCastInterop + { + /* + int ALIGMENT_CENTER = 0; + int ALIGMENT_TOP_LEFT = 1; + int ALIGMENT_TOP = 2; + int ALIGMENT_TOP_RIGHT = 3; + int ALIGMENT_RIGHT = 4; + int ALIGMENT_BOTTOM_RIGHT = 5; + int ALIGMENT_BOTTOM = 6; + int ALIGMENT_BOTTOM_LEFT = 7; + int ALIGMENT_LEFT = 8; + + int ROTATE_NONE = 0; + int ROTATE_CCW = 1; + int ROTATE_CW = 2; + int ROTATE_180 = 3; + */ + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void LogDelegate(string message, bool error); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate void CaptureImageDelegate(int width, int height, IntPtr buffer); + + [DllImport("MISTERCASTLIB.dll", EntryPoint = "Initialize", CallingConvention = CallingConvention.Cdecl)] + public static extern bool Initialize(LogDelegate logCallback, CaptureImageDelegate captureImageCallback); + + [DllImport("MISTERCASTLIB.dll", EntryPoint = "Shutdown", CallingConvention = CallingConvention.Cdecl)] + public static extern bool Shutdown(); + + [DllImport("MISTERCASTLIB.dll", EntryPoint = "StartStream", CallingConvention = CallingConvention.Cdecl)] + public static extern bool StartStream(string targetIp); + + [DllImport("MISTERCASTLIB.dll", EntryPoint = "StopStream", CallingConvention = CallingConvention.Cdecl)] + public static extern bool StopStream(); + + [DllImport("MISTERCASTLIB.dll", EntryPoint = "SetModeline", CallingConvention = CallingConvention.Cdecl)] + public static extern bool SetModeline( + Double pclock, + UInt16 hactive, + UInt16 hbegin, + UInt16 hend, + UInt16 htotal, + UInt16 vactive, + UInt16 vbegin, + UInt16 vend, + UInt16 vtotal, + bool interlace); + + [DllImport("MISTERCASTLIB.dll", EntryPoint = "SetSource", CallingConvention = CallingConvention.Cdecl)] + public static extern bool SetSource( + byte display, + bool audio, + bool preview, + byte alignment, + byte cropmode, + UInt16 width, + UInt16 height, + Int16 xoffset, + Int16 yoffset, + byte rotation); + } +} diff --git a/FrontEnd/Properties/AssemblyInfo.cs b/FrontEnd/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..8fe8d6f --- /dev/null +++ b/FrontEnd/Properties/AssemblyInfo.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("MiSTerCast")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MiSTerCast")] +[assembly: AssemblyCopyright("Copyright © 2024")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/FrontEnd/Properties/Resources.Designer.cs b/FrontEnd/Properties/Resources.Designer.cs new file mode 100644 index 0000000..12206cc --- /dev/null +++ b/FrontEnd/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace MiSTerCast.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MiSTerCast.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/FrontEnd/Properties/Resources.resx b/FrontEnd/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/FrontEnd/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/FrontEnd/Properties/Settings.Designer.cs b/FrontEnd/Properties/Settings.Designer.cs new file mode 100644 index 0000000..99ac4c3 --- /dev/null +++ b/FrontEnd/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace MiSTerCast.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/FrontEnd/Properties/Settings.settings b/FrontEnd/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/FrontEnd/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/FrontEnd/README.txt b/FrontEnd/README.txt new file mode 100644 index 0000000..57d578f --- /dev/null +++ b/FrontEnd/README.txt @@ -0,0 +1,24 @@ +MiSTerCast 1.00 + +MiSTerCast is a general-purpose tool for streaming your PC screen to your MiSTer through the Groovy_MiSTer core. This is not a replacement for Groovy_Mame or other integrated emulators. + +Make sure you already have Groovy_Mame working well with Groovy_MiSTer before using MiSTerCast. A direct ethernet connection to your MiSTer is recommended. +https://github.com/lutechsource/MiSTerStuff/blob/main/GroovyMiSTer/mame_documentation.md +https://github.com/psakhis/Groovy_MiSTer + +For audio, you will need to enable audio on the Groovy_MiSTer core. + +Known issues +- Frames may be dropped or doubled due to sync with video signal. +- At least 1-2 frames of latency. +- If the app crashes, you will need to restart your MiSTer and Groovy_MiSTer. +- Nothing over 720x480i is recommended at the moment due to throughput on MiSTer. This will improve soon. +- High refresh rate monitors are not supported due to frame times. Please change your monitor to ~60hz. + +Notes +The current pre-defined modelines are just for testing. You can add your own in modelines.dat. +It's best to use a refresh that matches your PC for better sync. +Find more modeline examples here: https://www.geocities.ws/podernixie/htpc/modes-en.html + +Contact +You can find me on the official MiSTer Discord \ No newline at end of file diff --git a/FrontEnd/app.ico b/FrontEnd/app.ico new file mode 100644 index 0000000000000000000000000000000000000000..d036beeb7b77beb5eee0b9f36c8d34e30fa0b104 GIT binary patch literal 67646 zcmeI51+*2#8^9kXc7TO}Eh;K1Sg0t+M(hR^yAWHk5Nzy3tfOLgccCa^cVJ+5cRT<0 zoBtWkFmGmdW_R|!d+)o$d2HO>otf{O`R40sYHHW~Z|Kmb=Ktq!>e9Yl)9g)6OmMjLG; z)>>;V(X(ey(W6HXvEqs=iq%(NU97wAx?8F>C#2?>C;CXe)!?y=9_O8@4WMlq!%>~x|(f1IRD~{FT`DU-6ak< z-~h403M)uFwQJWd7oKCr2V-L1dFK@yZn&X1{q)nt^Upsoe*N{=nuTR8H5@oU@x&A2 zkV6g;%Pg~uXy3kl>G<}I4aQr?jvdAN>#r{^xZnaYVZsCfkI^V;A@I#N--v6ky;jb- znP;B4RdMbb50g(mxmakSg(N*00$_19mEzq#u;Z6 z3of{zSZSq|#3q|;B6i(%SE&m;>Zqf{(4j-+A9|gE0|$y7cGy9zv(7rATeohaQ>RXH z4Jt#YK)ZVP?k%2v`sucXaILpre)*-?XP%S$PB`HNaou&-iC15JRs8tlkK%_Peh_cJ{kC}U!3U)t zm~pZ6(o0KyVoI8{zyb@1tFOMg&8-LH$B&o#RPa?4p3G0?Dm>#w7hNP?dF2&p+tG%a z`1I3H#TjRuAy!^_Wl0ZgbDFgW-UAx|dVgJ~2VeTtS6_+Iqen}6fqr?08D=QlcKQx| zf;ZIb)7xHe=h#!A#YOQx_0&_#@0@z-sp9q5Ul)J=`DY7IA$#9&!wq8DWtWYz10Z{B zvBefrZfo(;<{Qw6M;>`Z?7Q#2f;n7t9HZM{{lSh=&js4swQJWxxJT#98V$Yv_S_uhN2vkvG2wxAPFJW(vM#1e(Z0rUskBXUqZ7I2RpEiW#n zm|_a44?-`*{PPuP!H^+ClIRSvBi?`i{l4ntZy$d6p`_!`gS>r&j`Z)}zwX8WG{4Ba zpMU=OrL8S=Zov0p`^}37^Sz~(T1ve6=9`&g09lr`Xt&*V%grA!26ozMC-MFF-)GiW z3lG5Sj2dg-NL{VDW#@x>SS;vW7F{tda1u@y2_z)7eN2+!dsfE;O#rI2gZ z0{m5wk88PLqsLAj(r5FTIp&x{&Ut)!VhVD-`7Lz~T=Dri`Q($O90<++;DZk&UymOF zb{qAqd7NpcnZ!d6JrvUvX%A*ihgUVfmvzlMwDX^T{)wuyS{`QXBH!C&t*pKyYqRFY z+R~BpeRCff5nnD_p{_gcyi;`U+}Vk%U3S?e8XxE#@Dsrnl+|zZcXWmDJGEHOKmYt( z{u%K0tf8&~ZT;kvPo&=s`WbwH?z!h4IiJrt=Nu=`#~SqRyYIR_JMljLg40ertrb_; z_wT#!KJoCw4^ONYodE5{R_&Urj4jrKJiOr|i!35uc;SVK?W=_oc>THOp4$pTbBwEN z_@1q{+DhWq>?6Lp=t|MkZLq-x0+`^Jhb#z>0Ii&Ey6NPf`ksEoh74Xq3UVWQV*Pz} zP8-qJsCucJvBFw~KN7tArI%hReNSz)3xCu+d@yUMPu30TSEZhTN5{Wb!7StmT({qT zd#Pj7+X>#8+tBSJAAW33qB`#)-CFU$AC^8bkIRkpKOi;e1p(O>Fvup z=bPvlkT0sa&^~mT;4`aF>UZG0-FDlxka=Hw?KL?jQs`Ov#?eO~E%pCd@I%&c&41PA z&?CA7@W$GpmuH`SRv=S`euwAKagQB4R{u=ZokOEz=|2AWI!{In20YKAi!K@kpDb81 zCVe(Hbj9cmkO{NC74tiDcfIx2lQOK1u*V;NJi0Dy6U41lV}ak_W}9uIUTksvDMvc-xQ8ni`-E@)>&so z$w}S2cenD$>YJ@`WPKxs%7r8IKD_$nmtQ_fQ{m6iv6#=A*YMxiU(t`9d+xc?eh%F} z<&;yT-W2{CzTAAzyoL@i{?0q^ydWP0Xwn*MtRW6O@Id(vc6;g_K76>?amO8{-XX+S z72O8BjVjO{d{WHzxvm-O#IsdX898#Kz&_(@vw45dJ@=eMmx_FwweCWjkrAMwF8dF> zHaf>^uDM36x#pT83m@3`@E0-Lr(Yv?5L1Rs>souM7as%0!)mLomI({iaePcvAyzR) zR}38qu}7%ymw7}Nv)ppad10u-bIB!_oJ0?Uehj%^f5trL9rP!m`gi(^EuHlw3%_Pv z`Zckk4?OTdlz$HLFEWr%-^aLwcIk!qmYB7V7^rHH2Z3?u`b)p@(b3C|H{R%_zr^X{ zKcA-npWAQ0-K*2p@vWct?b|m>XP~zadVmar%%*=+KZhUGbrR^Ew%l?{=kxlzoFiwo zwq6jr<WxaF2xWSpM*Zq^b1LF__B3p|2PmchQG z7uJ{(u&IxQuF?J$s4klcqks0{{^9lX7n%h}N>?6c==>`6m zco^He@J_mJK!2~uIWgKk8bF+u&-R$N?$b^?&Du};e#8++wD>H3i#E8}?m;I*{L@-g zQs4$T4}83h*-rfIP1`7NcEx&O<3lb^U9kV6cQV_juE`Hm#(Ijp?}8V7HRmt9M~r=7 z1&@VYb0_eatb(Seu2u&qj;sGcsXW1^u0^t>_HRwK!!P z&>`9CMHg-JOCeT?coo|_wtLvzb=fLq+ts(AQ~mn&3vQDsc^lj!di zJx5<_tCw|j+_-T?Kks~>m`_{1%q1Ir239$LoaM=iKRKww^+l_Tl?zQ ze?xcb%Xh%ow#miV5wIneS>Qd12SoqEAO1dN$6k>KM2C+$M~|o5z|DT+-{{hjsBfqv zYbSn}WfXL1q55fP(Pfui=4}RXRW6%rnf5K@5Oj**fA78bo){;L?{HZKS;plPhi!wk z-c|5{LoQJ`&N^a8O5GP^b^LO(>Q=vp@FLJ*^aAP~{fJMHDHFNi6LTN_A6eX#JE8x; z4S(V?3c50vUljUpaO^Aa+@X45XsW4K_I*3#w-`PoR2G3(b?M`<+2JFXvY^vL|AX&= zKGyVee9rLCOkJQgwzZDB;R~_~Gz(g#x7Bx!UE8;&WYwqtjy)593txTuZ}1543L%A< z8~t7Voc^ZN#b5^vu?1sq#IBX9K#N`ezUV(pUo=;}_@&}cs>8^4jvhNjVa|m5c3|s@ zp`kqMioL_{f?P={3UVDW13q}^ziqbJW-WYn^R$C`>$Cr`=3tiyE#Opdr|-OTa~u=7 zFp3m>T4H=Vh;?I)4K2u9KE8r^qx(vQepi1E-Aare1(?DQklUfC4w`k-Codm+?6G=N zGtZ&vF@DiDy?bUQ+5_-Pz`{4q)Nk-qdGsgvN%(U8;ZNn#k7!M`p0aku$gHX_9Fa}X z6`6JmYemLQ-of^7@|pVg!6P|_Vhg;TiNm0+I&Yuf2XZ<*VV;6LnOHN`#;oJcJMWy!jvnLN!CI4d zjsgShiCO-i_>bU2@aUtDX6otS8_^-S^b1)q(|^Y%jGv0HFz#6^FKgC$VAIaB+hBt# zqHn@?FslyL2jV1S_36l~S@=-j=Sa+b(GuhT&69_FZ2+A-^H_lXo@L_GR|B8m`gTk+T9hw2Xp%n6VYw zS7M@EJ|e99=sZHdguFwzcMS%nNwuLk~Sv`jY06iDO`j-FRP~1((OJgoe8D|37CJT;tho>0#=Oj;m$VbR zx=j{ke1__jL;A}z*l)=X5L0~lTV3_W=68!}kL}q!`Qh+$h}FZRvr8@i0xMf=Gi!p& zAI}B%toy7rG4^}-{XDtXSYxpZRjOcn$m+Z8cYJALipxjd_U+Jn=nHJN0Pgu>qe8#0 zKL`F};wh2$U4F9qcg=HnF#Mz|Rbuk>=xt?^QPuIYIW$};x2R#FJ2mEo_cPo$ovckVQV!uG=MLY&R zZ^*wPzDEuGR|fy+{*deNp+x73PTpqYQ}`@;ByK0f<{XQE{J;A2=@ZR+(w5l0nTo&w zyN_+X%erUg{LE@YsQgC`#oMuVo$D07I-RO?6Qt`&ao5c ziT7!X_y>>JqR5SE!qL1&SB>973LZCh{4ke8>?Qci;B%$tdk?Lvt>Pc}U^he;?cy_u zr6B&Nl62q18@6q7A-ZCGv8S@PNM8B7E#n_pvlifI2aNGuCbo{;FvOg|@8$6?GI3b+ z8rc}TKI;LtOZ;AlwZM-7za(thc}rWzzk(aGEb+YPuS5Lk7#H|V;Y)=7IQ$x8DWsV1 zzWCycLAh0V4_ZVUQ~7%~@E?M)NuObyur6S~Koar8&fU6-C3J%QTy%lvZIyZD)=jNdcsw_P{zZ-a{a9ytZc6JW{-`ZaQeO%4xv zmwaV5-+6b7iv7~Se>nW0>+Bh!pZz?2x$$XB^^7rQl{e)*}8@KZsvs{O7fis6kUp_{W!S z;J|^&a+=gK{uu|1|Fzd%Th50N{l3Dveyj!jlY3{xh!HZLSB?J^$6Cj~f&n>xm=}5S z;#M>Mu>oQuBv(jLTCZTGAL|AGs!znJ5i9G;Ri)2+bzZIbCyz2djj=fm6f8oH^@e}? zOiVBHg?STVL$5aclbek_?(oGA$;%LeXV)|Jh=0{rbjavyh(+<0YQjG;w)muGXkg7nybC_h=1}HV9zJNvFYOv%z-^Ph>aC6<-JT8YcIOYoEgu zZK)UhL+ktZ?=SuJO+OL*_eYHyC1DLekOFQgzk#1QGXL1IV_U?dLBGhO5wbT_o;KAw z{=p}{we0(D(t2Y1h~r=%zm&P@^1wOd9b~`3g9lspMS-3Xub6sGsAc?ZwrrGH1BdP;U8L$o(4a9GY>6xp3$R6%XlkS3{RQ(D(l)N%e+(#1nrWe)(bO!Lg&_x@LefO#z|MZC*gzPP7>PyIrFmmKbDZ{3q^@v?jqMp?^rZ1s)7;o1KdIXOkXF~gwQ;5?CDh29)mV`u}@@H zoUQ63V_f-k68C|xZ|FXA>Km@3(!-A%H?GCHVrzG*d#o$OkfyRbK>whF&_iY8hF9#? zty|Fi@7c3w3pq1YUv1w(E+_wv3orV8_Aeoi&!9nrr2Z>~|0A@%d-v{MJDjcEsqbNT z95!rN@P5fUTx{p~zmneuA6R6hP#yp~h$&yD?yLSi)^f(Htq*12Lxuq6witfBjg>lQ zO@OaOmyw5ePXVvt!-rR@yA$hSFOw8)5B-LcA;@WL$`BJ9)ru#IcTb_u355yI!AD{X zt#l!7i+IkMw&r;j-5BzaO$K8Ms-mX6;AJ7~Y+8A95Z@)>kFDP&?-->{RQ8?)I4 zQuZVDx8U8`w|KyS0m=7o(_z)IW5?({_BH&|`AEFcw(v(Lq zFVzT#kTx-w$peBch5Z+svx3!Ld+jB201{JDtFS^>10Rge5IaCypz$Giw><-_;Cmae lvO%YDuK|Gu1X4qwnXhb=1_T-qXh5I=fd&K`5NKl%_#eS&=k)*p literal 0 HcmV?d00001 diff --git a/FrontEnd/modelines.dat b/FrontEnd/modelines.dat new file mode 100644 index 0000000..2bd99e0 --- /dev/null +++ b/FrontEnd/modelines.dat @@ -0,0 +1,9 @@ +; Add your modelines here +; Original modelines were found here https://www.geocities.ws/podernixie/htpc/modes-en.html + +;Name Pclock HDis HStr HEnd HTot VDsp VStr VEnd VTot Int +[256x240 NTSC (60Hz)] 4.905 256 264 287 312 240 241 244 262 0 +[320x240 NTSC (60Hz)] 6.700 320 336 367 426 240 244 247 262 0 +[320x480i NTSC (60Hz)] 6.700 320 336 367 426 480 488 493 525 1 +[640x480i NTSC (60Hz)] 12.336 640 662 720 784 480 488 494 525 1 +[720x480i NTSC (60Hz)] 13.846 720 744 809 880 480 488 494 525 1 \ No newline at end of file diff --git a/Library/MiSTerCastLib/AudioCapture.h b/Library/MiSTerCastLib/AudioCapture.h new file mode 100644 index 0000000..fdd4a7f --- /dev/null +++ b/Library/MiSTerCastLib/AudioCapture.h @@ -0,0 +1,145 @@ +#pragma once + +// REFERENCE_TIME time units per second and per millisecond +#define REFTIMES_PER_SEC 10000000 +#define REFTIMES_PER_MILLISEC 10000 + +// Buffers +#define AUDIO_BUFFER_SIZE 10000000 +std::atomic_uint AudioWritePos = 0; +std::atomic_uint AudioReadPos = 0; +std::atomic_int audioSampleRate; +uint16_t audioBuffer[AUDIO_BUFFER_SIZE] = {}; +uint16_t audioSendBuffer[AUDIO_BUFFER_SIZE] = {}; + +// Audio Capture +REFERENCE_TIME hnsRequestedDuration = REFTIMES_PER_SEC; +UINT32 bufferFrameCount; +UINT32 numFramesAvailable; +IMMDeviceEnumerator *pEnumerator = NULL; +IMMDevice *pDevice = NULL; +IAudioClient *pAudioClient = NULL; +IAudioCaptureClient *pCaptureClient = NULL; +WAVEFORMATEX *pwfx = NULL; + +bool InitAudioCapture() +{ + HRESULT hr; + + hr = CoInitialize(nullptr); + EXIT_ON_ERROR(hr, "CoInitialize failed"); + + hr = CoCreateInstance( + __uuidof(MMDeviceEnumerator), NULL, + CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), + (void**)&pEnumerator); + EXIT_ON_ERROR(hr, "CoCreateInstance of MMDeviceEnumerator failed"); + + hr = pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &pDevice); + EXIT_ON_ERROR(hr, "IMMDeviceEnumerator GetDefaultAudioEndpoint failed"); + + hr = pDevice->Activate( + __uuidof(IAudioClient), CLSCTX_ALL, + NULL, (void**)&pAudioClient); + EXIT_ON_ERROR(hr, "IMMDevice Activate failed"); + + hr = pAudioClient->GetMixFormat(&pwfx); + audioSampleRate = pwfx->nSamplesPerSec; + EXIT_ON_ERROR(hr, "IAudioClient GetMixFormat failed"); + + hr = pAudioClient->Initialize( + AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_STREAMFLAGS_LOOPBACK, + hnsRequestedDuration, + 0, + pwfx, + NULL); + EXIT_ON_ERROR(hr, "IAudioClient Initialize failed"); + + hr = pAudioClient->GetBufferSize(&bufferFrameCount); + EXIT_ON_ERROR(hr, "IAudioClient GetBufferSize failed"); + + hr = pAudioClient->GetService(__uuidof(IAudioCaptureClient), (void**)&pCaptureClient); + EXIT_ON_ERROR(hr, "IAudioClient GetService failed"); + + return true; +} + +void CleanupAudioCatpure() +{ + CoTaskMemFree(pwfx); + SAFE_RELEASE(pEnumerator) + SAFE_RELEASE(pDevice) + SAFE_RELEASE(pAudioClient) + SAFE_RELEASE(pCaptureClient) +} + +bool StartAudioCapture() +{ + HRESULT hr = pAudioClient->Start(); + EXIT_ON_ERROR(hr, "IAudioClient Start failed"); + + return true; +} + +bool StopAudioCapture() +{ + + HRESULT hr = pAudioClient->Stop(); + EXIT_ON_ERROR(hr, "IAudioCaptureClient Stop failed"); + + return true; +} + +bool TickAudioCapture() +{ + UINT32 packetLength = 0; + HRESULT hr = pCaptureClient->GetNextPacketSize(&packetLength); + EXIT_ON_ERROR(hr, "IAudioCaptureClient GetNextPacketSize failed"); + + while (packetLength != 0) + { + // Get the available data in the shared buffer. + BYTE *pData; + DWORD flags; + hr = pCaptureClient->GetBuffer( + &pData, + &numFramesAvailable, + &flags, NULL, NULL); + EXIT_ON_ERROR(hr, "IAudioCaptureClient GetBuffer failed"); + + bool silence = (flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0; + + float* pDataFloat = (float*)pData; + LONG lFloatsToWrite = numFramesAvailable * pwfx->nBlockAlign / sizeof(float); + LONG dataPos = 0; + LONG writePos = AudioWritePos; + while (dataPos < lFloatsToWrite) + { + LONG writeLength = std::min(AUDIO_BUFFER_SIZE - writePos, lFloatsToWrite - dataPos); + if (silence) + { + ZeroMemory(&audioBuffer[writePos], writeLength); + } + else + { + for (int i = 0; i < writeLength; i++) + { + audioBuffer[writePos + i] = (uint16_t)(pDataFloat[dataPos + i] * 32767); + } + } + + dataPos += writeLength; + writePos = (writePos + writeLength) % AUDIO_BUFFER_SIZE; + } + AudioWritePos = writePos; + + hr = pCaptureClient->ReleaseBuffer(numFramesAvailable); + EXIT_ON_ERROR(hr, "IAudioCaptureClient ReleaseBuffer failed"); + + hr = pCaptureClient->GetNextPacketSize(&packetLength); + EXIT_ON_ERROR(hr, "IAudioCaptureClient GetNextPacketSize failed"); + } + + return true; +} \ No newline at end of file diff --git a/Library/MiSTerCastLib/MiSTerCast.cpp b/Library/MiSTerCastLib/MiSTerCast.cpp new file mode 100644 index 0000000..14d69fe --- /dev/null +++ b/Library/MiSTerCastLib/MiSTerCast.cpp @@ -0,0 +1,106 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +// Windows Header Files +#include + +// C RunTime Header Files +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include + +#include "MiSTerCastLib.h" + +#define BUFFER_COUNT 3 + +typedef std::string Error; + + +// BGRA U8 Bitmap +struct Bitmap { + int Width = 0; + int Height = 0; + std::vector Buf; +}; + +enum Alignment : int +{ + Center, + TopLeft, + Top, + TopRight, + Right, + BottomRight, + Bottom, + BottomLeft, + Left +}; + +enum CropMode : int +{ + Custom, + X1, + X2, + X3, + X4, + X5, + Full43, + Full54, +}; + +struct SourceOptions { + bool syncrefresh; + int framedelay; + int display; + bool audio; + bool preview; + Alignment alignment; + CropMode cropmode; + int width; + int height; + int xoffset; + int yoffset; + int rotation; +}; + +// WinDesktopDup hides the gory details of capturing the screen using the +// Windows Desktop Duplication API +class WinDesktopDup { +public: + std::atomic_uint LastCaptureIndex = 0; + Bitmap Captures[BUFFER_COUNT]; + int OutputNumber = 0; + + WinDesktopDup(int outputNumber, log_function fnLog, capture_image_function fnCapture); + ~WinDesktopDup(); + + Error Initialize(); + void Close(); + bool CaptureNext(); + void SetSourceOptions(const SourceOptions* sourceOptions); + +private: + void LogMessage(std::string message, bool error); + + ID3D11Device* D3DDevice = nullptr; + ID3D11DeviceContext* D3DDeviceContext = nullptr; + IDXGIOutputDuplication* DeskDupl = nullptr; + DXGI_OUTPUT_DESC OutputDesc; + bool HaveFrameLock = false; + log_function logFunction; + capture_image_function captureFunction; + std::atomic_bool hasNewSourceOptions; + SourceOptions currentSourceOptions; + SourceOptions newSourceOptions; +}; \ No newline at end of file diff --git a/Library/MiSTerCastLib/MiSTerCastLib.cpp b/Library/MiSTerCastLib/MiSTerCastLib.cpp new file mode 100644 index 0000000..fc09a33 --- /dev/null +++ b/Library/MiSTerCastLib/MiSTerCastLib.cpp @@ -0,0 +1,247 @@ +#include "pch.h" +#include "MiSTerCastLib.h" +#include "AudioCapture.h" +#include "VideoCapture.h" + +#pragma comment(lib, "Ws2_32.lib") +#pragma comment(lib, "Winmm.lib") + +log_function logFunction = nullptr; +void LogMessage(std::string message, bool error) +{ + if (logFunction != nullptr) + logFunction(message.c_str(), error); +} + +std::atomic_bool stopCapture = false; +std::atomic_bool stopStream = false; +std::string targetIpString; + +#include "renderer_nogpu.h" + +std::atomic_bool capturing_screen = false; +void capture_screen() +{ + LogMessage("Screen capture starting."); + capturing_screen = true; + do + { + TickVideoCapture(); + } while (!stopCapture); + capturing_screen = false; + LogMessage("Screen capture stopped."); +} + +std::atomic_bool capturing_audio = false; +void capture_audio() +{ + if (source_config.audio) + { + LogMessage("Audio capture starting."); + StartAudioCapture(); + capturing_audio = true; + do + { + TickAudioCapture(); + } while (!stopStream); + StopAudioCapture(); + capturing_audio = false; + LogMessage("Audio capture stopped."); + } +} + +std::atomic_bool casting_screen = false; +void cast_screen() +{ + LogMessage("Casting to MiSTer starting."); + casting_screen = true; + auto renderer = std::make_unique(targetIpString); + do + { + renderer->draw(0); + } while (!stopStream); + casting_screen = false; + LogMessage("Casting to MiSTer stopped."); +} + +bool initialized = false; +std::unique_ptr captureScreenTask; +std::unique_ptr captureAudioTask; +MISTERCASTLIB_API bool Initialize(log_function fnLog, capture_image_function fnCapture) +{ + if (initialized) + { + LogMessage("MiSTerCast is already initialized.", true); + return true; + } + + logFunction = fnLog; + LogMessage("Initializing MiSTerCast"); + + source_config.syncrefresh = true; + source_config.framedelay = 0; + + selected_modeline.pclock = 6.700; + selected_modeline.hactive = 320; + selected_modeline.hbegin = 336; + selected_modeline.hend = 367; + selected_modeline.htotal = 426; + selected_modeline.vactive = 240; + selected_modeline.vbegin = 244; + selected_modeline.vend = 247; + selected_modeline.vtotal = 262; + selected_modeline.interlace = 0; + + + if (!InitializeVideoCapture(0, fnCapture)) + { + LogMessage("Failed to initialize video capture.", true); + return false; + } + + if (!InitAudioCapture()) + { + LogMessage("Failed to initialize audio capture.", true); + return false; + } + + // Fill the buffers to be safe + for (int i = 0; i < BUFFER_COUNT; i++) + TickVideoCapture(); + + captureScreenTask = std::make_unique(capture_screen); + + LogMessage("MiSTerCast ready."); + + initialized = true; + return true; +} + +MISTERCASTLIB_API bool Shutdown() +{ + stopCapture = true; + do {} while (capturing_screen); // wait for threads + stopCapture = false; + + captureScreenTask->detach(); + + return true; +} + +std::unique_ptr castScreenTask; + +MISTERCASTLIB_API bool StartStream(const char* targetIp) +{ + LogMessage("Starting stream."); + captureAudioTask = std::make_unique(capture_audio); + targetIpString = std::string(targetIp); + castScreenTask = std::make_unique(cast_screen); + + return true; +} + +MISTERCASTLIB_API bool StopStream() +{ + stopStream = true; + do {} while (capturing_audio || casting_screen); // wait for threads + stopStream = false; + + captureAudioTask->detach(); + castScreenTask->detach(); + return true; +} + +MISTERCASTLIB_API bool SetModeline( + double pclock, + UINT16 hactive, + UINT16 hbegin, + UINT16 hend, + UINT16 htotal, + UINT16 vactive, + UINT16 vbegin, + UINT16 vend, + UINT16 vtotal, + bool interlace) +{ + LogMessage("SetModeline called"); + selected_modeline.pclock = pclock; + selected_modeline.hactive = hactive; + selected_modeline.hbegin = hbegin; + selected_modeline.hend = hend; + selected_modeline.htotal = htotal; + selected_modeline.vactive = vactive; + selected_modeline.vbegin = vbegin; + selected_modeline.vend = vend; + selected_modeline.vtotal = vtotal; + selected_modeline.interlace = interlace; + + shouldUpdateVideoMode = true; + + return true; +} + +MISTERCASTLIB_API bool SetSource( + UINT8 display, + bool audio, + bool preview, + UINT8 alignment, + UINT8 cropmode, + UINT16 xcrop, + UINT16 ycrop, + INT16 xoffset, + INT16 yoffset, + UINT8 rotation) +{ + source_config.display = display; + source_config.audio = audio; + source_config.preview = preview; + source_config.alignment = (Alignment)alignment; + source_config.cropmode = (CropMode)cropmode; + source_config.width = xcrop; + source_config.height = ycrop; + source_config.xoffset = xoffset; + source_config.yoffset = yoffset; + source_config.rotation = rotation; + + switch (cropmode) + { + case CropMode::X1: + source_config.width = selected_modeline.hactive; + source_config.height = selected_modeline.vactive; + break; + case CropMode::X2: + source_config.width = selected_modeline.hactive * 2; + source_config.height = selected_modeline.vactive * 2; + break; + case CropMode::X3: + source_config.width = selected_modeline.hactive * 3; + source_config.height = selected_modeline.vactive * 3; + break; + case CropMode::X4: + source_config.width = selected_modeline.hactive * 4; + source_config.height = selected_modeline.vactive * 4; + break; + case CropMode::X5: + source_config.width = selected_modeline.hactive * 5; + source_config.height = selected_modeline.vactive * 5; + break; + default: + break; + } + + if (displayIndex != source_config.display) + { + stopCapture = true; + do {} while (capturing_screen); // wait for threads + stopCapture = false; + + captureScreenTask->detach(); + CleanupVideoCapture(); + InitializeVideoCapture(source_config.display, captureFunction); + captureScreenTask = std::make_unique(capture_screen); + } + + SetSourceOptions(&source_config); + + return true; +} diff --git a/Library/MiSTerCastLib/MiSTerCastLib.h b/Library/MiSTerCastLib/MiSTerCastLib.h new file mode 100644 index 0000000..65cf808 --- /dev/null +++ b/Library/MiSTerCastLib/MiSTerCastLib.h @@ -0,0 +1,99 @@ +#ifdef MISTERCASTLIB_EXPORTS +#define MISTERCASTLIB_API extern "C" __declspec(dllexport) +#else +#define MISTERCASTLIB_API __declspec(dllimport) +#endif + +void LogMessage(std::string message, bool error = false); + +#define EXIT_ON_ERROR(hres, message) \ + if (FAILED(hres)){\ + LogMessage(std::string(message) + ": " + std::to_string(hres), true);\ + return false;} +#define SAFE_RELEASE(punk) \ + if ((punk) != NULL) \ + { (punk)->Release(); (punk) = NULL; } + +enum Alignment : int +{ + Center, + TopLeft, + Top, + TopRight, + Right, + BottomRight, + Bottom, + BottomLeft, + Left +}; + +enum CropMode : int +{ + Custom, + X1, + X2, + X3, + X4, + X5, + Full43, + Full54, +}; + +enum Rotation : int +{ + None, + CW90, + CCW90, + Flip180 +}; + +struct SourceOptions { + bool syncrefresh; + UINT16 framedelay; + UINT8 display; + bool audio; + bool preview; + Alignment alignment; + CropMode cropmode; + UINT16 width; + UINT16 height; + INT16 xoffset; + INT16 yoffset; + UINT8 rotation; +}; + +typedef void(__stdcall *log_function)(const char* message, bool error); + +typedef void(__stdcall *capture_image_function)(int width, int height, void* buffer); + +MISTERCASTLIB_API bool Initialize(log_function fnLog, capture_image_function fnCapture); + +MISTERCASTLIB_API bool Shutdown(); + +MISTERCASTLIB_API bool StartStream(const char* targetIp); + +MISTERCASTLIB_API bool StopStream(); + +MISTERCASTLIB_API bool SetModeline( + double pclock, + UINT16 hactive, + UINT16 hbegin, + UINT16 hend, + UINT16 htotal, + UINT16 vactive, + UINT16 vbegin, + UINT16 vend, + UINT16 vtotal, + bool interlace); + +MISTERCASTLIB_API bool SetSource( + UINT8 display, + bool audio, + bool preview, + UINT8 alignment, + UINT8 cropmode, + UINT16 width, + UINT16 height, + INT16 xoffset, + INT16 yoffset, + UINT8 rotation); diff --git a/Library/MiSTerCastLib/MiSTerCastLib.vcxproj b/Library/MiSTerCastLib/MiSTerCastLib.vcxproj new file mode 100644 index 0000000..d6d82bd --- /dev/null +++ b/Library/MiSTerCastLib/MiSTerCastLib.vcxproj @@ -0,0 +1,190 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {3F2B9AA9-BCC6-475B-8B39-3562BFE8CEB7} + Win32Proj + MiSTerCastLib + 10.0.22621.0 + + + + DynamicLibrary + true + v141 + Unicode + + + DynamicLibrary + false + v141 + true + Unicode + + + DynamicLibrary + true + v141 + Unicode + + + DynamicLibrary + false + v141 + true + Unicode + + + + + + + + + + + + + + + + + + + + + MISTERCASTLIB + true + + + MISTERCASTLIB + true + + + MISTERCASTLIB + false + + + MISTERCASTLIB + false + + + + Use + Level3 + Disabled + true + WIN32;_DEBUG;MISTERCASTLIB_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + pch.h + ..\..\External\lz4\include + + + Windows + true + false + ..\..\External\lz4\dll\liblz4.dll.a;D3D11.lib;%(AdditionalDependencies) + + + + + Use + Level3 + Disabled + true + _DEBUG;MISTERCASTLIB_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + pch.h + + + Windows + true + false + D:\Projects\Other\lz4\dll\liblz4.dll.a;D3D11.lib;%(AdditionalDependencies) + + + + + Use + Level3 + MaxSpeed + true + true + true + WIN32;NDEBUG;MISTERCASTLIB_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + pch.h + ..\..\External\lz4\include + + + Windows + true + true + true + false + ..\..\External\lz4\dll\liblz4.dll.a;D3D11.lib;%(AdditionalDependencies) + + + + + Use + Level3 + MaxSpeed + true + true + true + NDEBUG;MISTERCASTLIB_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + pch.h + + + Windows + true + true + true + false + D:\Projects\Other\lz4\dll\liblz4.dll.a;D3D11.lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + + + Create + Create + Create + Create + + + + + + \ No newline at end of file diff --git a/Library/MiSTerCastLib/MiSTerCastLib.vcxproj.filters b/Library/MiSTerCastLib/MiSTerCastLib.vcxproj.filters new file mode 100644 index 0000000..479e3d2 --- /dev/null +++ b/Library/MiSTerCastLib/MiSTerCastLib.vcxproj.filters @@ -0,0 +1,48 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/Library/MiSTerCastLib/VideoCapture.h b/Library/MiSTerCastLib/VideoCapture.h new file mode 100644 index 0000000..81586e2 --- /dev/null +++ b/Library/MiSTerCastLib/VideoCapture.h @@ -0,0 +1,337 @@ +#pragma once + +#define BUFFER_COUNT 3 + + +struct Bitmap { + int width = 0; + int height = 0; + std::vector buffer; +}; + +SourceOptions source_config = {}; +std::atomic_uint lastVideoCaptureIndex = 0; +Bitmap* videoCaptures = nullptr; +int displayIndex = 0; +ID3D11Device* d3dDevice = nullptr; +ID3D11DeviceContext* d3dDeviceContext = nullptr; +IDXGIOutputDuplication* desktopDuplication = nullptr; +bool haveFrameLock = false; +capture_image_function captureFunction; +std::atomic_bool hasNewSourceOptions; +SourceOptions currentSourceOptions; +SourceOptions newSourceOptions; + +bool InitializeVideoCapture(int outputNumber, capture_image_function fnCapture) +{ + displayIndex = outputNumber; + captureFunction = fnCapture; + currentSourceOptions = {}; + currentSourceOptions.display = 0; + currentSourceOptions.framedelay = 0; + currentSourceOptions.alignment = Alignment::Center; + currentSourceOptions.audio = 1; + currentSourceOptions.syncrefresh = true; + currentSourceOptions.cropmode = CropMode::Full43; + + if (videoCaptures == nullptr) + { + videoCaptures = new Bitmap[BUFFER_COUNT]; + for (int i = 0; i < BUFFER_COUNT; i++) + videoCaptures[i] = Bitmap(); + } + + HDESK hDesk = OpenInputDesktop(0, FALSE, GENERIC_ALL); + if (!hDesk) + { + LogMessage("Failed to open desktop", true); + return false; + } + + // Attach desktop to this thread + // Is this required? Should we do this on the capture thread? + SetThreadDesktop(hDesk); + CloseDesktop(hDesk); + hDesk = nullptr; + + HRESULT hr = S_OK; + + D3D_DRIVER_TYPE driverTypes[] = { + D3D_DRIVER_TYPE_HARDWARE, + D3D_DRIVER_TYPE_WARP, + D3D_DRIVER_TYPE_REFERENCE, + }; + auto numDriverTypes = ARRAYSIZE(driverTypes); + + D3D_FEATURE_LEVEL featureLevels[] = { + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_1 }; + auto numFeatureLevels = ARRAYSIZE(featureLevels); + + D3D_FEATURE_LEVEL featureLevel; + for (size_t i = 0; i < numDriverTypes; i++) { + hr = D3D11CreateDevice(nullptr, driverTypes[i], nullptr, 0, featureLevels, (UINT)numFeatureLevels, + D3D11_SDK_VERSION, &d3dDevice, &featureLevel, &d3dDeviceContext); + if (SUCCEEDED(hr)) + break; + } + + EXIT_ON_ERROR(hr, "D3D11CreateDevice failed"); + + IDXGIDevice* dxgiDevice = nullptr; + hr = d3dDevice->QueryInterface(__uuidof(IDXGIDevice), (void**)&dxgiDevice); + EXIT_ON_ERROR(hr, "D3DDevice->QueryInterface failed"); + + IDXGIAdapter* dxgiAdapter = nullptr; + hr = dxgiDevice->GetParent(__uuidof(IDXGIAdapter), (void**)&dxgiAdapter); + dxgiDevice->Release(); + dxgiDevice = nullptr; + EXIT_ON_ERROR(hr, "DxgiDevice->GetParent failed"); + + IDXGIOutput* dxgiOutput = nullptr; + hr = dxgiAdapter->EnumOutputs(displayIndex, &dxgiOutput); + dxgiAdapter->Release(); + dxgiAdapter = nullptr; + EXIT_ON_ERROR(hr, "DxgiAdapter->EnumOutputs faile"); + + // DXGI_OUTPUT_DESC outputDesc; + // hr = dxgiOutput->GetDesc(&outputDesc); + // EXIT_ON_ERROR(hr, "DxgiOutput->GetDesc faile"); + + IDXGIOutput1* dxgiOutput1 = nullptr; + hr = dxgiOutput->QueryInterface(__uuidof(dxgiOutput1), (void**)&dxgiOutput1); + dxgiOutput->Release(); + dxgiOutput = nullptr; + EXIT_ON_ERROR(hr, "DxgiOutput->QueryInterface faile"); + + hr = dxgiOutput1->DuplicateOutput(d3dDevice, &desktopDuplication); + dxgiOutput1->Release(); + dxgiOutput1 = nullptr; + EXIT_ON_ERROR(hr, "DxgiOutput1->DuplicateOutput failed"); + + return true; +} + +void CleanupVideoCapture() +{ + SAFE_RELEASE(desktopDuplication); + SAFE_RELEASE(d3dDeviceContext); + SAFE_RELEASE(d3dDevice); + haveFrameLock = false; +} + +bool TickVideoCapture() +{ + if (!desktopDuplication) + return false; + + HRESULT hr; + + if (hasNewSourceOptions) + { + currentSourceOptions = newSourceOptions; + hasNewSourceOptions = false; + } + + // Release right before acquiring next frame + if (haveFrameLock) { + haveFrameLock = false; + hr = desktopDuplication->ReleaseFrame(); + } + + IDXGIResource* deskRes = nullptr; + DXGI_OUTDUPL_FRAME_INFO frameInfo; + hr = desktopDuplication->AcquireNextFrame(32, &frameInfo, &deskRes); + if (hr == DXGI_ERROR_WAIT_TIMEOUT) + return false; + + if (FAILED(hr)) { + // Try to reinitialize and capture next frame + LogMessage("Acquire failed: " + std::to_string(hr), true); + CleanupVideoCapture(); + InitializeVideoCapture(displayIndex, captureFunction); + return false; + } + + haveFrameLock = true; + + ID3D11Texture2D* gpuTex = nullptr; + hr = deskRes->QueryInterface(__uuidof(ID3D11Texture2D), (void**)&gpuTex); + deskRes->Release(); + deskRes = nullptr; + EXIT_ON_ERROR(hr, "Query Interface for ID3D11Texture2D failed"); + + bool ok = true; + + unsigned int width; + unsigned int height; + switch (source_config.rotation) + { + case Rotation::CW90: + case Rotation::CCW90: + width = currentSourceOptions.height; + height = currentSourceOptions.width; + break; + default: + width = currentSourceOptions.width; + height = currentSourceOptions.height; + break; + } + + D3D11_TEXTURE2D_DESC desc; + gpuTex->GetDesc(&desc); + desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE | D3D11_CPU_ACCESS_READ; + desc.Usage = D3D11_USAGE_STAGING; + desc.BindFlags = 0; + desc.MiscFlags = 0; + + switch (currentSourceOptions.cropmode) + { + case CropMode::Custom: + case CropMode::X1: + case CropMode::X2: + case CropMode::X3: + case CropMode::X4: + case CropMode::X5: + break; + case CropMode::Full43: + switch (source_config.rotation) + { + case Rotation::CW90: + case Rotation::CCW90: + width = desc.Height * 3 / 4; + break; + default: + width = desc.Height * 4 / 3; + break; + } + height = desc.Height; + break; + case CropMode::Full54: + switch (source_config.rotation) + { + case Rotation::CW90: + case Rotation::CCW90: + width = desc.Height * 4 / 5; + break; + default: + width = desc.Height * 5 / 4; + break; + } + height = desc.Height; + break; + default: + break; + } + + if (width > desc.Width) + width = desc.Width; + if (height > desc.Height) + height = desc.Height; + + int xoffset = currentSourceOptions.xoffset; + int yoffset = currentSourceOptions.yoffset; + switch (currentSourceOptions.alignment) + { + case Alignment::Center: + xoffset += desc.Width / 2 - width / 2; + yoffset += desc.Height / 2 - height / 2; + break; + case Alignment::TopLeft: + break; + case Alignment::Top: + xoffset += desc.Width / 2 - width / 2; + break; + case Alignment::TopRight: + xoffset += desc.Width - width; + break; + case Alignment::Right: + xoffset += desc.Width - width; + yoffset += desc.Height / 2 - height / 2; + break; + case Alignment::BottomRight: + xoffset += desc.Width - width; + yoffset += desc.Height - height; + break; + case Alignment::Bottom: + xoffset += desc.Width / 2 - width / 2; + yoffset += desc.Height - height; + break; + case Alignment::BottomLeft: + yoffset += desc.Height - height; + break; + case Alignment::Left: + yoffset += desc.Height / 2 - height / 2; + default: + break; + } + + if (xoffset < 0) + xoffset = 0; + else if (xoffset + width > desc.Width) + xoffset = desc.Width - width; + + if (yoffset < 0) + yoffset = 0; + else if (yoffset + height > desc.Height) + yoffset = desc.Height - height; + + desc.Width = width; + desc.Height = height; + + ID3D11Texture2D* cpuTex = nullptr; + hr = d3dDevice->CreateTexture2D(&desc, nullptr, &cpuTex); + EXIT_ON_ERROR(hr, "D3DDevice->CreateTexture2D failed"); + + D3D11_BOX sourceRegion; + sourceRegion.left = xoffset; + sourceRegion.right = xoffset + width; + sourceRegion.top = yoffset; + sourceRegion.bottom = yoffset + height; + sourceRegion.front = 0; + sourceRegion.back = 1; + + d3dDeviceContext->CopySubresourceRegion( + cpuTex, + 0, // sub resource + 0, //x + 0, //y + 0, //z + gpuTex, + 0, // sub resource + &sourceRegion); + + unsigned int nextIndex = (lastVideoCaptureIndex + 1) % BUFFER_COUNT; + D3D11_MAPPED_SUBRESOURCE sr; + hr = d3dDeviceContext->Map(cpuTex, 0, D3D11_MAP_READ, 0, &sr); + EXIT_ON_ERROR(hr, "D3DDeviceContext->Map failed"); + + if (videoCaptures[nextIndex].width != width || videoCaptures[nextIndex].height != height) + { + videoCaptures[nextIndex].width = width; + videoCaptures[nextIndex].height = height; + videoCaptures[nextIndex].buffer.resize(width * height * 4); + } + + for (int y = 0; y < (int)height; y++) // TODO: Can this be improved? + memcpy(videoCaptures[nextIndex].buffer.data() + y * width * 4, (uint8_t*)sr.pData + sr.RowPitch * y, width * 4); + d3dDeviceContext->Unmap(cpuTex, 0); + + if (currentSourceOptions.preview) + captureFunction(width, height, videoCaptures[nextIndex].buffer.data()); + + lastVideoCaptureIndex = nextIndex; + + cpuTex->Release(); + gpuTex->Release(); + + return ok; +} + +void SetSourceOptions(const SourceOptions* sourceOptions) +{ + newSourceOptions = *sourceOptions; + hasNewSourceOptions = true; +} \ No newline at end of file diff --git a/Library/MiSTerCastLib/cpp.hint b/Library/MiSTerCastLib/cpp.hint new file mode 100644 index 0000000..290b2bc --- /dev/null +++ b/Library/MiSTerCastLib/cpp.hint @@ -0,0 +1,2 @@ +#define MISTERCASTLIB_API __declspec(dllexport) +#define MISTERCASTLIB_API __declspec(dllimport) diff --git a/Library/MiSTerCastLib/dllmain.cpp b/Library/MiSTerCastLib/dllmain.cpp new file mode 100644 index 0000000..d8549a0 --- /dev/null +++ b/Library/MiSTerCastLib/dllmain.cpp @@ -0,0 +1,18 @@ +#include "pch.h" + +BOOL APIENTRY DllMain( HMODULE hModule, + DWORD ul_reason_for_call, + LPVOID lpReserved + ) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} + diff --git a/Library/MiSTerCastLib/pch.cpp b/Library/MiSTerCastLib/pch.cpp new file mode 100644 index 0000000..1730571 --- /dev/null +++ b/Library/MiSTerCastLib/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" \ No newline at end of file diff --git a/Library/MiSTerCastLib/pch.h b/Library/MiSTerCastLib/pch.h new file mode 100644 index 0000000..7d7e09f --- /dev/null +++ b/Library/MiSTerCastLib/pch.h @@ -0,0 +1,51 @@ +// pch.h: This is a precompiled header file. +// Files listed below are compiled only once, improving build performance for future builds. +// This also affects IntelliSense performance, including code completion and many code browsing features. +// However, files listed here are ALL re-compiled if any one of them is updated between builds. +// Do not add files here that you will be updating frequently as this negates the performance advantage. + +#ifndef PCH_H +#define PCH_H + +#define WIN32_LEAN_AND_MEAN +#define _WINSOCK_DEPRECATED_NO_WARNINGS +#define NOMINMAX +#define no_init_all + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + + +#include +#include + +#endif //PCH_H diff --git a/Library/MiSTerCastLib/renderer_nogpu.h b/Library/MiSTerCastLib/renderer_nogpu.h new file mode 100644 index 0000000..7c542a4 --- /dev/null +++ b/Library/MiSTerCastLib/renderer_nogpu.h @@ -0,0 +1,821 @@ +// Groovy_MiSTer communication adapted from the Groovy_Mame source. +// See https://github.com/antonioginer/GroovyMAME for original source + +// Original license: +// license:BSD-3-Clause +// copyright-holders:Aaron Giles, Antonio Giner, Sergi Clara + +// Modification by Shane Lynch + +#pragma once + +uint64_t CurrentTicks() +{ + // use the standard library clock function + LARGE_INTEGER ticks; + QueryPerformanceCounter(&ticks); + return ticks.QuadPart; +} + +uint64_t TicksPerSecond() noexcept +{ + LARGE_INTEGER val; + QueryPerformanceFrequency(&val); + return val.QuadPart; +} + +void SleepTicks(uint64_t duration) noexcept +{ + std::this_thread::sleep_for(std::chrono::high_resolution_clock::duration(duration)); +} + +inline double get_ms(uint64_t ticks) { return (double)ticks / TicksPerSecond() * 1000; }; + +#define MAX_BUFFER_WIDTH 768 +#define MAX_BUFFER_HEIGHT 576 +#define VRAM_BUFFER_SIZE 65536 +#define SEND_BIFFER_SIZE 2097152 //2 * 1024 * 1024 +#define MAX_LZ4_BLOCK 61440 +#define MAX_SAMPLE_RATE 48000 +#define STREAMS_UPDATE_FREQUENCY 50 // sound.h + +// nogpu UDP server +#define UDP_PORT 32100 + +// Server commands +#define CMD_CLOSE 1 +#define CMD_INIT 2 +#define CMD_SWITCHRES 3 +#define CMD_AUDIO 4 +#define CMD_GET_STATUS 5 +#define CMD_BLIT_VSYNC 6 + +// Status bits +#define VRAM_READY 1 << 0 +#define VRAM_END_FRAME 1 << 1 +#define VRAM_SYNCED 1 << 2 +#define VRAM_FRAMESKIP 1 << 3 +#define VGA_VBLANK 1 << 4 +#define VGA_FIELD 1 << 5 +#define FPGA_AUDIO 1 << 6 +#define VGA_QUEUE 1 << 7 + +#pragma pack(1) + +typedef struct nogpu_modeline +{ + double pclock; + uint16_t hactive; + uint16_t hbegin; + uint16_t hend; + uint16_t htotal; + uint16_t vactive; + uint16_t vbegin; + uint16_t vend; + uint16_t vtotal; + bool interlace; +} nogpu_modeline; + +std::atomic_bool shouldUpdateVideoMode = false; +nogpu_modeline selected_modeline = {}; + +typedef struct nogpu_status +{ + uint16_t vcount; + uint32_t frame_num; +} nogpu_status; + +typedef struct nogpu_blit_status +{ + uint32_t frame_req; + uint16_t vcount_req; + uint32_t frame_gpu; + uint16_t vcount_gpu; + uint8_t bits; +} nogpu_blit_status; + +typedef struct cmd_init +{ + const uint8_t cmd = CMD_INIT; + uint8_t compression; + uint8_t sound_rate; + uint8_t sound_channels; +} cmd_init; + +typedef struct cmd_close +{ + const uint8_t cmd = CMD_CLOSE; +} cmd_close; + +typedef struct cmd_switchres +{ + const uint8_t cmd = CMD_SWITCHRES; + nogpu_modeline mode; +} cmd_switchres; + +typedef struct cmd_audio +{ + const uint8_t cmd = CMD_AUDIO; + uint16_t sample_bytes; +} cmd_blit; + +typedef struct cmd_blit_vsync +{ + const uint8_t cmd = CMD_BLIT_VSYNC; + uint32_t frame; + uint16_t vsync; + uint16_t block_size; +} cmd_blit_vsync; + + +typedef struct cmd_get_status +{ + const uint8_t cmd = CMD_GET_STATUS; +} cmd_get_status; + +#pragma pack() + +// renderer_nogpu is the information for the current screen +class renderer_nogpu +{ +public: + renderer_nogpu(std::string targetip) + : m_bmdata(nullptr) + , m_bmsize(0) + , m_targetip(targetip) + { + } + + ~renderer_nogpu(); + int create(); + int draw(const int update); + void save() {} + void record() {} + void toggle_fsfx() {} + void add_audio_to_recording(const uint16_t *buffer, int samples_this_frame); + +private: + std::unique_ptr m_bmdata; + size_t m_bmsize; + + // npgpu private members + bool m_initialized = false; + bool m_first_blit = true; + int m_compression = 0; + bool m_show_window = false; + bool m_is_internal_fe = false; + bool m_autofilter = false; + bool m_bilinear = false; + int m_frame = 0; + int m_field = 0; + unsigned int m_width = 0; + unsigned int m_height = 0; + int m_vtotal = 0; + int m_vsync_scanline = 0; + bool m_sleep_allowed = false; + double m_period = 16.666667; + double m_line_period = 0.064; + double m_frame_delay = 0.0; + double m_fd_margin = 1.5; + float m_aspect = 4.0f / 3.0f; + float m_pixel_aspect = 1.0f; + int m_sample_rate = MAX_SAMPLE_RATE; + nogpu_status m_status; + nogpu_blit_status m_blit_status; + nogpu_modeline m_current_mode; + + uint64_t time_start = 0; + uint64_t time_entry = 0; + uint64_t time_blit = 0; + uint64_t time_exit = 0; + uint64_t time_frame[16]; + uint64_t time_frame_avg = 0; + uint64_t time_frame_dm = 0; + uint64_t time_sleep = uint64_t(TicksPerSecond() / 1000.0); // 1 ms + + int m_sockfd = -1; //INVALID_SOCKET; + sockaddr_in m_server_addr; + std::string m_targetip; + + char m_fb[MAX_BUFFER_HEIGHT * MAX_BUFFER_WIDTH * 3]; + char m_fb_compressed[MAX_BUFFER_HEIGHT * MAX_BUFFER_WIDTH * 3]; + char inp_buf[2][MAX_LZ4_BLOCK + 1]; + char m_ab[MAX_SAMPLE_RATE / STREAMS_UPDATE_FREQUENCY * 2 * 2]; + + bool nogpu_init(); + bool nogpu_send_command(void *command, int command_size); + bool nogpu_switch_video_mode(); + void nogpu_blit(uint32_t frame, uint16_t vsync, uint16_t line_width); + void nogpu_send_mtu(char *buffer, int bytes_to_send, int chunk_max_size); + void nogpu_send_lz4(char *buffer, int bytes_to_send, int block_size); + int nogpu_compress(int id_compress, char *buffer_comp, const char *buffer_rgb, uint32_t buffer_size); + bool nogpu_wait_ack(double timeout); + bool nogpu_wait_status(nogpu_blit_status *status, double timeout); + void nogpu_register_frametime(uint64_t frametime); +}; + +//============================================================ +// renderer_nogpu::create +//============================================================ + +int renderer_nogpu::create() +{ + return 0; +} + +//============================================================ +// renderer_nogpu::~renderer_nogpu +//============================================================ + +renderer_nogpu::~renderer_nogpu() +{ + // Wait for fpga to flush last blit + SleepTicks(uint64_t(m_period * time_sleep)); + + LogMessage("Sending CMD_CLOSE..."); + cmd_close command; + + nogpu_send_command(&command, sizeof(command)); + LogMessage("Done."); + closesocket(m_sockfd); + WSACleanup(); +} + +//============================================================ +// renderer_nogpu::draw +//============================================================ +int renderer_nogpu::draw(const int update) +{ + // Hack because these aren't intiailized... + m_width = selected_modeline.hactive; + m_height = selected_modeline.interlace ? selected_modeline.vactive / 2 : selected_modeline.vactive; + + // resize window if required + static int old_width = 0; + static int old_height = 0; + if (old_width != m_width || old_height != m_height) + { + old_width = m_width; + old_height = m_height; + } + + // compute pitch of target + unsigned int const pitch = (m_width + 3) & ~3; + + // make sure our temporary bitmap is big enough + if ((pitch * m_height * 4) > m_bmsize) + { + m_bmsize = pitch * m_height * 4 * 2; + m_bmdata.reset(); + m_bmdata = std::make_unique(m_bmsize); + } + + // initialize nogpu right before first blit + if (m_first_blit && !m_initialized) + { + m_initialized = nogpu_init(); + if (m_initialized) + { + LogMessage("Done."); + nogpu_switch_video_mode(); + } + else + { + LogMessage("Failed.", true); + m_first_blit = false; + } + } + + // only send frame if nogpu is initialized + if (!m_initialized) + return 0; + + // get current field for interlaced mode + if (m_current_mode.interlace) + m_field = (m_blit_status.bits & VGA_FIELD ? 1 : 0) ^ ((m_frame - m_blit_status.frame_gpu) % 2); + + unsigned int drawIndex = lastVideoCaptureIndex; + int screenwidth = videoCaptures[drawIndex].width; + int screenheight = videoCaptures[drawIndex].height; + bool drawInt = (m_field != 0); + + int j = 0; + + float stepx; + float stepy; + float interlaceStepX = 0; + float interlaceStepY = 0; + switch (source_config.rotation) + { + case Rotation::CW90: + stepx = ((float)(screenwidth) / (float)m_height); + stepy = ((float)(screenheight) / (float)m_width); + interlaceStepX = int(stepx / 2.0f); + case Rotation::CCW90: + stepx = ((float)(screenwidth) / (float)m_height); + stepy = ((float)(screenheight) / (float)m_width); + interlaceStepX = int(stepx / 2.0f); + drawInt = !drawInt; + break; + case Rotation::Flip180: + stepx = ((float)(screenwidth) / (float)m_width); + stepy = ((float)(screenheight) / (float)m_height); + interlaceStepY = int(stepy / 2.0f); + drawInt = !drawInt; + default: + stepx = ((float)(screenwidth) / (float)m_width); + stepy = ((float)(screenheight) / (float)m_height); + interlaceStepY = int(stepy / 2.0f); + break; + } + + for (unsigned int i = 0; i < (pitch * m_height * 4); i += 4) + { + int x = (i / 4) % m_width; + int y = (i / 4) / m_width; + int tx = x; + switch (source_config.rotation) + { + case Rotation::CW90: + x = m_height - y - 1; + y = tx; + break; + case Rotation::CCW90: + x = y; + y = m_width - tx - 1; + break; + case Rotation::Flip180: + x = m_width - x - 1; + y = m_height - y - 1; + break; + } + + int bmpx = int(x * stepx); + int bmpy = int(y * stepy); + if (m_current_mode.interlace && drawInt) + { + bmpx += interlaceStepX; + bmpy += interlaceStepY; + } + int bmpi = (bmpy * screenwidth + bmpx) * 4; + if (bmpi + 4 >= (screenheight * screenwidth * 4)) + continue; + + m_fb[j] = (char)videoCaptures[drawIndex].buffer[bmpi]; + m_fb[j + 1] = (char)videoCaptures[drawIndex].buffer[bmpi + 1]; + m_fb[j + 2] = (char)videoCaptures[drawIndex].buffer[bmpi + 2]; + j += 3; + } + + // change video mode right before the blit + if (shouldUpdateVideoMode) + nogpu_switch_video_mode(); + + bool valid_status = false; + + time_entry = CurrentTicks(); + + if (source_config.syncrefresh && m_first_blit) + { + time_start = time_entry; + time_blit = time_entry; + time_exit = time_entry; + + m_first_blit = false; + m_frame = 1; + + // Skip blitting first frame, so we avoid glitches while MAME loads roms + return 0; + } + + // Blit now + nogpu_blit(m_frame, m_width, m_height); + + // Send audio + unsigned int writePos = AudioWritePos; + unsigned sendPos = 0; + while (writePos != AudioReadPos) + { + int samplesToWrite = AudioReadPos < writePos ? + writePos - AudioReadPos : + AUDIO_BUFFER_SIZE - AudioReadPos; + + memcpy(&audioSendBuffer[sendPos], &audioBuffer[AudioReadPos], samplesToWrite * sizeof(uint16_t)); + sendPos += samplesToWrite; + AudioReadPos = (AudioReadPos + samplesToWrite) % AUDIO_BUFFER_SIZE; + } + + if (sendPos > 0) + add_audio_to_recording(audioSendBuffer, sendPos); + + time_blit = CurrentTicks(); + + nogpu_register_frametime(time_entry - time_exit); + + // Wait raster position + if (source_config.syncrefresh) + valid_status = nogpu_wait_status(&m_blit_status, std::max(0.0, m_period - get_ms(time_blit - time_exit))); + + if (source_config.syncrefresh && valid_status) + m_frame = m_blit_status.frame_req + 1; + else + m_frame++; + + time_exit = CurrentTicks(); + + return 0; +} + +//============================================================ +// renderer_nogpu::nogpu_init +//============================================================ + +bool renderer_nogpu::nogpu_init() +{ + int result; + + LogMessage("Initializing Winsock..."); + WSADATA wsa; + result = WSAStartup(MAKEWORD(2, 2), &wsa); + if (result != NO_ERROR) + { + LogMessage("Failed. Error code : " + std::to_string(WSAGetLastError())); + return false; + } + LogMessage("Done."); + + LogMessage("Initializing socket... "); + m_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + + if (m_sockfd < 0) + { + LogMessage("Could not create socket!", true); + return false; + } + else + LogMessage("Done."); + + m_server_addr = {}; + m_server_addr.sin_family = AF_INET; + m_server_addr.sin_port = htons(UDP_PORT); + m_server_addr.sin_addr.s_addr = inet_addr(m_targetip.c_str()); + + LogMessage("Setting socket async..."); + + u_long opt = 1; + if (ioctlsocket(m_sockfd, FIONBIO, &opt) < 0) + LogMessage("Could not set nonblocking."); + + LogMessage("Setting send buffer to " + std::to_string(SEND_BIFFER_SIZE)); + int opt_val = SEND_BIFFER_SIZE; + result = setsockopt(m_sockfd, SOL_SOCKET, SO_SNDBUF, (char*)&opt_val, sizeof(opt_val)); + if (result < 0) + { + LogMessage("Unable to set send buffer: " + std::to_string(result), true); + return false; + } + + m_compression = 0x01; // lz4 compression + + switch (audioSampleRate) + { + case 22050: + m_sample_rate = 1; + break; + case 44100: + m_sample_rate = 2; + break; + case 48000: + m_sample_rate = 3; + break; + default: + LogMessage("Unsupported audio sample rate. Only 48kHz, 44.1kHz and 22.05kHz are supported."); + m_sample_rate = 0; + } + + LogMessage("Sending CMD_INIT..."); + cmd_init command; + command.compression = m_compression; + command.sound_rate = m_sample_rate; + command.sound_channels = 2; + + // Reset current mode + m_current_mode = {}; + + if (nogpu_send_command(&command, sizeof(command))) + return nogpu_wait_ack(1000); + + return false; +} + +//============================================================ +// renderer_nogpu::nogpu_switch_video_mode() +//============================================================ + +bool renderer_nogpu::nogpu_switch_video_mode() +{ + nogpu_modeline *mode = &selected_modeline; + if (mode == nullptr) + return false; + + m_current_mode = *mode; + + // Send new modeline to nogpu + LogMessage("Sending CMD_SWITCHRES..."); + + cmd_switchres command; + nogpu_modeline *m = &command.mode; + + m->pclock = mode->pclock; + m->hactive = mode->hactive; + m->hbegin = mode->hbegin; + m->hend = mode->hend; + m->htotal = mode->htotal; + m->vactive = mode->vactive; + m->vbegin = mode->vbegin; + m->vend = mode->vend; + m->vtotal = mode->vtotal; + m->interlace = mode->interlace; + + m_width = mode->hactive; + m_height = mode->vactive; + m_vtotal = mode->vtotal; + m_field = 0; + + shouldUpdateVideoMode = false; + return nogpu_send_command(&command, sizeof(command)); +} + +//============================================================ +// renderer_nogpu::nogpu_wait_ack +//============================================================ + +bool renderer_nogpu::nogpu_wait_ack(double timeout) +{ + uint64_t time_1 = CurrentTicks(); + socklen_t server_addr_size = sizeof(m_server_addr); + + // Poll server for ack + do + { + int bytes_recv = recvfrom(m_sockfd, (char *)&m_status, sizeof(nogpu_blit_status), 0, (sockaddr*)&m_server_addr, &server_addr_size); + + if (bytes_recv == sizeof(nogpu_blit_status)) + break; + + uint64_t time_2 = CurrentTicks(); + if (get_ms(time_2 - time_1) > timeout) + { + LogMessage("Server ack timeout.", true); + return false; + } + + SleepTicks(time_sleep); + + } while (true); + + return true; +} + +//============================================================ +// renderer_nogpu::nogpu_wait_status +//============================================================ + +bool renderer_nogpu::nogpu_wait_status(nogpu_blit_status *status, double timeout) +{ + int retries = 0; + uint64_t time_1 = 0; + uint64_t time_2 = 0; + socklen_t server_addr_size = sizeof(m_server_addr); + int bytes_recv = 0; + + time_1 = CurrentTicks(); + + // Poll server for blit line timestamp + do + { + retries++; + bytes_recv = recvfrom(m_sockfd, (char *)status, sizeof(nogpu_blit_status), 0, (sockaddr*)&m_server_addr, &server_addr_size); + + if (bytes_recv > 0 && m_frame == status->frame_req) + break; + + time_2 = CurrentTicks(); + if (get_ms(time_2 - time_1) > timeout) + { + return false; + } + + if (m_sleep_allowed) SleepTicks(time_sleep); + + } while (true); + + + // Compute line target for next blit, relative to last blit line timestamp + int lines_to_wait = (status->frame_req - status->frame_gpu) * m_current_mode.vtotal + status->vcount_req - status->vcount_gpu; + if (m_current_mode.interlace) + lines_to_wait /= 2; + + // Compute time target for emulation of next frame, so that blit after it happens at desired line target + uint64_t time_target = time_entry + (uint64_t)((double)lines_to_wait * m_line_period * time_sleep) - time_frame_avg; + + // Wait for target time + if ((int)(time_target - CurrentTicks()) > 0) + { + do + { + time_2 = CurrentTicks(); + if (time_2 >= time_target) + break; + + if (m_sleep_allowed && get_ms(time_target - time_2) > 2.0) + SleepTicks(time_sleep); + + } while (true); + } + + // Make sure our frame counter hasn't fallen behind gpu's + if (status->frame_gpu > status->frame_req) + status->frame_req = status->frame_gpu + 1; + + return true; +} + +//============================================================ +// renderer_nogpu::nogpu_register_frametime +//============================================================ + +void renderer_nogpu::nogpu_register_frametime(uint64_t frametime) +{ + static int i = 0; + static int regs = 0; + const int max_regs = sizeof(time_frame) / sizeof(time_frame[0]); + uint64_t acum = 0; + uint64_t diff = 0; + + // Discard invalid values + if (frametime <= 0 || get_ms(frametime) > m_period) + return; + + // Register value and compute current average + time_frame[i] = frametime; + i++; + + if (i > max_regs) + i = 0; + + if (regs < max_regs) + regs++; + + for (int k = 0; k < regs; k++) + acum += time_frame[k]; + + time_frame_avg = acum / regs; + + // Compute current max deviation + uint64_t max_diff = 0; + + for (int k = 1; k <= regs; k++) + { + diff = time_frame[k] - time_frame[k - 1]; + + if (diff > 0 && diff > max_diff) + max_diff = diff; + } + + time_frame_dm = max_diff; +} + +//============================================================ +// renderer_nogpu::nogpu_send_mtu +//============================================================ + +void renderer_nogpu::nogpu_send_mtu(char *buffer, int bytes_to_send, int chunk_max_size) +{ + int bytes_this_chunk = 0; + int chunk_size = 0; + uint32_t offset = 0; + + do + { + chunk_size = bytes_to_send > chunk_max_size ? chunk_max_size : bytes_to_send; + bytes_to_send -= chunk_size; + bytes_this_chunk = chunk_size; + + nogpu_send_command(buffer + offset, bytes_this_chunk); + offset += chunk_size; + + } while (bytes_to_send > 0); +} + +//============================================================ +// renderer_nogpu::nogpu_send_lz4 +//============================================================ + +void renderer_nogpu::nogpu_send_lz4(char *buffer, int bytes_to_send, int block_size) +{ + LZ4_stream_t lz4_stream_body; + LZ4_stream_t* lz4_stream = &lz4_stream_body; + LZ4_initStream(lz4_stream, sizeof(*lz4_stream)); + + int inp_buf_index = 0; + int bytes_this_chunk = 0; + int chunk_size = 0; + uint32_t offset = 0; + + do + { + chunk_size = bytes_to_send > block_size ? block_size : bytes_to_send; + bytes_to_send -= chunk_size; + bytes_this_chunk = chunk_size; + + char* const inp_ptr = inp_buf[inp_buf_index]; + memcpy((char *)&inp_ptr[0], buffer + offset, chunk_size); + + const uint16_t c_size = LZ4_compress_fast_continue(lz4_stream, inp_ptr, (char *)&m_fb_compressed[2], bytes_this_chunk, MAX_LZ4_BLOCK, 1); + uint16_t *c_size_ptr = (uint16_t *)&m_fb_compressed[0]; + *c_size_ptr = c_size; + + nogpu_send_mtu((char *)&m_fb_compressed[0], c_size + 2, 1472); + offset += chunk_size; + inp_buf_index ^= 1; + + } while (bytes_to_send > 0); +} + +//============================================================ +// renderer_nogpu::nogpu_blit +//============================================================ + +void renderer_nogpu::nogpu_blit(uint32_t frame, uint16_t width, uint16_t height) +{ + // Compressed blocks are 16 lines long + int block_size = m_compression ? (width << 4) * 3 : 0; + + int vsync_offset = 0; + + // Calculate frame delay factor + if (m_is_internal_fe) + // Internal frontend needs fd > 0 + m_frame_delay = .5; + + else if (source_config.framedelay == 0) + // automatic + m_frame_delay = std::max((double)(m_period - std::max(m_fd_margin, get_ms(time_frame_dm))) / m_period, 0.0); + else + { + // user defined + m_frame_delay = (double)(source_config.framedelay) / 10.0; + vsync_offset = 0;// window().machine().video().vsync_offset(); + } + + // Update vsync scanline + m_vsync_scanline = std::min(int((m_current_mode.vtotal) * m_frame_delay + vsync_offset + 1), m_current_mode.vtotal); + + // Send CMD_BLIT + cmd_blit_vsync command; + command.frame = frame; + command.vsync = source_config.syncrefresh ? m_vsync_scanline : 0; + command.block_size = block_size; + nogpu_send_command(&command, sizeof(command)); + + if (m_compression == 0) + nogpu_send_mtu(&m_fb[0], width * height * 3, 1470); + + else + nogpu_send_lz4(&m_fb[0], width * height * 3, block_size); +} + +//============================================================ +// renderer_nogpu::add_audio_to_recording +//============================================================ + +void renderer_nogpu::add_audio_to_recording(const uint16_t *buffer, int samples_this_frame) +{ + if (m_blit_status.bits & FPGA_AUDIO && m_sample_rate) + { + // Send CMD_AUDIO + cmd_audio command; + command.sample_bytes = samples_this_frame << 1; + nogpu_send_command(&command, sizeof(command)); + nogpu_send_mtu((char*)buffer, command.sample_bytes, 1472); + } +} + + +//============================================================ +// renderer_nogpu::nogpu_send_command +//============================================================ + +bool renderer_nogpu::nogpu_send_command(void *command, int command_size) +{ + int rc = sendto(m_sockfd, (char *)command, command_size, 0, (sockaddr*)&m_server_addr, sizeof(m_server_addr)); + + if (rc < 0) + { + LogMessage("Send command failed.", true); + return false; + } + + return true; +}