diff --git a/GraphicsDeviceService.cs b/GraphicsDeviceService.cs new file mode 100644 index 0000000..a995a17 --- /dev/null +++ b/GraphicsDeviceService.cs @@ -0,0 +1,111 @@ +/// +/// Helper class responsible for creating and managing the GraphicsDevice. +/// All GraphicsDeviceControl instances share the same GraphicsDeviceService, +/// so even though there can be many controls, there will only ever be a +/// single underlying GraphicsDevice. This implements the standard +/// IGraphicsDeviceService interface, which provides notification events for +/// when the device is reset or disposed. +/// +public class GraphicsDeviceService : IGraphicsDeviceService +{ + // Singleton device service instance. + private static GraphicsDeviceService singletonInstance; + + // Keep track of how many controls are sharing the singletonInstance. + private static int referenceCount; + + /// + /// Gets the single instance of the service class for the application. + /// + public static GraphicsDeviceService Instance + { + get + { + if (singletonInstance == null) + singletonInstance = new GraphicsDeviceService(); + return singletonInstance; + } + } + + // Store the current device settings. + private PresentationParameters parameters; + + /// + /// Gets the current graphics device. + /// + public GraphicsDevice GraphicsDevice { get; private set; } + + // IGraphicsDeviceService events. + public event EventHandler DeviceCreated; + public event EventHandler DeviceDisposing; + public event EventHandler DeviceReset; + public event EventHandler DeviceResetting; + + /// + /// Constructor is private, because this is a singleton class: + /// client controls should use the public AddRef method instead. + /// + GraphicsDeviceService() { } + + /// + /// Creates the GraphicsDevice for the service. + /// + private void CreateDevice(IntPtr windowHandle) + { + parameters = new PresentationParameters(); + + // since we're using render targets anyway, the + // backbuffer size is somewhat irrelevant + parameters.BackBufferWidth = 480; + parameters.BackBufferHeight = 320; + parameters.BackBufferFormat = SurfaceFormat.Color; + parameters.DeviceWindowHandle = windowHandle; + parameters.DepthStencilFormat = DepthFormat.Depth24Stencil8; + parameters.IsFullScreen = false; + + GraphicsDevice = new GraphicsDevice( + GraphicsAdapter.DefaultAdapter, + GraphicsProfile.HiDef, + parameters); + + if (DeviceCreated != null) + DeviceCreated(this, EventArgs.Empty); + } + + /// + /// Gets a reference to the singleton instance. + /// + public static GraphicsDeviceService AddRef(IntPtr windowHandle) + { + // Increment the "how many controls sharing the device" + // reference count. + if (Interlocked.Increment(ref referenceCount) == 1) + { + // If this is the first control to start using the + // device, we must create the device. + Instance.CreateDevice(windowHandle); + } + + return singletonInstance; + } + + /// + /// Releases a reference to the singleton instance. + /// + public void Release() + { + // Decrement the "how many controls sharing the device" + // reference count. + if (Interlocked.Decrement(ref referenceCount) == 0) + { + // If this is the last control to finish using the + // device, we should dispose the singleton instance. + if (DeviceDisposing != null) + DeviceDisposing(this, EventArgs.Empty); + + GraphicsDevice.Dispose(); + + GraphicsDevice = null; + } + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b83bf6e --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010 Nick Gravelyn, Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5499391 --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +# Overview + +This is a repository preserving example code by [Nick Gravelyn](https://www.nickgravelyn.com/) did some blog post on XNA in the Microsoft Docs. As long as it's there, the original post can be found in the [Microsoft Docs Blog Archive](https://docs.microsoft.com/en-us/archive/blogs/nicgrave/rendering-with-xna-framework-4-0-inside-of-a-wpf-application). Originally there was no license included. By mail and in [this issue at MerjTek's WPF integration](https://github.com/MerjTek/MerjTek.WpfIntegration/issues/1#issuecomment-902622749) he kindly answered the code may be considered MIT or Microsoft Public License, so I put it here under MIT. + +This repository allows for using the code without legal concerns which may occur with the missing license in the blog post. + + +# Example Code + +There was more example code included in the blogpost, which is preserved here. + + +## Example 1 + +```csharp + // What importers or processors should we load? +private const string xnaVersion = + ", Version=4.0.0.0, PublicKeyToken=6d5c3888ef60e27d"; +private static readonly string[] pipelineAssemblies = +{ + "Microsoft.Xna.Framework.Content.Pipeline.AudioImporters" + xnaVersion, + "Microsoft.Xna.Framework.Content.Pipeline.EffectImporter" + xnaVersion, + "Microsoft.Xna.Framework.Content.Pipeline.FBXImporter" + xnaVersion, + "Microsoft.Xna.Framework.Content.Pipeline.TextureImporter" + xnaVersion, + "Microsoft.Xna.Framework.Content.Pipeline.VideoImporters" + xnaVersion, + "Microsoft.Xna.Framework.Content.Pipeline.XImporter" + xnaVersion, +}; +``` + + +## Example 2 + +```csharp +using Microsoft.Build.Evaluation; +``` + + +## Example 3 + +```csharp +// MSBuild objects used to dynamically build content. +private ProjectCollection projectCollection; +private Project msBuildProject; +``` + +## Example 4 + +```csharp +/// +/// Creates a temporary MSBuild content project in memory. +/// +void CreateBuildProject() +{ + string projectPath = + Path.Combine(buildDirectory, "content.contentproj"); + string outputPath = Path.Combine(buildDirectory, "bin"); + + // Create the project collection + projectCollection = new ProjectCollection(); + + // Hook up our custom error logger. + errorLogger = new ErrorLogger(); + projectCollection.RegisterLogger(errorLogger); + + // Create the build project. + msBuildProject = new Project(projectCollection); + msBuildProject.FullPath = projectPath; + + // set up the properties we care about + msBuildProject.SetProperty("XnaPlatform", "Windows"); + msBuildProject.SetProperty("XnaFrameworkVersion", "v4.0"); + msBuildProject.SetProperty("XnaProfile", "HiDef"); + msBuildProject.SetProperty("Configuration", "Release"); + msBuildProject.SetProperty("OutputPath", outputPath); + + // Register any custom importers or processors. + foreach (string pipelineAssembly in pipelineAssemblies) + { + msBuildProject.AddItem("Reference", pipelineAssembly); + } + + // Include the standard targets file that defines + // how to build XNA Framework content. + msBuildProject.Xml.AddImport( + "$(MSBuildExtensionsPath)\\Microsoft\\XNA Game Studio\\v4.0\\" + + "Microsoft.Xna.GameStudio.ContentPipeline.targets"); +} +``` + + +## Example 5 + +```csharp +/// +/// Adds a new content file to the MSBuild project. The importer and +/// processor are optional: if you leave the importer null, it will +/// be autodetected based on the file extension, and if you leave the +/// processor null, data will be passed through without any processing. +/// +public void Add( + string filename, string name, + string importer, string processor) +{ + // set up the metadata for this item + var metadata = new SortedList(); + metadata.Add("Link", Path.GetFileName(filename)); + metadata.Add("Name", name); + + if (!string.IsNullOrEmpty(importer)) + metadata.Add("Importer", importer); + + if (!string.IsNullOrEmpty(processor)) + metadata.Add("Processor", processor); + + // add the item + msBuildProject.AddItem("Compile", filename, metadata); +} +``` + + +## Example 6 + +```csharp +/// +/// Removes all content files from the MSBuild project. +/// +public void Clear() +{ + // select all compiled objects in the project and remove them + var compileObjects = from i in project.Items + where i.ItemType == "Compile" + select i; + project.RemoveItems(compileObjects); +} +``` \ No newline at end of file diff --git a/XnaControl.cs b/XnaControl.cs new file mode 100644 index 0000000..5336587 --- /dev/null +++ b/XnaControl.cs @@ -0,0 +1,105 @@ +public partial class XnaControl : UserControl +{ + private GraphicsDeviceService graphicsService; + private XnaImageSource imageSource; + + /// + /// Gets the GraphicsDevice behind the control. + /// + public GraphicsDevice GraphicsDevice + { + get { return graphicsService.GraphicsDevice; } + } + + /// + /// Invoked when the XnaControl needs to be redrawn. + /// + public Action DrawFunction; + + public XnaControl() + { + InitializeComponent(); + + // hook up an event to fire when the control has finished loading + Loaded += new RoutedEventHandler(XnaControl_Loaded); + } + + ~XnaControl() + { + imageSource.Dispose(); + + // release on finalizer to clean up the graphics device + if (graphicsService != null) + graphicsService.Release(); + } + + void XnaControl_Loaded(object sender, RoutedEventArgs e) + { + // if we're not in design mode, initialize the graphics device + if (DesignerProperties.GetIsInDesignMode(this) == false) + { + InitializeGraphicsDevice(); + } + } + + protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) + { + // if we're not in design mode, recreate the + // image source for the new size + if (DesignerProperties.GetIsInDesignMode(this) == false && + graphicsService != null) + { + // recreate the image source + imageSource.Dispose(); + imageSource = new XnaImageSource( + GraphicsDevice, (int)ActualWidth, (int)ActualHeight); + rootImage.Source = imageSource.WriteableBitmap; + } + + base.OnRenderSizeChanged(sizeInfo); + } + + private void InitializeGraphicsDevice() + { + if (graphicsService == null) + { + // add a reference to the graphics device + graphicsService = GraphicsDeviceService.AddRef( + (PresentationSource.FromVisual(this) as HwndSource).Handle); + + // create the image source + imageSource = new XnaImageSource( + GraphicsDevice, (int)ActualWidth, (int)ActualHeight); + rootImage.Source = imageSource.WriteableBitmap; + + // hook the rendering event + CompositionTarget.Rendering += CompositionTarget_Rendering; + } + } + + /// + /// Draws the control and allows subclasses to override + /// the default behavior of delegating the rendering. + /// + protected virtual void Render() + { + // invoke the draw delegate so someone will draw something pretty + if (DrawFunction != null) + DrawFunction(GraphicsDevice); + } + + void CompositionTarget_Rendering(object sender, EventArgs e) + { + // set the image source render target + GraphicsDevice.SetRenderTarget(imageSource.RenderTarget); + + // allow the control to draw + Render(); + + // unset the render target + GraphicsDevice.SetRenderTarget(null); + + // commit the changes to the image source + imageSource.Commit(); + } +} \ No newline at end of file diff --git a/XnaControl.xaml b/XnaControl.xaml new file mode 100644 index 0000000..23063fb --- /dev/null +++ b/XnaControl.xaml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/XnaImageSource.cs b/XnaImageSource.cs new file mode 100644 index 0000000..0774c2c --- /dev/null +++ b/XnaImageSource.cs @@ -0,0 +1,98 @@ +/// +/// A wrapper for a RenderTarget2D and WriteableBitmap +/// that handles taking the XNA rendering and moving it +/// into the WriteableBitmap which is consumed as the +/// ImageSource for an Image control. +/// +public class XnaImageSource : IDisposable +{ + // the render target we draw to + private RenderTarget2D renderTarget; + + // a WriteableBitmap we copy the pixels into for + // display into the Image + private WriteableBitmap writeableBitmap; + + // a buffer array that gets the data from the render target + private byte[] buffer; + + /// + /// Gets the render target used for this image source. + /// + public RenderTarget2D RenderTarget + { + get { return renderTarget; } + } + + /// + /// Gets the underlying WriteableBitmap that can + /// be bound as an ImageSource. + /// + public WriteableBitmap WriteableBitmap + { + get { return writeableBitmap; } + } + + /// + /// Creates a new XnaImageSource. + /// + /// The GraphicsDevice to use. + /// The width of the image source. + /// The height of the image source. + public XnaImageSource(GraphicsDevice graphics, int width, int height) + { + // create the render target and buffer to hold the data + renderTarget = new RenderTarget2D( + graphics, width, height, false, + SurfaceFormat.Color, + DepthFormat.Depth24Stencil8); + buffer = new byte[width * height * 4]; + writeableBitmap = new WriteableBitmap( + width, height, 96, 96, + PixelFormats.Bgra32, null); + } + + ~XnaImageSource() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + renderTarget.Dispose(); + + if (disposing) + GC.SuppressFinalize(this); + } + + /// + /// Commits the render target data into our underlying bitmap source. + /// + public void Commit() + { + // get the data from the render target + renderTarget.GetData(buffer); + + // because the only 32 bit pixel format for WPF is + // BGRA but XNA is all RGBA, we have to swap the R + // and B bytes for each pixel + for (int i = 0; i < buffer.Length - 2; i += 4) + { + byte r = buffer[i]; + buffer[i] = buffer[i + 2]; + buffer[i + 2] = r; + } + + // write our pixels into the bitmap source + writeableBitmap.Lock(); + Marshal.Copy(buffer, 0, writeableBitmap.BackBuffer, buffer.Length); + writeableBitmap.AddDirtyRect( + new Int32Rect(0, 0, renderTarget.Width, renderTarget.Height)); + writeableBitmap.Unlock(); + } +} \ No newline at end of file