diff --git a/Samples/Audio/SimplePlay3DSound/SimplePlay3DSound.h b/Samples/Audio/SimplePlay3DSound/SimplePlay3DSound.h
index 454887f0..4c3eecab 100644
--- a/Samples/Audio/SimplePlay3DSound/SimplePlay3DSound.h
+++ b/Samples/Audio/SimplePlay3DSound/SimplePlay3DSound.h
@@ -18,7 +18,7 @@ class Sample final : public IXAudio2EngineCallback
public:
Sample() noexcept(false);
- virtual ~Sample();
+ ~Sample();
Sample(Sample&&) = delete;
Sample& operator= (Sample&&) = delete;
diff --git a/Samples/Audio/SimplePlaySound/SimplePlaySound.h b/Samples/Audio/SimplePlaySound/SimplePlaySound.h
index f5ce0eae..45310237 100644
--- a/Samples/Audio/SimplePlaySound/SimplePlaySound.h
+++ b/Samples/Audio/SimplePlaySound/SimplePlaySound.h
@@ -18,7 +18,7 @@ class Sample final : public IXAudio2EngineCallback
public:
Sample() noexcept(false);
- virtual ~Sample();
+ ~Sample();
Sample(Sample&&) = delete;
Sample& operator= (Sample&&) = delete;
diff --git a/Samples/Audio/SimplePlaySoundStream/SimplePlaySoundStream.h b/Samples/Audio/SimplePlaySoundStream/SimplePlaySoundStream.h
index 18e964be..4b81d826 100644
--- a/Samples/Audio/SimplePlaySoundStream/SimplePlaySoundStream.h
+++ b/Samples/Audio/SimplePlaySoundStream/SimplePlaySoundStream.h
@@ -63,7 +63,7 @@ class Sample final : public IXAudio2EngineCallback
public:
Sample() noexcept(false);
- virtual ~Sample();
+ ~Sample();
Sample(Sample&&) = delete;
Sample& operator= (Sample&&) = delete;
diff --git a/Samples/Tools/RemoteIterationToolsSample/CopySearchOptions.cs b/Samples/Tools/RemoteIterationToolsSample/CopySearchOptions.cs
index 98e7fe0b..a4d70423 100644
--- a/Samples/Tools/RemoteIterationToolsSample/CopySearchOptions.cs
+++ b/Samples/Tools/RemoteIterationToolsSample/CopySearchOptions.cs
@@ -5,7 +5,9 @@ public class CopySearchOptions
public string? IncludeFilePattern { get; set; }
public string? ExcludeFilePattern { get; set; }
public string? ExcludeDirPattern { get; set; }
- public ulong IncludeFileAttributes { get; set; }
- public ulong ExcludeFileAttributes { get; set; }
+ public uint IncludeFileAttributes { get; set; }
+ public uint ExcludeFileAttributes { get; set; }
+ public uint IncludeDirectoryAttributes { get; set; }
+ public uint ExcludeDirectoryAttributes { get; set; }
}
}
diff --git a/Samples/Tools/RemoteIterationToolsSample/MainWindow.xaml b/Samples/Tools/RemoteIterationToolsSample/MainWindow.xaml
index ba21f09b..2aeaad68 100644
--- a/Samples/Tools/RemoteIterationToolsSample/MainWindow.xaml
+++ b/Samples/Tools/RemoteIterationToolsSample/MainWindow.xaml
@@ -5,13 +5,25 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="Remote Deployment" Height="961" Width="752"
- Background="#FF1E1E24" WindowStartupLocation="CenterScreen">
+ Background="#FF1E1E24" WindowStartupLocation="CenterScreen" Closing="Window_Closing">
+
+
-
+
@@ -97,7 +109,7 @@
-
+
@@ -117,78 +129,113 @@
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -200,9 +247,13 @@
-
+
+
+
+
-
+
diff --git a/Samples/Tools/RemoteIterationToolsSample/MainWindow.xaml.cs b/Samples/Tools/RemoteIterationToolsSample/MainWindow.xaml.cs
index 0174f4bd..1c911eaa 100644
--- a/Samples/Tools/RemoteIterationToolsSample/MainWindow.xaml.cs
+++ b/Samples/Tools/RemoteIterationToolsSample/MainWindow.xaml.cs
@@ -1,30 +1,29 @@
-using Microsoft.Gaming.WdRemoteApi;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Text;
+using Microsoft.Gaming.WdRemoteApi;
+using System.ComponentModel;
using System.Windows;
+using System.Windows.Threading;
using Windows.Win32.Foundation;
namespace RemoteIterationToolsSample
{
- ///
- /// Interaction logic for MainWindow.xaml
- ///
public partial class MainWindow : Window
{
- private WdCancellationHandleWrapper? _cancellationHandleWrapper;
-
- private static ulong _totalBytesToDeploy = 0;
-
- private ProcThreadId _remoteProcessId;
+ private const int OutputBatchSize = 128;
+ private OperationState? _activeOperation;
+ private readonly DispatcherTimer _progressTimer;
public MainWindow()
{
InitializeComponent();
+ _progressTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
+ _progressTimer.Tick += ProgressTimer_Tick;
}
private void BrowseFolder_Click(object sender, RoutedEventArgs e)
{
+ if (_activeOperation != null)
+ return;
+
using var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.Description = "Select folder to deploy";
dialog.UseDescriptionForTitle = true;
@@ -34,385 +33,368 @@ private void BrowseFolder_Click(object sender, RoutedEventArgs e)
}
}
- [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
- public unsafe static HRESULT FileProgressUpdate(
- nuint fileProgressCount,
- WdCopyFileProgressInfo* fileUpdates,
- WdCopyOperationSummary* summary,
- void* context)
+ private bool BeginOperation(OperationState operation)
{
- try
+ if (_activeOperation != null)
+ return false;
+
+ _activeOperation = operation;
+ SetInputsEnabled(false);
+ OutputTextBox.Clear();
+ return true;
+ }
+
+ private void EndOperation(OperationState operation)
+ {
+ _progressTimer.Stop();
+ operation.Dispose();
+ if (ReferenceEquals(_activeOperation, operation))
{
- if (fileUpdates == null || fileProgressCount == 0)
- {
- return new HRESULT(0);
- }
+ _activeOperation = null;
+ CancelButton.IsEnabled = false;
+ SetInputsEnabled(true);
+ }
+ }
- _totalBytesToDeploy = summary->totalByteCount;
+ private void SetInputsEnabled(bool enabled)
+ {
+ DeviceInputs.IsEnabled = enabled;
+ SourceInputs.IsEnabled = enabled;
+ DestinationInputs.IsEnabled = enabled;
+ AdvancedOptions.IsEnabled = enabled;
+ DeployButton.IsEnabled = enabled;
+ ExecutableInputs.IsEnabled = enabled;
+ GameActions.IsEnabled = enabled;
+ }
- var fileProgressLines = new List();
- ulong deployedBytes = summary->bytesTransferredCount;
+ private void Window_Closing(object? sender, CancelEventArgs e)
+ {
+ if (_activeOperation == null)
+ return;
- for (nuint i = 0; i < fileProgressCount; i++)
- {
- WdCopyFileProgressInfo* info = &fileUpdates[i];
- string? filePath = info->relativeFilePath.ToString();
+ e.Cancel = true;
+ AppendOutput("\nWait for the active operation to return before closing. " +
+ "If it appears stalled, choose Cancel when available and wait for it to return. " +
+ "If it remains unresponsive, end RemoteIterationToolsSample.exe in Task Manager as a last resort. " +
+ "Force-closing does not undo completed transfers or confirm that the remote operation stopped.\n");
+ }
- if (info->fileSize > 0)
- {
- double pct = (double)info->bytesTransferred / info->fileSize * 100;
- if (info->bytesTransferred == info->fileSize)
- {
- fileProgressLines.Add($"[Completed] {filePath}\n");
- }
- else
- {
- fileProgressLines.Add($"[Deploy] {filePath} ({info->bytesTransferred:N0} / {info->fileSize:N0} bytes - {pct:F1}%)\n");
- }
- }
- else
- {
- fileProgressLines.Add($"[Completed] {filePath}\n");
- }
- }
+ private void ProgressTimer_Tick(object? sender, EventArgs e)
+ {
+ OperationState? operation = _activeOperation;
+ if (operation == null || !operation.CopyActive)
+ return;
- ulong totalFiles = summary->totalFileCount;
- ulong totalBytes = summary->totalByteCount;
- ulong filesCompleted = summary->filesCompletedCount;
+ if (!DrainCopyUpdates(operation))
+ _progressTimer.Stop();
+ }
- System.Windows.Application.Current?.Dispatcher.InvokeAsync(() =>
+ private bool DrainCopyUpdates(OperationState operation)
+ {
+ if (!ReferenceEquals(_activeOperation, operation))
+ return false;
+
+ try
+ {
+ string output = operation.TakeOutputBatch(OutputBatchSize);
+ Exception? callbackFailure = operation.CallbackFailure;
+ if (callbackFailure != null && !operation.CallbackFailureReported)
+ output += FormatCallbackFailure(callbackFailure);
+ if (output.Length > 0)
+ AppendOutput(output);
+ if (callbackFailure != null)
+ operation.CallbackFailureReported = true;
+
+ if (operation.LatestSummary is WdCopyOperationSummary summary)
{
- var mainWindow = System.Windows.Application.Current?.MainWindow as MainWindow;
- if (mainWindow != null)
+ double percentage = summary.totalByteCount == 0
+ ? 0
+ : Math.Min((double)summary.bytesTransferredCount / summary.totalByteCount * 100, 100);
+ DeploymentProgressBar.Value = percentage;
+ ProgressPercentText.Text = $"{percentage:F1}%";
+ if (!operation.CancellationRequested)
{
- foreach (var line in fileProgressLines)
- {
- mainWindow.AppendOutput(line);
- }
-
- if (totalBytes > 0)
- {
- double overallProgress = Math.Min((double)deployedBytes / totalBytes * 100, 100.0);
- mainWindow.DeploymentProgressBar.Value = overallProgress;
- mainWindow.ProgressPercentText.Text = $"{overallProgress:F1}%";
-
- if (filesCompleted >= totalFiles)
- {
- mainWindow.ProgressStatusText.Text = "Deployment completed.";
- }
- else
- {
- mainWindow.ProgressStatusText.Text = $"Deployed {deployedBytes:N0} / {totalBytes:N0} bytes ({filesCompleted:N0} / {totalFiles:N0} files)";
- }
- }
+ ProgressStatusText.Text =
+ $"Copied {summary.bytesTransferredCount:N0} / {summary.totalByteCount:N0} bytes " +
+ $"({summary.filesCompletedCount:N0} / {summary.totalFileCount:N0} files); " +
+ (operation.CopyActive ? "waiting for copy result." : "displaying remaining copy output.");
}
- }, System.Windows.Threading.DispatcherPriority.Background);
+ }
+ return true;
+ }
+ catch (Exception ex)
+ {
+ // WPF processing errors need the same visible outcome as native callback errors.
+ operation.RecordCallbackFailure(ex);
+ ReportCallbackFailure(operation);
+ return false;
+ }
+ }
+
+ private async Task DrainRemainingCopyUpdatesAsync(OperationState operation)
+ {
+ while (ReferenceEquals(_activeOperation, operation))
+ {
+ if (!DrainCopyUpdates(operation) || !operation.HasPendingOutput)
+ return;
- return new HRESULT(0);
+ // Let input and rendering run between batches, even after the native call returns.
+ await System.Windows.Threading.Dispatcher.Yield(DispatcherPriority.Background);
}
- catch
+ }
+
+ private static string FormatCallbackFailure(Exception ex) =>
+ $"\n✗ Callback processing failed: {ex.Message} (HRESULT 0x{ex.HResult:X8}). " +
+ "A callback failure does not stop native copy or replace its final HRESULT.\n";
+
+ private void ReportCallbackFailure(OperationState operation)
+ {
+ if (operation.CallbackFailure is Exception ex && !operation.CallbackFailureReported)
{
- return new HRESULT(-1);
+ operation.CallbackFailureReported = true;
+ AppendOutput(FormatCallbackFailure(ex));
}
}
private async void Deploy_Click(object sender, RoutedEventArgs e)
{
+ if (_activeOperation != null)
+ return;
+
string ip = DeviceIpTextBox.Text.Trim();
string folder = FolderPathTextBox.Text.Trim();
string destination = DestinationPathTextBox.Text.Trim();
-
- if (string.IsNullOrWhiteSpace(ip))
- {
- System.Windows.MessageBox.Show("Please enter a device name or IP Address.");
- return;
- }
- if (string.IsNullOrWhiteSpace(folder))
- {
- System.Windows.MessageBox.Show("Select a deployment folder.");
- return;
- }
- if (string.IsNullOrWhiteSpace(destination))
+ if (string.IsNullOrWhiteSpace(ip) || string.IsNullOrWhiteSpace(folder) || string.IsNullOrWhiteSpace(destination))
{
- System.Windows.MessageBox.Show("Enter destination folder name.");
+ System.Windows.MessageBox.Show("Enter a device, a local source folder, and a remote destination folder.");
return;
}
- _cancellationHandleWrapper = new WdCancellationHandleWrapper();
+ if (!TryReadAttributeMask(IncludeFileAttributesTextBox.Text, "Include File Attributes", out uint includeAttributes) ||
+ !TryReadAttributeMask(ExcludeFileAttributesTextBox.Text, "Exclude File Attributes", out uint excludeAttributes))
+ return;
-
- // Disable deploy button during deployment
- var deployButton = sender as System.Windows.Controls.Button;
- if (deployButton != null)
+ var operation = new OperationState("Deployment", ip, destination)
{
- deployButton.IsEnabled = false;
- }
-
- OutputTextBox.Clear();
-
- // Reset progress tracking
- _totalBytesToDeploy = 0;
-
- AppendOutput($"> Deploying to {ip}:{destination} ...\n\n");
- AppendOutput($"Source: {folder}\n");
- AppendOutput($"Destination: {destination}\n\n");
-
- string includeFilePattern = IncludeFilePatternTextBox.Text.Trim();
- string excludeFilePattern = ExcludeFilePatternTextBox.Text.Trim();
- string excludeDirPattern = ExcludeDirPatternTextBox.Text.Trim();
- string includeFileAttribText = IncludeFileAttributesTextBox.Text.Trim();
- string excludeFileAttribText = ExcludeFileAttributesTextBox.Text.Trim();
+ LocalSourcePath = folder,
+ SearchOptions = new CopySearchOptions
+ {
+ IncludeFilePattern = IncludeFilePatternTextBox.Text.Trim(),
+ ExcludeFilePattern = ExcludeFilePatternTextBox.Text.Trim(),
+ ExcludeDirPattern = ExcludeDirPatternTextBox.Text.Trim(),
+ IncludeFileAttributes = includeAttributes,
+ ExcludeFileAttributes = excludeAttributes
+ }
+ };
+ if (!BeginOperation(operation))
+ return;
try
{
- // Build search options from advanced options UI
- CopySearchOptions searchOptions = new CopySearchOptions();
-
- if (!string.IsNullOrEmpty(includeFilePattern))
- searchOptions.IncludeFilePattern = includeFilePattern;
- if (!string.IsNullOrEmpty(excludeFilePattern))
- searchOptions.ExcludeFilePattern = excludeFilePattern;
- if (!string.IsNullOrEmpty(excludeDirPattern))
- searchOptions.ExcludeDirPattern = excludeDirPattern;
- if (!string.IsNullOrEmpty(includeFileAttribText))
+ DeploymentProgressBar.Value = 0;
+ ProgressPercentText.Text = "0%";
+ ProgressStatusText.Text = "Copying; waiting for progress.";
+ AppendOutput($"> Deploying to {operation.Device}:{operation.RemotePath}\nSource: {operation.LocalSourcePath}\n\n");
+ WdCopyStatusCallbacks callbacks = operation.CreateCopyCallbacks();
+ operation.CopyActive = true;
+ CancelButton.IsEnabled = true;
+ _progressTimer.Start();
+
+ HRESULT hr = await RemoteIteration.CopyAsync(
+ remoteDevice: operation.Device,
+ localSourcePath: operation.LocalSourcePath,
+ remoteDestPath: operation.RemotePath,
+ cancellationHandleWrapper: operation.CancellationHandle,
+ searchOptions: operation.SearchOptions,
+ statusCallbacks: callbacks);
+
+ operation.CopyActive = false;
+ CancelButton.IsEnabled = false;
+ _progressTimer.Stop();
+ // Callbacks only enqueue owned data. Drain before setting the authoritative result.
+ AppendOutput($"\nCopy returned HRESULT 0x{hr.Value:X8}.\n");
+ ProgressStatusText.Text = "Copy returned; displaying remaining output.";
+ await DrainRemainingCopyUpdatesAsync(operation);
+ if (hr.Failed)
{
- if (ulong.TryParse(includeFileAttribText, out ulong includeAttribs))
- searchOptions.IncludeFileAttributes = includeAttribs;
- else
- {
- System.Windows.MessageBox.Show("Include File Attributes must be a valid number.");
- return;
- }
+ ProgressStatusText.Text = "Deployment failed during copy.";
+ AppendOutput($"✗ Copy failed: HRESULT 0x{hr.Value:X8}." +
+ (operation.CancellationRequested ? " Cancellation was also requested; this does not establish the failure's cause." : "") +
+ " Remote path validation skipped.\n");
+ return;
}
- if (!string.IsNullOrEmpty(excludeFileAttribText))
+ if (operation.CancellationRequested)
{
- if (ulong.TryParse(excludeFileAttribText, out ulong excludeAttribs))
- searchOptions.ExcludeFileAttributes = excludeAttribs;
- else
- {
- System.Windows.MessageBox.Show("Exclude File Attributes must be a valid number.");
- return;
- }
+ ProgressStatusText.Text = "Cancellation requested; deployment completeness not confirmed.";
+ AppendOutput("Cancellation was requested. Copy returning success does not confirm a complete deployment. Remote path validation skipped.\n");
+ return;
}
-
- WdCopyStatusCallbacks copyStatusCallbacks = new WdCopyStatusCallbacks();
-
- unsafe
+ if (operation.CallbackFailure != null)
{
- copyStatusCallbacks.copyFilesStatusCallback = &FileProgressUpdate;
+ ProgressStatusText.Text = "Copy returned success, but callback processing failed.";
+ AppendOutput("Deployment status is not confirmed because callback processing failed. Remote path validation skipped.\n");
+ return;
}
- copyStatusCallbacks.refreshRateMs = 500;
-
- HRESULT hr = await RemoteIteration.CopyAsync(
- remoteDevice: ip,
- localSourcePath: folder,
- remoteDestPath: destination,
- cancellationHandleWrapper: _cancellationHandleWrapper,
- searchOptions: searchOptions,
- statusCallbacks: copyStatusCallbacks);
+ ProgressStatusText.Text = "Copy finished. Validating remote path.";
+ AppendOutput("Validating remote path with WdRegisterRemoteXboxGame (not platform game registration).\n");
+ hr = await RemoteIteration.RegisterRemoteXboxGameAsync(operation.Device, operation.RemotePath);
+ AppendOutput($"Remote path validation returned HRESULT 0x{hr.Value:X8}.\n");
if (hr.Failed)
{
- AppendOutput($"\n✗ Deployment failed during copy: HRESULT 0x{hr.Value:X8}\n");
- System.Windows.MessageBox.Show($"Deployment failed during copy with error code: 0x{hr.Value:X8}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ ProgressStatusText.Text = "Copy finished; remote path validation failed.";
return;
}
DeploymentProgressBar.Value = 100;
ProgressPercentText.Text = "100%";
- ProgressStatusText.Text = "Deployment completed.";
-
- hr = await RemoteIteration.RegisterRemoteXboxGameAsync(ip, destination);
-
- if (hr.Failed)
- {
- AppendOutput($"\n✗ Deployment failed during registration: HRESULT 0x{hr.Value:X8}\n");
- System.Windows.MessageBox.Show($"Deployment failed during registration with error code: 0x{hr.Value:X8}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
- return;
- }
- }
- catch (RemoteIterationException ex)
- {
- AppendOutput($"\n✗ Deployment failed: {ex.Message} (HRESULT: 0x{ex.HResult:X8})\n");
- System.Windows.MessageBox.Show($"Deployment failed: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
-
- ProgressStatusText.Text = "Deployment failed.";
+ ProgressStatusText.Text = "Deployment completed; remote path validated.";
}
catch (Exception ex)
{
- AppendOutput($"\n✗ Deployment failed: {ex.Message}\n");
- System.Windows.MessageBox.Show($"Deployment failed: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
-
+ operation.CopyActive = false;
+ CancelButton.IsEnabled = false;
+ _progressTimer.Stop();
+ await DrainRemainingCopyUpdatesAsync(operation);
+ ShowOperationFailure(operation, ex);
ProgressStatusText.Text = "Deployment failed.";
}
finally
{
- if (deployButton != null)
- {
- deployButton.IsEnabled = true;
- }
-
- _cancellationHandleWrapper?.Dispose();
- _cancellationHandleWrapper = null;
+ operation.CopyActive = false;
+ EndOperation(operation);
}
}
- private async void Cancel_Click(object sender, RoutedEventArgs e)
+ private static bool TryReadAttributeMask(string text, string name, out uint value)
+ {
+ text = text.Trim();
+ value = 0;
+ if (text.Length == 0 || uint.TryParse(text, out value))
+ return true;
+
+ System.Windows.MessageBox.Show($"{name} must be a decimal number between 0 and {uint.MaxValue}.", "Invalid attributes");
+ return false;
+ }
+
+ private void Cancel_Click(object sender, RoutedEventArgs e)
{
+ OperationState? operation = _activeOperation;
+ if (operation == null || !operation.CopyActive || operation.CancellationRequested || operation.CancellationHandle == null)
+ return;
+
+ operation.CancellationRequested = true;
+ CancelButton.IsEnabled = false;
+ ProgressStatusText.Text = "Cancellation requested; waiting for copy to return.";
+ AppendOutput("\nCancellation requested; waiting for the copy result.\n");
try
{
- if(_cancellationHandleWrapper == null)
+ // The signal is nonblocking. Running it on the UI thread prevents the awaited
+ // copy continuation from disposing its handle while cancellation uses it.
+ HRESULT hr = RemoteIteration.RequestCopyCancellation(operation.CancellationHandle);
+ AppendOutput($"Cancellation signal returned HRESULT 0x{hr.Value:X8}.\n");
+ if (hr.Failed)
{
- AppendOutput("\n⚠ No deployment to cancel.\n");
- return;
+ ProgressStatusText.Text = "Cancellation signal failed; waiting for copy to return.";
+ AppendOutput("✗ Cancellation signal failed; copy may continue. Cancellation intent is retained.\n");
}
-
- await RemoteIteration.CancelDeployAsync(_cancellationHandleWrapper);
-
- OutputTextBox.Clear();
- AppendOutput($"Deploy cancelled \n\n");
- }
- catch (RemoteIterationException ex)
- {
- AppendOutput($"\n✗ Cancel failed: {ex.Message} (HRESULT: 0x{ex.HResult:X8})\n");
- System.Windows.MessageBox.Show($"Cancel failed: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
catch (Exception ex)
{
- AppendOutput($"\n✗ Cancel failed: {ex.Message}\n");
- System.Windows.MessageBox.Show($"Cancel failed: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ AppendOutput($"✗ Cancellation signal failed: {ex.Message} (HRESULT 0x{ex.HResult:X8}). Copy may continue.\n");
+ ProgressStatusText.Text = "Cancellation signal failed; waiting for copy to return.";
}
}
-
+
private async void Launch_Click(object sender, RoutedEventArgs e)
{
- await LaunchProcessAsync(launchOptions: null);
+ await LaunchProcessAsync(null);
}
private async void LaunchSuspended_Click(object sender, RoutedEventArgs e)
{
- var launchOptions = new WdLaunchOptions
- {
- launchMode = WdLaunchMode.Suspended
- };
-
- await LaunchProcessAsync(launchOptions);
+ await LaunchProcessAsync(new WdLaunchOptions { launchMode = WdLaunchMode.Suspended });
}
private async Task LaunchProcessAsync(WdLaunchOptions? launchOptions)
{
string ip = DeviceIpTextBox.Text.Trim();
string executable = ExecutablePathTextBox.Text.Trim();
-
- if (string.IsNullOrWhiteSpace(ip))
+ if (string.IsNullOrWhiteSpace(ip) || string.IsNullOrWhiteSpace(executable))
{
- System.Windows.MessageBox.Show("Please enter a device name or IP Address.");
- return;
- }
- if (string.IsNullOrWhiteSpace(executable))
- {
- System.Windows.MessageBox.Show("Please enter an executable path.");
+ System.Windows.MessageBox.Show("Enter a device and a remote executable path.");
return;
}
- OutputTextBox.Clear();
+ var operation = new OperationState("Launch", ip, executable) { LaunchOptions = launchOptions };
+ if (!BeginOperation(operation))
+ return;
try
{
- (HRESULT hr, ProcThreadId procThreadId) = await RemoteIteration.LaunchRemoteGameAsync(
- remoteDevice: ip, remotePath: executable, launchOptions: launchOptions);
-
- if (hr.Failed)
- {
- AppendOutput($"\n✗ Launch failed: HRESULT 0x{hr.Value:X8}\n");
- System.Windows.MessageBox.Show($"Launch failed with error code: 0x{hr.Value:X8}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
- return;
- }
-
- _remoteProcessId = procThreadId;
-
- AppendOutput($"> Launching application on {ip} ...\n\n");
- AppendOutput($"Path: {executable}\n");
- AppendOutput($"\nDone. (Process ID: {_remoteProcessId.ProcessId}, Thread ID: {_remoteProcessId.ThreadId})\n");
+ AppendOutput($"> Launching {operation.RemotePath} on {operation.Device}" +
+ (operation.LaunchOptions?.launchMode == WdLaunchMode.Suspended ? " (suspended)" : "") + " ...\n");
+ (HRESULT hr, ProcThreadId process) = await RemoteIteration.LaunchRemoteGameAsync(
+ operation.Device, operation.RemotePath, launchOptions: operation.LaunchOptions);
+ AppendOutput(hr.Failed
+ ? $"✗ Launch failed: HRESULT 0x{hr.Value:X8}.\n"
+ : $"Launch succeeded (Process ID: {process.ProcessId}, Thread ID: {process.ThreadId}).\n");
}
catch (Exception ex)
{
- AppendOutput($"\n✗ Launch failed: {ex.Message}\n");
- System.Windows.MessageBox.Show($"Launch failed: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ ShowOperationFailure(operation, ex);
+ }
+ finally
+ {
+ EndOperation(operation);
}
}
private async void Resume_Click(object sender, RoutedEventArgs e)
{
- string ip = DeviceIpTextBox.Text.Trim();
- if (string.IsNullOrWhiteSpace(ip))
- {
- System.Windows.MessageBox.Show("Please enter a device name or IP Address.");
- return;
- }
- OutputTextBox.Clear();
-
- try
- {
- HRESULT hr = await RemoteIteration.ResumeGameAsync(ip);
-
- if (hr.Failed)
- {
- AppendOutput($"{ip} failed to resume remote game with error 0x{hr.Value:X8}.");
- }
- else
- {
- AppendOutput($"{ip} resumed remote game {_remoteProcessId.ProcessId} successfully.");
- }
- }
- catch (RemoteIterationException ex)
- {
- AppendOutput($"\n✗ Resume failed: {ex.Message} (HRESULT: 0x{ex.HResult:X8})\n");
- System.Windows.MessageBox.Show($"Resume failed: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- catch (Exception ex)
- {
- AppendOutput($"\n✗ Resume failed: {ex.Message}\n");
- System.Windows.MessageBox.Show($"Resume failed: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
- }
+ await ControlGameAsync(resume: true);
}
private async void Terminate_Click(object sender, RoutedEventArgs e)
{
- string ip = DeviceIpTextBox.Text.Trim();
+ await ControlGameAsync(resume: false);
+ }
+ private async Task ControlGameAsync(bool resume)
+ {
+ string ip = DeviceIpTextBox.Text.Trim();
if (string.IsNullOrWhiteSpace(ip))
{
- System.Windows.MessageBox.Show("Please enter a device name or IP Address.");
+ System.Windows.MessageBox.Show("Enter a device name or IP address.");
return;
}
- OutputTextBox.Clear();
-
+ var operation = new OperationState(resume ? "Resume" : "Terminate", ip);
+ if (!BeginOperation(operation))
+ return;
+
try
{
- HRESULT hr = await RemoteIteration.TerminateRemoteGameAsync(ip);
-
- if (hr.Failed)
- {
- AppendOutput($"{ip} failed to terminate remote game with error 0x{hr.Value:X8}.");
- }
- else
- {
- AppendOutput($"{ip} terminated remote game successfully.");
- }
+ HRESULT hr = resume
+ ? await RemoteIteration.ResumeGameAsync(operation.Device)
+ : await RemoteIteration.TerminateRemoteGameAsync(operation.Device);
+ AppendOutput($"{operation.Name} on {operation.Device} " +
+ (hr.Failed ? "failed" : "succeeded") + $" (HRESULT 0x{hr.Value:X8}).\n");
}
- catch (RemoteIterationException ex)
+ catch (Exception ex)
{
- AppendOutput($"\n✗ Terminate failed: {ex.Message} (HRESULT: 0x{ex.HResult:X8})\n");
- System.Windows.MessageBox.Show($"Terminate failed: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ ShowOperationFailure(operation, ex);
}
- catch (Exception ex)
+ finally
{
- AppendOutput($"\n✗ Terminate failed: {ex.Message}\n");
- System.Windows.MessageBox.Show($"Terminate failed: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ EndOperation(operation);
}
-
+ }
+
+ private void ShowOperationFailure(OperationState operation, Exception ex)
+ {
+ AppendOutput($"\n✗ {operation.Name} failed: {ex.Message} (HRESULT 0x{ex.HResult:X8})." +
+ (operation.CancellationRequested ? " Cancellation was also requested." : "") + "\n");
}
private void AppendOutput(string text)
@@ -421,4 +403,4 @@ private void AppendOutput(string text)
OutputTextBox.ScrollToEnd();
}
}
-}
\ No newline at end of file
+}
diff --git a/Samples/Tools/RemoteIterationToolsSample/OperationState.cs b/Samples/Tools/RemoteIterationToolsSample/OperationState.cs
new file mode 100644
index 00000000..7b8bc6be
--- /dev/null
+++ b/Samples/Tools/RemoteIterationToolsSample/OperationState.cs
@@ -0,0 +1,176 @@
+using Microsoft.Gaming.WdRemoteApi;
+using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using Windows.Win32.Foundation;
+
+namespace RemoteIterationToolsSample
+{
+ internal sealed class OperationState : IDisposable
+ {
+ private GCHandle _callbackContext;
+ private Exception? _callbackFailure;
+ private CopyProgressUpdate? _latestProgressUpdate;
+ private CopyProgressUpdate? _pendingProgressUpdate;
+ private int _nextFileMessage;
+
+ public OperationState(string name, string device, string remotePath = "")
+ {
+ Name = name;
+ Device = device;
+ RemotePath = remotePath;
+ }
+
+ public string Name { get; }
+ public string Device { get; }
+ public string RemotePath { get; }
+ public string LocalSourcePath { get; init; } = "";
+ public CopySearchOptions? SearchOptions { get; init; }
+ public WdLaunchOptions? LaunchOptions { get; init; }
+ public WdCancellationHandleWrapper? CancellationHandle { get; private set; }
+ public bool CopyActive { get; set; }
+ public bool CancellationRequested { get; set; }
+ public bool CallbackFailureReported { get; set; }
+ public Exception? CallbackFailure => Volatile.Read(ref _callbackFailure);
+ public ConcurrentQueue ProgressUpdates { get; } = new();
+ public ConcurrentQueue Diagnostics { get; } = new();
+ public WdCopyOperationSummary? LatestSummary => Volatile.Read(ref _latestProgressUpdate)?.Summary;
+ public bool HasPendingOutput => _pendingProgressUpdate != null || !ProgressUpdates.IsEmpty || !Diagnostics.IsEmpty;
+
+ // Only the UI thread consumes batches. The budget limits dispatch work, not retained output.
+ public string TakeOutputBatch(int maximumMessages)
+ {
+ if (maximumMessages < 1)
+ throw new ArgumentOutOfRangeException(nameof(maximumMessages));
+
+ var output = new StringBuilder();
+ while (maximumMessages > 0)
+ {
+ if (Diagnostics.TryDequeue(out string? diagnostic))
+ {
+ output.Append(diagnostic);
+ maximumMessages--;
+ continue;
+ }
+
+ if (_pendingProgressUpdate == null && !ProgressUpdates.TryDequeue(out _pendingProgressUpdate))
+ break;
+
+ List messages = _pendingProgressUpdate.FileMessages;
+ while (maximumMessages > 0 && _nextFileMessage < messages.Count)
+ {
+ output.Append(messages[_nextFileMessage++]);
+ maximumMessages--;
+ }
+
+ if (_nextFileMessage == messages.Count)
+ {
+ // Summary-only notifications must also consume budget to bound each batch.
+ if (messages.Count == 0)
+ maximumMessages--;
+ _pendingProgressUpdate = null;
+ _nextFileMessage = 0;
+ }
+ }
+ return output.ToString();
+ }
+
+ public unsafe WdCopyStatusCallbacks CreateCopyCallbacks()
+ {
+ CancellationHandle = new WdCancellationHandleWrapper();
+ _callbackContext = GCHandle.Alloc(this);
+ return new WdCopyStatusCallbacks
+ {
+ copyFilesStatusCallback = &FileProgressUpdate,
+ copyErrorCallback = &CopyDiagnostic,
+ refreshRateMs = 500, // Desired update interval, not a guaranteed cadence.
+ context = (void*)GCHandle.ToIntPtr(_callbackContext)
+ };
+ }
+
+ public void RecordCallbackFailure(Exception exception)
+ {
+ // Preserve the first failure without allocating or invoking UI code on a native thread.
+ Interlocked.CompareExchange(ref _callbackFailure, exception, null);
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static unsafe HRESULT FileProgressUpdate(
+ nuint fileProgressCount,
+ WdCopyFileProgressInfo* fileUpdates,
+ WdCopyOperationSummary* summary,
+ void* context)
+ {
+ // This context is owned until the blocking copy call and all its callbacks return.
+ OperationState? operation = null;
+ try
+ {
+ operation = (OperationState)GCHandle.FromIntPtr((IntPtr)context).Target!;
+ if (summary == null || (fileProgressCount > 0 && fileUpdates == null))
+ {
+ throw new InvalidOperationException("Copy progress callback supplied missing progress data.");
+ }
+
+ var lines = new List();
+ for (nuint i = 0; i < fileProgressCount; i++)
+ {
+ WdCopyFileProgressInfo info = fileUpdates[i];
+ string? filePath = Marshal.PtrToStringUTF8((IntPtr)info.relativeFilePath.Value);
+ if (info.bytesTransferred == info.fileSize)
+ {
+ lines.Add($"[Completed] {filePath}\n");
+ }
+ else
+ {
+ lines.Add($"[Deploy] {filePath} ({info.bytesTransferred:N0} / {info.fileSize:N0} bytes)\n");
+ }
+ }
+
+ // Copy the strings and scalar counters; no native pointer survives the callback.
+ // A summary without per-file updates is still a valid progress notification.
+ var update = new CopyProgressUpdate(lines, *summary);
+ operation.ProgressUpdates.Enqueue(update);
+ Volatile.Write(ref operation._latestProgressUpdate, update);
+ return new HRESULT(0);
+ }
+ catch (Exception ex)
+ {
+ operation?.RecordCallbackFailure(ex);
+ // A failed callback HRESULT does not stop the copy. The UI also reports this failure.
+ return new HRESULT(ex.HResult);
+ }
+ }
+
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })]
+ private static unsafe HRESULT CopyDiagnostic(
+ WdCopyErrorSeverity severity, PCSTR message, HRESULT error, void* context)
+ {
+ OperationState? operation = null;
+ try
+ {
+ operation = (OperationState)GCHandle.FromIntPtr((IntPtr)context).Target!;
+ operation.Diagnostics.Enqueue($"[{severity}] {Marshal.PtrToStringUTF8((IntPtr)message.Value)} (HRESULT 0x{error.Value:X8})\n");
+ return new HRESULT(0);
+ }
+ catch (Exception ex)
+ {
+ operation?.RecordCallbackFailure(ex);
+ return new HRESULT(ex.HResult);
+ }
+ }
+
+ public void Dispose()
+ {
+ // Called on the UI thread only after awaiting native work. Cancellation is signalled
+ // synchronously on that same thread, so it cannot race this resource cleanup.
+ if (_callbackContext.IsAllocated)
+ {
+ _callbackContext.Free();
+ }
+ CancellationHandle?.Dispose();
+ }
+ }
+
+ internal sealed record CopyProgressUpdate(List FileMessages, WdCopyOperationSummary Summary);
+}
diff --git a/Samples/Tools/RemoteIterationToolsSample/README.md b/Samples/Tools/RemoteIterationToolsSample/README.md
index b4141fcd..4f173a07 100644
--- a/Samples/Tools/RemoteIterationToolsSample/README.md
+++ b/Samples/Tools/RemoteIterationToolsSample/README.md
@@ -19,34 +19,218 @@ description: "A WPF sample application demonstrating how to use the Remote Itera
# Remote Iteration Tools Sample
-A WPF sample application demonstrating how to use the **Remote Iteration Client API**
+A WPF sample application demonstrating how to use the **Xbox PC Remote Iteration API**
(`Microsoft.GDK.RemoteIterationClientApi`) to deploy, launch, and manage game builds on
-remote Xbox devices.
+remote development devices.
## What This Sample Demonstrates
- **Deploy** — Copy a local game build to a remote device using delta file transfer
-- **Register** — Register a deployed game on the remote device
+- **Validate remote path** — After a successful, uncancelled copy, call
+ `WdRegisterRemoteXboxGame` to resolve and validate the remote folder path.
+ Despite its name, this API does not perform platform game registration.
- **Launch** — Start an executable on the remote device (normal or suspended)
- **Resume** — Resume a suspended game process
-- **Terminate** — Stop a running game on the remote device
-- **Cancel** — Cancel an in-progress deployment
+- **Terminate** — Stop the last game launched through the API on the remote device
+- **Cancel** — Request cancellation of an in-progress copy, then wait for its result
+
+The deploy and game-control actions are implemented in
+[`MainWindow.xaml.cs`, lines 159–393](MainWindow.xaml.cs#L159-L393).
+See also the [path-validation wrapper, lines 60–80](RemoteIteration.cs#L60-L80).
## Prerequisites
-- Visual Studio with support for WPF applications
-- .NET SDK installed
-- Pair remote device using [Xbox PC Remote Tools overview](https://learn.microsoft.com/en-us/gaming/gdk/docs/gdk-dev/pc-dev/overviews/remote-win-gamedev)
+- Windows with Visual Studio WPF support, or the .NET 8 SDK for command-line builds
+ ([project settings, lines 3–16](RemoteIterationToolsSample.csproj#L3-L16))
+- The `Microsoft.GDK.RemoteIterationClientApi` NuGet package version referenced
+ by the project, restored during the build
+ ([package references, lines 30–37](RemoteIterationToolsSample.csproj#L30-L37))
+- A compatible Remote Tools endpoint running on a target device paired with the
+ development PC. Follow the [Xbox PC Remote Tools setup and pairing guide](https://learn.microsoft.com/en-us/gaming/gdk/docs/gdk-dev/pc-dev/overviews/remote-win-gamedev).
## Build
-Open `RemoteIterationToolsSample.sln` in Visual Studio and build the **x64** or **ARM64** configuration,
-or from the command line:
+Open `RemoteIterationToolsSample.sln` in Visual Studio and build the **x64** or
+**ARM64** configuration, or run these commands from this sample's directory:
```shell
-dotnet build RemoteIterationToolsSample/RemoteIterationToolsSample.csproj -p:Platform=x64
-dotnet build RemoteIterationToolsSample/RemoteIterationToolsSample.csproj -p:Platform=ARM64
+dotnet build .\RemoteIterationToolsSample.csproj -c Release -p:Platform=x64
+dotnet build .\RemoteIterationToolsSample.csproj -c Release -p:Platform=ARM64
+```
+
+The API NuGet package supplies the native binary and WinMD metadata.
+CsWin32 generates the C# bindings from that metadata
+([project, lines 20–37](RemoteIterationToolsSample.csproj#L20-L37)).
+
+## Using the sample
+
+The three numbered groups in the UI cover device selection, deployment, and game
+control. These are separate actions: **Deploy does not launch the game, and Launch
+does not deploy it**. Use your own paired device and a build containing a runnable
+executable and its dependencies.
+
+### 1. Choose a paired device
+
+Complete the [Remote Tools setup and pairing workflow](https://learn.microsoft.com/en-us/gaming/gdk/docs/gdk-dev/pc-dev/overviews/remote-win-gamedev)
+first, with the Remote Tools endpoint running on the target. The sample does not
+discover, provision, or pair devices.
+
+Enter the target's **IP address or resolvable host name** in **Device**, for example
+`192.168.1.100` or `MyDevPC`. This field identifies the remote machine, not a local
+folder or executable ([device guidance, lines 132–138](MainWindow.xaml#L132-L138)).
+
+### 2. Deploy a local build
+
+Suppose your local build contains `C:\Builds\MyGame\MyGame.exe`. Use a disposable
+remote destination while learning the sample.
+
+| UI field | Example value | Meaning |
+|----------|---------------|---------|
+| Device | `MyDevPC` | The paired target machine |
+| Source Folder (this PC) | `C:\Builds\MyGame` | Local build folder; **Browse** selects a folder on this PC |
+| Destination Folder (device) | `MyGame` | Remote folder relative to the endpoint's default Common Root |
+| Executable to launch (device) | `MyGame\MyGame.exe` | Remote executable to use in step 3 |
+
+The paths connect like this:
+
+```text
+This PC Paired target device
+C:\Builds\MyGame\MyGame.exe --Deploy--> \MyGame\MyGame.exe
+
+Destination Folder: MyGame
+Executable to launch: MyGame\MyGame.exe
+```
+
+Relative destination and executable paths start at the endpoint's **default Common
+Root**. Absolute paths on the target, such as `D:\Games\MyGame` and
+`D:\Games\MyGame\MyGame.exe`, can be used instead. The UI does not expose a Common Root
+alias. **Executable to launch is not automatically relative to
+Destination Folder**: entering just `MyGame.exe` does not prepend `MyGame`.
+The sample passes the destination and executable as separate API inputs
+([deployment inputs, lines 164–188](MainWindow.xaml.cs#L164-L188);
+[launch inputs, lines 321–338](MainWindow.xaml.cs#L321-L338);
+[destination guidance, lines 154–162](MainWindow.xaml#L154-L162);
+[executable guidance, lines 217–227](MainWindow.xaml#L217-L227)).
+
+Leave **Advanced Options** blank for the first deployment. Empty patterns use the
+default selection and blank attribute masks mean zero. Choose **Deploy**. It calls
+`WdRemoteCopy` and, only after successful copy without cancellation or a callback
+processing failure, `WdRegisterRemoteXboxGame` to validate the remote folder path.
+This is **not game installation or platform registration**.
+
+Wait for **"Deployment completed; remote path validated."** in the status area.
+Use **Command Output** to inspect the copy and path-validation HRESULTs or failure
+messages; progress reaching 100% is not the authoritative result
+([deployment result handling, lines 209–252](MainWindow.xaml.cs#L209-L252)).
+
+During a copy, choose **Cancel** to request cancellation and wait for the operation
+to finish. Cancellation does not establish
+that the build is complete, even when the copy's HRESULT indicates success; the
+sample skips path validation in that case
+([cancellation, lines 280–307](MainWindow.xaml.cs#L280-L307);
+[cancelled-copy result, lines 226–231](MainWindow.xaml.cs#L226-L231)).
+
+### 3. Launch and control the remote game
+
+After deployment, enter **`MyGame\MyGame.exe`** in **Executable to launch** for this
+example. You may instead launch a build already present on the device.
+
+Choose **Launch** to start the executable normally. Alternatively, choose
+**Launch Suspended** to start it suspended, then **Resume** to let it run.
+Check **Command Output** for the result and returned process/thread IDs.
+Choose **Terminate** to stop the game.
+
+Resume operates on the last game launched suspended through `WdLaunchRemoteGame`;
+Terminate operates on the last game launched through that API. These buttons use
+**Device**, not the executable-path field, and do not accept an arbitrary process ID
+([launch actions, lines 309–350](MainWindow.xaml.cs#L309-L350);
+[resume/terminate, lines 353–390](MainWindow.xaml.cs#L353-L390);
+[native wrappers, lines 82–144](RemoteIteration.cs#L82-L144)).
+
+Each new action clears **Command Output**. Copy any diagnostics you want to keep
+before starting the next action
+([operation start, lines 36–45](MainWindow.xaml.cs#L36-L45)).
+
+### Common Root configuration
+
+A **Common Root** is a named base directory on the target device for relative
+remote paths. If no roots are configured, the default is
+`%ProgramData%\Microsoft GDK\gameroot`. This sample uses the endpoint's default
+Common Root; it does not configure roots.
+
+To customize the roots, create or edit
+`%ProgramData%\Microsoft GDK\wdEndpoint\wdEndpoint.json` **on the target device**.
+Merge the `gameRoots` setting into any existing configuration, preserving other
+settings. For example:
+
+```json
+{
+ "gameRoots": [
+ {
+ "root": "D:\\Games",
+ "alias": "games",
+ "default": true
+ }
+ ]
+}
```
-> **Note:** The project references the `Microsoft.GDK.RemoteIterationClientApi` NuGet
-> package.
+Restart `wdEndpoint` after changing the configuration. With this example, the
+sample's relative destination `MyGame` resolves to `D:\Games\MyGame`.
+Additional entries define other aliases; mark the intended default with
+`"default": true`. See the [Common Root configuration guide](https://learn.microsoft.com/en-us/gaming/gdk/docs/tools/tools-pc/commandlinetools/gr-wdremote#deploy,-launch,-and-terminate-applications)
+for details.
+
+## From UI action to API call
+
+Use the walkthrough to observe the behavior, then follow these entry points to
+see the integration code:
+
+| UI action | Handler | Managed wrapper and native API |
+|-----------|---------|--------------------------------|
+| Deploy: copy | [`Deploy_Click`, lines 159–210](MainWindow.xaml.cs#L159-L210) | [`CopyAsync` → `WdRemoteCopy`, lines 16–58](RemoteIteration.cs#L16-L58) |
+| Deploy: validate remote path | [`Deploy_Click`, lines 238–246](MainWindow.xaml.cs#L238-L246) | [`RegisterRemoteXboxGameAsync` → `WdRegisterRemoteXboxGame`, lines 60–80](RemoteIteration.cs#L60-L80) |
+| Cancel | [`Cancel_Click`, lines 280–307](MainWindow.xaml.cs#L280-L307) | [`RequestCopyCancellation` → `WdCancelRemoteCopy`, lines 148–156](RemoteIteration.cs#L148-L156) |
+| Launch / Launch Suspended | [`LaunchProcessAsync`, lines 309–350](MainWindow.xaml.cs#L309-L350) | [`LaunchRemoteGameAsync` → `WdLaunchRemoteGame`, lines 101–131](RemoteIteration.cs#L101-L131); suspended launch supplies `WdLaunchMode.Suspended` |
+| Resume | [`Resume_Click` / `ControlGameAsync`, lines 353–390](MainWindow.xaml.cs#L353-L390) | [`ResumeGameAsync` → `WdResumeRemoteGame`, lines 133–144](RemoteIteration.cs#L133-L144) |
+| Terminate | [`Terminate_Click` / `ControlGameAsync`, lines 358–390](MainWindow.xaml.cs#L358-L390) | [`TerminateRemoteGameAsync` → `WdTerminateRemoteGame`, lines 82–94](RemoteIteration.cs#L82-L94) |
+
+For progress and diagnostic callbacks, start with
+[`OperationState.CreateCopyCallbacks`, lines 79–90](OperationState.cs#L79-L90)
+and [UI processing, lines 82–157](MainWindow.xaml.cs#L82-L157).
+
+## Implementation notes
+
+- File-attribute masks accept decimal `uint` values (`0` through `4294967295`).
+ Blank means zero. Malformed and out-of-range values are rejected before work
+ starts
+ ([input validation, lines 173–175](MainWindow.xaml.cs#L173-L175);
+ [mask parsing, lines 269–277](MainWindow.xaml.cs#L269-L277)).
+- Directory exclusions match **names, not paths**, and exclusions take precedence
+ over inclusions ([filter tooltips, lines 174–197](MainWindow.xaml#L174-L197)).
+ The managed search model also includes `IncludeDirectoryAttributes` and
+ `ExcludeDirectoryAttributes`, both zero by default and not exposed in the UI.
+ All seven search fields are passed to the API
+ ([model, lines 3–13](CopySearchOptions.cs#L3-L13);
+ [mapping, lines 36–42](RemoteIteration.cs#L36-L42)).
+- Only one action runs at a time. Device/path/filter inputs and conflicting
+ actions are disabled until it returns. Closing the window is blocked while
+ native work is active. If an operation appears stalled, choose **Cancel** when
+ available and wait for it to return. If it remains unresponsive, end
+ `RemoteIterationToolsSample.exe` in Task Manager as a last resort. Force-closing
+ does not undo completed transfers or confirm that the remote operation stopped
+ ([operation lifetime and closing guidance, lines 36–81](MainWindow.xaml.cs#L36-L81)).
+- Cancellation is a request, not confirmation that the build is complete.
+ The sample records the request separately from the copy's HRESULT and skips
+ remote-path validation when cancellation was requested, even if copy returns
+ success. Copy failures are still reported
+ ([copy result, lines 209–237](MainWindow.xaml.cs#L209-L237);
+ [cancellation, lines 280–307](MainWindow.xaml.cs#L280-L307)).
+- Native callbacks copy progress and diagnostic data into operation-owned queues
+ rather than updating WPF controls directly. The UI consumes bounded batches to
+ stay responsive and drains remaining output before showing the final result.
+ Callback-processing failures are reported separately; returning a failed
+ callback HRESULT does not abort native copy
+ ([bounded queue consumption, lines 38–77](OperationState.cs#L38-L77);
+ [callback ownership and capture, lines 79–176](OperationState.cs#L79-L176);
+ [UI processing, lines 82–157](MainWindow.xaml.cs#L82-L157)).
diff --git a/Samples/Tools/RemoteIterationToolsSample/RemoteIteration.cs b/Samples/Tools/RemoteIterationToolsSample/RemoteIteration.cs
index c5a4ff30..ec60bd85 100644
--- a/Samples/Tools/RemoteIterationToolsSample/RemoteIteration.cs
+++ b/Samples/Tools/RemoteIterationToolsSample/RemoteIteration.cs
@@ -25,8 +25,6 @@ public static Task CopyAsync(
{
return Task.Run(() =>
{
- Thread.CurrentThread.Name = "CopyAsync Worker";
-
HRESULT hr;
unsafe
{
@@ -40,11 +38,14 @@ public static Task CopyAsync(
nativeSearchOptions.excludeDirPattern = new PCSTR(excludeDirPatternBytes);
nativeSearchOptions.includeFileAttributes = searchOptions?.IncludeFileAttributes ?? 0;
nativeSearchOptions.excludeFileAttributes = searchOptions?.ExcludeFileAttributes ?? 0;
+ nativeSearchOptions.includeDirectoryAttributes = searchOptions?.IncludeDirectoryAttributes ?? 0;
+ nativeSearchOptions.excludeDirectoryAttributes = searchOptions?.ExcludeDirectoryAttributes ?? 0;
+ // CsWin32's string overload uses UTF-8 on .NET 8, but does not append NUL.
hr = PInvoke.WdRemoteCopy(
- remoteDevice,
- localSourcePath,
- remoteDestPath,
+ remoteDevice + '\0',
+ localSourcePath + '\0',
+ remoteDestPath + '\0',
copyOptions,
nativeSearchOptions,
statusCallbacks,
@@ -56,7 +57,8 @@ public static Task CopyAsync(
});
}
- /* The WdRegisterRemoteXboxGame API registers a game on a remote device. */
+ /* WdRegisterRemoteXboxGame resolves and validates a remote folder path.
+ * It does not perform platform game registration. */
public static Task RegisterRemoteXboxGameAsync(
string remoteDevice,
string remoteFolderPath,
@@ -64,9 +66,16 @@ public static Task RegisterRemoteXboxGameAsync(
{
return Task.Run(() =>
{
- Thread.CurrentThread.Name = "RegisterAsync Worker";
- HRESULT hr = PInvoke.WdRegisterRemoteXboxGame(remoteDevice, remoteFolderPath, commonRootAlias);
- return hr;
+ unsafe
+ {
+ fixed (byte* deviceBytes = Encoding.UTF8.GetBytes(remoteDevice + '\0'))
+ fixed (byte* folderBytes = Encoding.UTF8.GetBytes(remoteFolderPath + '\0'))
+ fixed (byte* aliasBytes = string.IsNullOrEmpty(commonRootAlias) ? null : Encoding.UTF8.GetBytes(commonRootAlias + '\0'))
+ {
+ return PInvoke.WdRegisterRemoteXboxGame(
+ new PCSTR(deviceBytes), new PCSTR(folderBytes), new PCSTR(aliasBytes));
+ }
+ }
});
}
@@ -78,9 +87,7 @@ public static Task TerminateRemoteGameAsync(string remoteDevice)
{
return Task.Run(() =>
{
- Thread.CurrentThread.Name = "TerminateAsync Worker";
-
- HRESULT hr = PInvoke.WdTerminateRemoteGame(remoteDevice);
+ HRESULT hr = PInvoke.WdTerminateRemoteGame(remoteDevice + '\0');
return hr;
});
@@ -100,23 +107,25 @@ public static Task TerminateRemoteGameAsync(string remoteDevice)
{
return Task.Run(() =>
{
- Thread.CurrentThread.Name = "LaunchAsync Worker";
-
unsafe
{
- uint processId;
- uint threadId;
- HRESULT hr;
- hr = PInvoke.WdLaunchRemoteGame(
- remoteDevice,
- remotePath,
- args,
- launchOptions,
- out processId,
- out threadId
- );
+ fixed (byte* deviceBytes = Encoding.UTF8.GetBytes(remoteDevice + '\0'))
+ fixed (byte* pathBytes = Encoding.UTF8.GetBytes(remotePath + '\0'))
+ fixed (byte* argsBytes = string.IsNullOrEmpty(args) ? null : Encoding.UTF8.GetBytes(args + '\0'))
+ {
+ WdLaunchOptions nativeOptions = launchOptions.GetValueOrDefault();
+ uint processId = 0;
+ uint threadId = 0;
+ HRESULT hr = PInvoke.WdLaunchRemoteGame(
+ new PCSTR(deviceBytes),
+ new PCSTR(pathBytes),
+ new PCSTR(argsBytes),
+ launchOptions.HasValue ? &nativeOptions : null,
+ &processId,
+ &threadId);
- return (hr, new ProcThreadId(processId, threadId));
+ return (hr, new ProcThreadId(processId, threadId));
+ }
}
});
}
@@ -128,8 +137,7 @@ string remoteDevice
{
return Task.Run(() =>
{
- Thread.CurrentThread.Name = "ResumeGameAsync Worker";
- HRESULT hr = PInvoke.WdResumeRemoteGame(remoteDevice);
+ HRESULT hr = PInvoke.WdResumeRemoteGame(remoteDevice + '\0');
return hr;
});
}
@@ -137,18 +145,14 @@ string remoteDevice
/* Cancels an ongoing WdRemoteCopy operation. This is a non-blocking call that signals the
* copy operation to stop as soon as possible. */
- public static Task CancelDeployAsync(WdCancellationHandleWrapper cancellationHandleWrapper)
+ public static HRESULT RequestCopyCancellation(WdCancellationHandleWrapper cancellationHandleWrapper)
{
- return Task.Run(() =>
+ if (cancellationHandleWrapper.Handle.IsInvalid)
{
- Thread.CurrentThread.Name = "CancelDeployAsync Worker";
- if(cancellationHandleWrapper.Handle.IsInvalid)
- {
- throw new ArgumentException("Invalid cancellation handle.");
- }
+ throw new ArgumentException("Invalid cancellation handle.");
+ }
- PInvoke.WdCancelRemoteCopy(cancellationHandleWrapper.Handle);
- });
+ return PInvoke.WdCancelRemoteCopy(cancellationHandleWrapper.Handle);
}
}
diff --git a/Samples/Tools/RemoteIterationToolsSample/RemoteIterationToolsSample.csproj b/Samples/Tools/RemoteIterationToolsSample/RemoteIterationToolsSample.csproj
index f2e8473a..5ff82d23 100644
--- a/Samples/Tools/RemoteIterationToolsSample/RemoteIterationToolsSample.csproj
+++ b/Samples/Tools/RemoteIterationToolsSample/RemoteIterationToolsSample.csproj
@@ -28,7 +28,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/gdk-samples-list.md b/gdk-samples-list.md
index 1edd0c71..6660918e 100644
--- a/gdk-samples-list.md
+++ b/gdk-samples-list.md
@@ -1,4 +1,4 @@
-# Microsoft Game Development Kit samples list
+# Microsoft Game Development Kit samples list
## Categories