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 0000000..d036bee
Binary files /dev/null and b/FrontEnd/app.ico differ
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;
+}