├── .gitattributes ├── .gitignore ├── LICENSE ├── OpenTkControl.sln ├── OpenTkControl ├── OpenTK.dll.config ├── OpenTkControl.csproj ├── OpenTkControl.nuspec ├── OpenTkControlBase.xaml ├── OpenTkControlBase.xaml.cs ├── Properties │ └── AssemblyInfo.cs ├── ThreadOpenTkControl.cs ├── UiOpenTkControl.cs └── packages.config ├── OpenTkControlExample ├── App.config ├── App.xaml ├── App.xaml.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── OpenTK.dll.config ├── OpenTkControlExamples.csproj ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings └── packages.config └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jay Fleischer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /OpenTkControl.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26430.14 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTkControl", "OpenTkControl\OpenTkControl.csproj", "{06F9FD53-C287-4275-8B98-4B891EA3ABDD}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTkControlExamples", "OpenTkControlExample\OpenTkControlExamples.csproj", "{097A4564-77D3-497B-8B84-16E8222F164D}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {06F9FD53-C287-4275-8B98-4B891EA3ABDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {06F9FD53-C287-4275-8B98-4B891EA3ABDD}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {06F9FD53-C287-4275-8B98-4B891EA3ABDD}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {06F9FD53-C287-4275-8B98-4B891EA3ABDD}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {097A4564-77D3-497B-8B84-16E8222F164D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {097A4564-77D3-497B-8B84-16E8222F164D}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {097A4564-77D3-497B-8B84-16E8222F164D}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {097A4564-77D3-497B-8B84-16E8222F164D}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /OpenTkControl/OpenTK.dll.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /OpenTkControl/OpenTkControl.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {06F9FD53-C287-4275-8B98-4B891EA3ABDD} 8 | Library 9 | Properties 10 | OpenTkControl 11 | OpenTkControl 12 | v4.5 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | false 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | false 34 | 35 | 36 | 37 | ..\packages\OpenTK.2.0.0\lib\net20\OpenTK.dll 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | OpenTkControlBase.xaml 50 | 51 | 52 | 53 | 54 | 55 | Designer 56 | MSBuild:Compile 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /OpenTkControl/OpenTkControl.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JayFleischer.OpenTkControl 5 | 1.0.2 6 | $title$ 7 | Jay Fleischer 8 | jayhf614 9 | https://github.com/jayhf/OpenTkControl/blob/master/LICENSE 10 | https://github.com/jayhf/OpenTkControl/ 11 | false 12 | A faster way to use OpenTk in WPF without Forms dependencies 13 | Initial OpenTkControl Release 14 | Copyright 2020 15 | WPF OpenTK OpenGL Graphics C# .NET 16 | 17 | 18 | -------------------------------------------------------------------------------- /OpenTkControl/OpenTkControlBase.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /OpenTkControl/OpenTkControlBase.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Windows; 9 | using System.Windows.Interop; 10 | using System.Windows.Media; 11 | using System.Windows.Media.Imaging; 12 | using OpenTK; 13 | using OpenTK.Graphics; 14 | using OpenTK.Graphics.OpenGL; 15 | using OpenTK.Platform; 16 | using PixelFormat = OpenTK.Graphics.OpenGL.PixelFormat; 17 | 18 | namespace OpenTkControl 19 | { 20 | /// 21 | /// Interaction logic for OpenTkControlBase.xaml. OpenTkControlBase is a base class for OpenTK WPF controls 22 | /// 23 | public abstract partial class OpenTkControlBase 24 | { 25 | /// 26 | /// Initialize the OpenTk Toolkit 27 | /// 28 | static OpenTkControlBase() 29 | { 30 | Toolkit.Init(new ToolkitOptions 31 | { 32 | Backend = PlatformBackend.PreferNative 33 | }); 34 | } 35 | 36 | private volatile string _openGlVersion = (string) OpenGlVersionProperty.DefaultMetadata.DefaultValue; 37 | public static readonly DependencyProperty OpenGlVersionProperty = DependencyProperty.Register( 38 | nameof(OpenGlVersion), typeof(string), typeof(OpenTkControlBase), new PropertyMetadata("3.0")); 39 | 40 | /// 41 | /// Specifies the OpenGL Version to use. Should be formatted as X.X 42 | /// 43 | public string OpenGlVersion 44 | { 45 | get => (string)GetValue(OpenGlVersionProperty); 46 | set => SetValue(OpenGlVersionProperty, value); 47 | } 48 | 49 | private volatile float _frameRateLimit = (float) FrameRateLimitProperty.DefaultMetadata.DefaultValue; 50 | public static readonly DependencyProperty FrameRateLimitProperty = DependencyProperty.Register( 51 | nameof(FrameRateLimit), typeof(float), typeof(OpenTkControlBase), new PropertyMetadata(float.PositiveInfinity)); 52 | 53 | /// 54 | /// The maximum frame rate to render at. Anything over 1000 is treated as unlimited. 55 | /// 56 | public float FrameRateLimit 57 | { 58 | get => (float) GetValue(FrameRateLimitProperty); 59 | set => SetValue(FrameRateLimitProperty, value); 60 | } 61 | 62 | private volatile float _pixelScale = (float) PixelScaleProperty.DefaultMetadata.DefaultValue; 63 | public static readonly DependencyProperty PixelScaleProperty = DependencyProperty.Register( 64 | nameof(PixelScale), typeof(float), typeof(OpenTkControlBase), new PropertyMetadata(1f)); 65 | 66 | /// 67 | /// Scales the pixel size to change the number of pixels rendered. Mainly useful for improving performance. 68 | /// A scale greater than 1 means that pixels will be bigger and the resolution will decrease. 69 | /// 70 | public float PixelScale 71 | { 72 | get => (float) GetValue(PixelScaleProperty); 73 | set => SetValue(PixelScaleProperty, value); 74 | } 75 | 76 | private volatile uint _maxPixels = (uint) MaxPixelsProperty.DefaultMetadata.DefaultValue; 77 | public static readonly DependencyProperty MaxPixelsProperty = DependencyProperty.Register( 78 | nameof(MaxPixels), typeof(uint), typeof(OpenTkControlBase), new PropertyMetadata(uint.MaxValue)); 79 | 80 | /// 81 | /// Sets the maximum number of pixels to draw. If the control size is larger than this, the scale will 82 | /// be changed as necessary to stay under this limit. 83 | /// 84 | public uint MaxPixels 85 | { 86 | get => (uint) GetValue(MaxPixelsProperty); 87 | set => SetValue(MaxPixelsProperty, value); 88 | } 89 | 90 | protected volatile bool _continuous = (bool) ContinuousProperty.DefaultMetadata.DefaultValue; 91 | public static readonly DependencyProperty ContinuousProperty = DependencyProperty.Register( 92 | nameof(Continuous), typeof(bool), typeof(UiOpenTkControl), new PropertyMetadata(true)); 93 | 94 | /// 95 | /// Determines whether this control is in continuous mode. If set to false, RequestRepaint must be called 96 | /// to get the control to render. Otherwise, it will automatically Render as fast as it possible up 97 | /// to the 98 | /// 99 | public bool Continuous 100 | { 101 | get => (bool)GetValue(ContinuousProperty); 102 | set => SetValue(ContinuousProperty, value); 103 | } 104 | 105 | /// 106 | /// The event arguments that are sent when a event occurs 107 | /// 108 | public class GlRenderEventArgs : EventArgs 109 | { 110 | /// 111 | /// True if the width or height has change since the previous render event. Always false for screenshots 112 | /// 113 | public bool Resized { get; } 114 | 115 | /// 116 | /// True if this render will be saved to a screenshot 117 | /// 118 | public bool Screenshot { get; } 119 | 120 | /// 121 | /// If set, the OpenGL context has been recreated and any existing OpenGL objects will be invalid. 122 | /// 123 | public bool NewContext { get; } 124 | 125 | /// 126 | /// The width of the drawing area in pixels 127 | /// 128 | public int Width { get; } 129 | 130 | /// 131 | /// The height of the drawing area in pixels 132 | /// 133 | public int Height { get; } 134 | 135 | /// 136 | /// Can be set to only redraw a certain part of the canvas. Not used for screenshots 137 | /// 138 | public Int32Rect RepaintRect { get; set; } 139 | 140 | /// 141 | /// Creates a 142 | /// 143 | /// 144 | /// 145 | /// 146 | /// 147 | public GlRenderEventArgs(int width, int height, bool resized, bool screenshot, bool newContext) 148 | { 149 | Width = width; 150 | Height = height; 151 | RepaintRect = new Int32Rect(0, 0, Width, Height); 152 | Resized = resized; 153 | Screenshot = screenshot; 154 | NewContext = newContext; 155 | } 156 | } 157 | 158 | /// 159 | /// Called whenever another render should occur 160 | /// 161 | public event EventHandler GlRender; 162 | 163 | /// 164 | /// Called whenever an exception occurs during initialization, rendering or deinitialization 165 | /// 166 | public event EventHandler ExceptionOccurred; 167 | 168 | /// 169 | /// An OpenTK graphics context 170 | /// 171 | private IGraphicsContext _context; 172 | 173 | /// 174 | /// The source of the internal Image 175 | /// 176 | private volatile WriteableBitmap _bitmap; 177 | 178 | /// 179 | /// The width of in pixels/> 180 | /// 181 | private int _bitmapWidth; 182 | 183 | /// 184 | /// The height of in pixels/> 185 | /// 186 | private int _bitmapHeight; 187 | 188 | /// 189 | /// A pointer to 's back buffer 190 | /// 191 | private IntPtr _backBuffer = IntPtr.Zero; 192 | 193 | /// 194 | /// Information about the current window 195 | /// 196 | private IWindowInfo _windowInfo; 197 | 198 | /// 199 | /// A Task that represents updating the screen with the current WriteableBitmap back buffer 200 | /// 201 | private Task _previousUpdateImageTask; 202 | 203 | /// 204 | /// Stores any pending screenshots that need to be captured 205 | /// 206 | private readonly ConcurrentQueue, int, int>> _screenshotQueue = 207 | new ConcurrentQueue, int, int>>(); 208 | 209 | /// 210 | /// True if a new OpenGL context has been created since the last render call 211 | /// 212 | private bool _newContext; 213 | 214 | /// 215 | /// Keeps track of any pending repaint requests that need to be notified upon completion 216 | /// 217 | private readonly ConcurrentQueue> _repaintRequestQueue = 218 | new ConcurrentQueue>(); 219 | 220 | /// 221 | /// Set whenever a repaint is requested 222 | /// 223 | protected readonly ManualResetEvent ManualRepaintEvent = new ManualResetEvent(false); 224 | 225 | /// 226 | /// The last time a frame was rendered 227 | /// 228 | private DateTime _lastFrameTime = DateTime.MinValue; 229 | 230 | /// 231 | /// The OpenGL framebuffer 232 | /// 233 | private int _frameBuffer; 234 | 235 | /// 236 | /// The OpenGL render buffer. It stores data in Rgba8 format with color attachment 0 237 | /// 238 | private int _renderBuffer; 239 | 240 | /// 241 | /// The OpenGL depth buffer 242 | /// 243 | private int _depthBuffer; 244 | 245 | /// 246 | /// True if OnLoaded has already been called 247 | /// 248 | private bool _alreadyLoaded; 249 | 250 | /// 251 | /// Creates the /> 252 | /// 253 | protected OpenTkControlBase() 254 | { 255 | InitializeComponent(); 256 | 257 | // Update all of the volatile copies the variables 258 | // This is a workaround for the WPF threading restrictions on DependencyProperties 259 | // that allows other threads to read the values. 260 | DependencyPropertyDescriptor.FromProperty(OpenGlVersionProperty, typeof(OpenTkControlBase)) 261 | .AddValueChanged(this, (sender, args) => _openGlVersion = OpenGlVersion); 262 | DependencyPropertyDescriptor.FromProperty(FrameRateLimitProperty, typeof(OpenTkControlBase)) 263 | .AddValueChanged(this, (sender, args) => _frameRateLimit = FrameRateLimit); 264 | DependencyPropertyDescriptor.FromProperty(PixelScaleProperty, typeof(OpenTkControlBase)) 265 | .AddValueChanged(this, (sender, args) => _pixelScale = PixelScale); 266 | DependencyPropertyDescriptor.FromProperty(MaxPixelsProperty, typeof(OpenTkControlBase)) 267 | .AddValueChanged(this, (sender, args) => _maxPixels = MaxPixels); 268 | DependencyPropertyDescriptor.FromProperty(ContinuousProperty, typeof(OpenTkControlBase)) 269 | .AddValueChanged(this, (sender, args) => 270 | { 271 | _continuous = Continuous; 272 | // Handle the case where we switched to continuous, but the thread is still waiting for a request 273 | if (_continuous) 274 | RequestRepaint(); 275 | }); 276 | 277 | Loaded += (sender, args) => 278 | { 279 | if (_alreadyLoaded) 280 | return; 281 | 282 | _alreadyLoaded = true; 283 | OnLoaded(sender, args); 284 | }; 285 | Unloaded += (sender, args) => 286 | { 287 | if (!_alreadyLoaded) 288 | return; 289 | 290 | _alreadyLoaded = false; 291 | OnUnloaded(sender, args); 292 | }; 293 | } 294 | 295 | /// 296 | /// Requests that the next frame be drawn, which is the only way to get the control to render 297 | /// when not in mode. 298 | /// In continuous mode this still returns a task that will complete when a frame has rendered. 299 | /// 300 | /// A task that will complete when the next render completes 301 | public Task RequestRepaint() 302 | { 303 | TaskCompletionSource tcs = new TaskCompletionSource(); 304 | _repaintRequestQueue.Enqueue(tcs); 305 | ManualRepaintEvent.Set(); 306 | return tcs.Task; 307 | } 308 | 309 | /// 310 | /// Check if it is run in designer mode. 311 | /// 312 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 313 | protected bool IsDesignMode() => DesignerProperties.GetIsInDesignMode(this); 314 | 315 | /// 316 | /// Renders a screenshot of the frame with the specified dimensions. It will be in bgra format with 317 | /// [0,0] at the bottom left corner. Note that this is not meant for taking screenshots of what is 318 | /// displayed on the screen each frame. To do that, just use GL.ReadPixels. 319 | /// 320 | /// The width of the screenshot in pixels or 0 to use the current width 321 | /// The height of the screenshot in pixels or 0 to use the current height 322 | /// A task that completes when the screenshot is ready 323 | public Task Screenshot(int width = 0, int height = 0) 324 | { 325 | TaskCompletionSource tcs = new TaskCompletionSource(); 326 | _screenshotQueue.Enqueue(new Tuple, int, int>(tcs, width, height)); 327 | return tcs.Task; 328 | } 329 | 330 | /// 331 | /// Executes an action on the UI thread 332 | /// 333 | /// The action to run 334 | /// a Task that will complete when the action finishes running or null if already complete 335 | public abstract Task RunOnUiThread(Action action); 336 | 337 | /// 338 | /// Called when this control is loaded 339 | /// 340 | /// The object that sent the event 341 | /// Information about the event 342 | protected virtual void OnLoaded(object sender, RoutedEventArgs args) 343 | { 344 | #if DEBUG 345 | if (IsDesignMode()) 346 | return; 347 | #endif 348 | 349 | _windowInfo = Utilities.CreateWindowsWindowInfo( 350 | new WindowInteropHelper(Window.GetWindow(this)).Handle); 351 | } 352 | 353 | /// 354 | /// Called when this control is unloaded 355 | /// 356 | /// The object that sent the event 357 | /// Information about the event 358 | protected virtual async void OnUnloaded(object sender, RoutedEventArgs args) 359 | { 360 | try 361 | { 362 | Task previousUpdateImageTask = _previousUpdateImageTask; 363 | if(previousUpdateImageTask != null) 364 | { 365 | await previousUpdateImageTask; 366 | } 367 | } 368 | catch (TaskCanceledException) { } 369 | catch (Exception e) 370 | { 371 | ExceptionOccurred?.Invoke(this, new UnhandledExceptionEventArgs(e, false)); 372 | } 373 | 374 | _previousUpdateImageTask = null; 375 | 376 | _windowInfo = null; 377 | _backBuffer = IntPtr.Zero; 378 | 379 | _bitmap = null; 380 | 381 | _lastFrameTime = DateTime.MinValue; 382 | } 383 | 384 | /// 385 | /// Initializes a variety of OpenGL resources 386 | /// 387 | protected void InitOpenGl() 388 | { 389 | try 390 | { 391 | Version version = Version.Parse(_openGlVersion); 392 | GraphicsMode mode = new GraphicsMode(DisplayDevice.Default.BitsPerPixel, 16, 0, 4, 0, 2, false); 393 | _context = new GraphicsContext(mode, _windowInfo, version.Major, version.Minor, GraphicsContextFlags.Default); 394 | _newContext = true; 395 | _context.LoadAll(); 396 | _context.MakeCurrent(_windowInfo); 397 | } 398 | catch (Exception e) 399 | { 400 | ExceptionOccurred?.Invoke(this, new UnhandledExceptionEventArgs(e, false)); 401 | } 402 | } 403 | 404 | /// 405 | /// Releases any OpenGL resources in use. Must be called from the Render Thread. 406 | /// 407 | protected void DeInitOpenGl() 408 | { 409 | try 410 | { 411 | DeInitOpenGlBuffers(); 412 | 413 | _context.Dispose(); 414 | _context = null; 415 | 416 | while (_screenshotQueue.TryDequeue(out var tuple)) 417 | { 418 | tuple.Item1.SetCanceled(); 419 | } 420 | } 421 | catch (Exception e) 422 | { 423 | ExceptionOccurred?.Invoke(this, new UnhandledExceptionEventArgs(e, false)); 424 | } 425 | } 426 | 427 | /// 428 | /// Handles generating screenshots and updating the display image 429 | /// 430 | protected TimeSpan Render() 431 | { 432 | try 433 | { 434 | RenderScreenshots(out int currentBufferWidth, out int currentBufferHeight); 435 | 436 | CalculateBufferSize(out int width, out int height); 437 | 438 | if ((_continuous && !IsVisible) || width == 0 || height == 0) 439 | return TimeSpan.FromMilliseconds(20); 440 | 441 | 442 | if (_continuous && _frameRateLimit > 0 && _frameRateLimit < 1000) 443 | { 444 | DateTime now = DateTime.Now; 445 | TimeSpan delayTime = TimeSpan.FromSeconds(1 / _frameRateLimit) - (now - _lastFrameTime); 446 | if (delayTime.CompareTo(TimeSpan.Zero) > 0) 447 | return delayTime; 448 | 449 | _lastFrameTime = now; 450 | } 451 | else 452 | { 453 | _lastFrameTime = DateTime.MinValue; 454 | } 455 | 456 | if (!ReferenceEquals(GraphicsContext.CurrentContext, _context)) 457 | _context.MakeCurrent(_windowInfo); 458 | 459 | bool resized = false; 460 | Task resizeBitmapTask = null; 461 | //Need Abs(...) > 1 to handle an edge case where the resizing the bitmap causes the height to increase in an infinite loop 462 | if (_bitmap == null || Math.Abs(_bitmapWidth - width) > 1 || Math.Abs(_bitmapHeight - height) > 1) 463 | { 464 | resized = true; 465 | _bitmapWidth = width; 466 | _bitmapHeight = height; 467 | resizeBitmapTask = RunOnUiThread(() => 468 | { 469 | _bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Pbgra32, null); 470 | _backBuffer = _bitmap.BackBuffer; 471 | }); 472 | } 473 | 474 | if (currentBufferWidth != _bitmapWidth || currentBufferHeight != _bitmapHeight) 475 | { 476 | CreateOpenGlBuffers(_bitmapWidth, _bitmapHeight); 477 | } 478 | 479 | List> repaintRequests = null; 480 | while (_repaintRequestQueue.TryDequeue(out var tcs)) 481 | { 482 | if (repaintRequests == null) 483 | { 484 | repaintRequests = new List>(); 485 | } 486 | repaintRequests.Add(tcs); 487 | } 488 | 489 | GlRenderEventArgs args = new GlRenderEventArgs(_bitmapWidth, _bitmapHeight, resized, false, CheckNewContext()); 490 | try 491 | { 492 | OnGlRender(args); 493 | } 494 | finally 495 | { 496 | if (repaintRequests != null) 497 | { 498 | foreach (var taskCompletionSource in repaintRequests) 499 | { 500 | taskCompletionSource.SetResult(null); 501 | } 502 | } 503 | } 504 | 505 | Int32Rect dirtyArea = args.RepaintRect; 506 | 507 | if (dirtyArea.Width <= 0 || dirtyArea.Height <= 0) 508 | return TimeSpan.Zero; 509 | 510 | try 511 | { 512 | resizeBitmapTask?.Wait(); 513 | try 514 | { 515 | _previousUpdateImageTask?.Wait(); 516 | } 517 | finally 518 | { 519 | _previousUpdateImageTask = null; 520 | } 521 | } 522 | catch (TaskCanceledException) 523 | { 524 | return TimeSpan.Zero; 525 | } 526 | 527 | if(_backBuffer != IntPtr.Zero) 528 | GL.ReadPixels(0, 0, _bitmapWidth, _bitmapHeight, PixelFormat.Bgra, PixelType.UnsignedByte, _backBuffer); 529 | 530 | _previousUpdateImageTask = RunOnUiThread(() => UpdateImage(dirtyArea)); 531 | } 532 | catch (Exception e) 533 | { 534 | ExceptionOccurred?.Invoke(this, new UnhandledExceptionEventArgs(e, false)); 535 | } 536 | 537 | return TimeSpan.Zero; 538 | } 539 | 540 | /// 541 | /// Renders all of the requested screenshots 542 | /// 543 | /// The new OpenGl buffer width 544 | /// The new OpenGl buffer height 545 | private void RenderScreenshots(out int currentWidth, out int currentHeight) 546 | { 547 | currentWidth = _bitmapWidth; 548 | currentHeight = _bitmapHeight; 549 | while (_screenshotQueue.TryDequeue(out var screenshotInfo)) 550 | { 551 | TaskCompletionSource tcs = screenshotInfo.Item1; 552 | int screenshotWidth = screenshotInfo.Item2; 553 | int screenshotHeight = screenshotInfo.Item3; 554 | if (screenshotWidth <= 0) 555 | screenshotWidth = _bitmapWidth; 556 | if (screenshotHeight <= 0) 557 | screenshotHeight = _bitmapHeight; 558 | 559 | try 560 | { 561 | uint[,] screenshot = new uint[screenshotHeight, screenshotWidth]; 562 | 563 | //Handle the case where the window has 0 width or height 564 | if (screenshotHeight == 0 || screenshotWidth == 0) 565 | { 566 | tcs.SetResult(screenshot); 567 | continue; 568 | } 569 | 570 | if (screenshotWidth != currentWidth || screenshotHeight != currentHeight) 571 | { 572 | currentWidth = screenshotWidth; 573 | currentHeight = screenshotHeight; 574 | CreateOpenGlBuffers(screenshotWidth, screenshotHeight); 575 | } 576 | OnGlRender(new GlRenderEventArgs(screenshotWidth, screenshotHeight, false, true, CheckNewContext())); 577 | GL.ReadPixels(0, 0, screenshotWidth, screenshotHeight, 578 | PixelFormat.Bgra, PixelType.UnsignedByte, 579 | screenshot); 580 | tcs.SetResult(screenshot); 581 | } 582 | catch (Exception e) 583 | { 584 | tcs.SetException(e); 585 | } 586 | } 587 | } 588 | 589 | /// 590 | /// Updates 591 | /// 592 | /// True if there is a new context 593 | private bool CheckNewContext() 594 | { 595 | if (_newContext) 596 | { 597 | _newContext = false; 598 | return true; 599 | } 600 | 601 | return false; 602 | } 603 | 604 | /// 605 | /// Determines the current buffer size based on the ActualWidth and ActualHeight of the control 606 | /// in addition to the and settings 607 | /// 608 | /// The new buffer width 609 | /// The new buffer height 610 | private void CalculateBufferSize(out int width, out int height) 611 | { 612 | width = (int) (ActualWidth / _pixelScale); 613 | height = (int) (ActualHeight / _pixelScale); 614 | 615 | if (width <= 0 || height <= 0) 616 | return; 617 | 618 | if (width * height > _maxPixels) 619 | { 620 | float scale = (float)Math.Sqrt((float)_maxPixels / width / height); 621 | width = (int)(width * scale); 622 | height = (int)(height * scale); 623 | } 624 | } 625 | 626 | /// 627 | /// Updates what is currently being drawn on the screen from the back buffer. 628 | /// Must be called from the UI thread 629 | /// 630 | /// The dirty dirtyArea of the screen that should be updated 631 | private void UpdateImage(Int32Rect dirtyArea) 632 | { 633 | WriteableBitmap bitmap = _bitmap; 634 | if(bitmap == null) 635 | { 636 | Image.Source = null; 637 | return; 638 | } 639 | 640 | bitmap.Lock(); 641 | bitmap.AddDirtyRect(dirtyArea); 642 | bitmap.Unlock(); 643 | 644 | Image.Source = bitmap; 645 | } 646 | 647 | /// 648 | /// Creates new OpenGl buffers of the specified size, including , , 649 | /// and . This method is virtual so the behavior can be overriden, but the default behavior 650 | /// should work for most purposes. 651 | /// 652 | /// The width of the new buffers 653 | /// The height of the new buffers 654 | protected virtual void CreateOpenGlBuffers(int width, int height) 655 | { 656 | DeInitOpenGlBuffers(); 657 | 658 | _frameBuffer = GL.GenFramebuffer(); 659 | GL.BindFramebuffer(FramebufferTarget.Framebuffer, _frameBuffer); 660 | 661 | _depthBuffer = GL.GenRenderbuffer(); 662 | GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _depthBuffer); 663 | GL.RenderbufferStorage(RenderbufferTarget.Renderbuffer, RenderbufferStorage.DepthComponent24, width, height); 664 | GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthAttachment, 665 | RenderbufferTarget.Renderbuffer, _depthBuffer); 666 | 667 | _renderBuffer = GL.GenRenderbuffer(); 668 | GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _renderBuffer); 669 | GL.RenderbufferStorage(RenderbufferTarget.Renderbuffer, RenderbufferStorage.Rgba8, width, height); 670 | GL.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, 671 | RenderbufferTarget.Renderbuffer, _renderBuffer); 672 | 673 | FramebufferErrorCode error = GL.CheckFramebufferStatus(FramebufferTarget.Framebuffer); 674 | if (error != FramebufferErrorCode.FramebufferComplete) 675 | { 676 | throw new GraphicsErrorException("Error creating frame buffer: " + error); 677 | } 678 | } 679 | 680 | /// 681 | /// Releases all of the OpenGL buffers currently in use 682 | /// 683 | protected virtual void DeInitOpenGlBuffers() 684 | { 685 | if (_frameBuffer != 0) 686 | { 687 | GL.DeleteFramebuffer(_frameBuffer); 688 | _frameBuffer = 0; 689 | } 690 | if (_depthBuffer != 0) 691 | { 692 | GL.DeleteRenderbuffer(_depthBuffer); 693 | _depthBuffer = 0; 694 | } 695 | if (_renderBuffer != 0) 696 | { 697 | GL.DeleteRenderbuffer(_renderBuffer); 698 | _renderBuffer = 0; 699 | } 700 | } 701 | 702 | /// 703 | /// A helper to actually invoke 704 | /// 705 | /// The render arguments 706 | private void OnGlRender(GlRenderEventArgs args) 707 | { 708 | GlRender?.Invoke(this, args); 709 | 710 | ErrorCode error = GL.GetError(); 711 | if (error != ErrorCode.NoError) 712 | throw new GraphicsException(error.ToString()); 713 | } 714 | } 715 | } 716 | -------------------------------------------------------------------------------- /OpenTkControl/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("OpenTkControl")] 8 | [assembly: AssemblyDescription("A faster way to use OpenTk in WPF without Forms dependencies")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("Jay Fleischer")] 11 | [assembly: AssemblyProduct("OpenTkControl")] 12 | [assembly: AssemblyCopyright("Copyright © 2017")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("06f9fd53-c287-4275-8b98-4b891ea3abdd")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.1")] 35 | [assembly: AssemblyFileVersion("1.0.1")] 36 | -------------------------------------------------------------------------------- /OpenTkControl/ThreadOpenTkControl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using System.Windows; 5 | 6 | namespace OpenTkControl 7 | { 8 | /// 9 | /// A WPF control that performs all OpenGL rendering on a thread separate from the UI thread to improve performance 10 | /// 11 | public class ThreadOpenTkControl : OpenTkControlBase 12 | { 13 | public static readonly DependencyProperty ThreadNameProperty = DependencyProperty.Register( 14 | nameof(ThreadName), typeof(string), typeof(ThreadOpenTkControl), new PropertyMetadata("OpenTk Render Thread")); 15 | 16 | /// 17 | /// The name of the background thread that does the OpenGL rendering 18 | /// 19 | public string ThreadName 20 | { 21 | get => (string) GetValue(ThreadNameProperty); 22 | set => SetValue(ThreadNameProperty, value); 23 | } 24 | 25 | /// 26 | /// This event is set to notify the thread to wake up when the control becomes visible 27 | /// 28 | private readonly ManualResetEvent _becameVisibleEvent = new ManualResetEvent(false); 29 | 30 | /// 31 | /// The Thread object for the rendering thread 32 | /// 33 | private Thread _renderThread; 34 | 35 | /// 36 | /// The CTS used to stop the thread when this control is unloaded 37 | /// 38 | private CancellationTokenSource _endThreadCts; 39 | 40 | 41 | public ThreadOpenTkControl() 42 | { 43 | IsVisibleChanged += OnIsVisibleChanged; 44 | } 45 | 46 | public override Task RunOnUiThread(Action action) 47 | { 48 | return Dispatcher.InvokeAsync(action).Task; 49 | } 50 | 51 | protected override void OnLoaded(object sender, RoutedEventArgs args) 52 | { 53 | base.OnLoaded(sender, args); 54 | 55 | _endThreadCts = new CancellationTokenSource(); 56 | 57 | _renderThread = new Thread(RenderThread) 58 | { 59 | IsBackground = true, 60 | Priority = ThreadPriority.Highest, 61 | Name = ThreadName 62 | }; 63 | _renderThread.Start(_endThreadCts.Token); 64 | } 65 | 66 | protected override void OnUnloaded(object sender, RoutedEventArgs args) 67 | { 68 | base.OnUnloaded(sender, args); 69 | 70 | _endThreadCts.Cancel(); 71 | _renderThread.Join(); 72 | } 73 | 74 | /// 75 | /// Wakes up the thread when the control becomes visible 76 | /// 77 | /// The object that sent the event 78 | /// The event arguments about this event 79 | private void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs args) 80 | { 81 | bool visible = (bool)args.NewValue; 82 | 83 | if(visible) 84 | _becameVisibleEvent.Set(); 85 | } 86 | 87 | /// 88 | /// The function that the thread runs to render the control 89 | /// 90 | /// 91 | private void RenderThread(object boxedToken) 92 | { 93 | #if DEBUG 94 | // Don't render in design mode to prevent errors from calling OpenGL API methods. 95 | if (Dispatcher.Invoke(() => IsDesignMode())) 96 | return; 97 | #endif 98 | 99 | CancellationToken token = (CancellationToken) boxedToken; 100 | 101 | InitOpenGl(); 102 | 103 | WaitHandle[] notContinousHandles = {token.WaitHandle, ManualRepaintEvent}; 104 | WaitHandle[] notVisibleHandles = {token.WaitHandle, _becameVisibleEvent}; 105 | while (!token.IsCancellationRequested) 106 | { 107 | if (!_continuous) 108 | { 109 | WaitHandle.WaitAny(notContinousHandles); 110 | } 111 | else if (!IsVisible) 112 | { 113 | WaitHandle.WaitAny(notVisibleHandles); 114 | _becameVisibleEvent.Reset(); 115 | 116 | if(!_continuous) 117 | continue; 118 | } 119 | 120 | if (token.IsCancellationRequested) 121 | break; 122 | 123 | ManualRepaintEvent.Reset(); 124 | 125 | TimeSpan sleepTime = Render(); 126 | if(sleepTime.CompareTo(TimeSpan.Zero) > 0) 127 | Thread.Sleep(sleepTime); 128 | } 129 | 130 | DeInitOpenGl(); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /OpenTkControl/UiOpenTkControl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Threading.Tasks; 4 | using System.Windows; 5 | using System.Windows.Media; 6 | 7 | namespace OpenTkControl 8 | { 9 | /// 10 | /// A WPF control that performs OpenGL rendering on the UI thread 11 | /// 12 | public class UiOpenTkControl : OpenTkControlBase 13 | { 14 | private DateTime _nextRenderTime = DateTime.MinValue; 15 | 16 | /// 17 | /// Creates a UiOpenTkControl 18 | /// 19 | public UiOpenTkControl() 20 | { 21 | IsVisibleChanged += OnIsVisibleChanged; 22 | } 23 | 24 | public override Task RunOnUiThread(Action action) 25 | { 26 | action(); 27 | return null; 28 | } 29 | 30 | protected override void OnLoaded(object sender, RoutedEventArgs args) 31 | { 32 | base.OnLoaded(sender, args); 33 | 34 | InitOpenGl(); 35 | } 36 | 37 | protected override void OnUnloaded(object sender, RoutedEventArgs routedEventArgs) 38 | { 39 | DeInitOpenGl(); 40 | 41 | base.OnUnloaded(sender, routedEventArgs); 42 | } 43 | 44 | /// 45 | /// Performs the OpenGl rendering when this control is visible 46 | /// 47 | /// The object that sent the event 48 | /// The event arguments about this event 49 | private void CompositionTargetOnRendering(object sender, EventArgs args) 50 | { 51 | #if DEBUG 52 | //We needn't call render() for avoiding crash by calling OpenGL API methods. 53 | if (IsDesignMode()) 54 | return; 55 | #endif 56 | 57 | DateTime now = DateTime.Now; 58 | if ((_continuous && now > _nextRenderTime) || ManualRepaintEvent.WaitOne(0)) 59 | { 60 | ManualRepaintEvent.Reset(); 61 | _nextRenderTime = now + Render(); 62 | } 63 | } 64 | 65 | /// 66 | /// Handles subscribing and unsubcribing when this component's visibility has changed 67 | /// 68 | /// The object that sent the event 69 | /// The event arguments about this event 70 | private void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs args) 71 | { 72 | bool visible = (bool)args.NewValue; 73 | 74 | if (visible) 75 | CompositionTarget.Rendering += CompositionTargetOnRendering; 76 | else 77 | CompositionTarget.Rendering -= CompositionTargetOnRendering; 78 | 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /OpenTkControl/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /OpenTkControlExample/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /OpenTkControlExample/App.xaml: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /OpenTkControlExample/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace OpenTkControlExample 4 | { 5 | /// 6 | /// Interaction logic for App.xaml 7 | /// 8 | public partial class App : Application 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /OpenTkControlExample/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /OpenTkControlExample/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Windows.Threading; 5 | using OpenTkControl; 6 | using OpenTK; 7 | using OpenTK.Graphics.OpenGL; 8 | using System.Drawing; 9 | 10 | namespace OpenTkControlExample 11 | { 12 | public partial class MainWindow 13 | { 14 | private readonly DispatcherTimer _fpsTimer = new DispatcherTimer(); 15 | private readonly Stopwatch _sw = Stopwatch.StartNew(); 16 | private int _fps; 17 | private float _angle; 18 | private int _displayList; 19 | 20 | public MainWindow() 21 | { 22 | _fpsTimer.Interval = TimeSpan.FromSeconds(1); 23 | _fpsTimer.Tick += (sender, args) => 24 | { 25 | double seconds = _sw.Elapsed.TotalSeconds; 26 | _sw.Restart(); 27 | Title = (Interlocked.Exchange(ref _fps, 0)/seconds).ToString("F1") + " FPS"; 28 | }; 29 | _fpsTimer.Start(); 30 | } 31 | 32 | private void OpenTkControl_OnGlRender(object sender, OpenTkControlBase.GlRenderEventArgs e) 33 | { 34 | GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); 35 | 36 | GL.MatrixMode(MatrixMode.Projection); 37 | GL.LoadIdentity(); 38 | float halfWidth = e.Width / 2f; 39 | float halfHeight = e.Height / 2f; 40 | GL.Ortho(-halfWidth, halfWidth, -halfHeight, halfHeight, 1000, -1000); 41 | GL.Viewport(0, 0, e.Width, e.Height); 42 | 43 | //Using the same example as used by https://github.com/freakinpenguin/OpenTK-WPF 44 | //to make it easier to compare different approaches 45 | if (_displayList <= 0 || e.NewContext) 46 | { 47 | _displayList = GL.GenLists(1); 48 | GL.NewList(_displayList, ListMode.Compile); 49 | 50 | GL.Color3(Color.Red); 51 | 52 | GL.Begin(PrimitiveType.Points); 53 | 54 | Random rnd = new Random(); 55 | for (int i = 0; i < 1000000; i++) 56 | { 57 | float factor = 0.2f; 58 | Vector3 position = new Vector3( 59 | rnd.Next(-1000, 1000) * factor, 60 | rnd.Next(-1000, 1000) * factor, 61 | rnd.Next(-1000, 1000) * factor); 62 | GL.Vertex3(position); 63 | 64 | position.Normalize(); 65 | GL.Normal3(position); 66 | } 67 | 68 | GL.End(); 69 | 70 | GL.EndList(); 71 | } 72 | 73 | GL.Enable(EnableCap.Lighting); 74 | GL.Enable(EnableCap.Light0); 75 | GL.Enable(EnableCap.Blend); 76 | GL.BlendFunc(BlendingFactorSrc.SrcAlpha, BlendingFactorDest.OneMinusSrcAlpha); 77 | GL.Enable(EnableCap.DepthTest); 78 | 79 | GL.ClearColor(Color.FromArgb(200, Color.LightBlue)); 80 | GL.Clear(ClearBufferMask.DepthBufferBit | ClearBufferMask.ColorBufferBit); 81 | 82 | GL.MatrixMode(MatrixMode.Modelview); 83 | GL.LoadIdentity(); 84 | 85 | _angle += 1f; 86 | GL.Rotate(_angle, Vector3.UnitZ); 87 | GL.Rotate(_angle, Vector3.UnitY); 88 | GL.Rotate(_angle, Vector3.UnitX); 89 | GL.Translate(0.5f, 0, 0); 90 | 91 | GL.CallList(_displayList); 92 | 93 | GL.MatrixMode(MatrixMode.Modelview); 94 | GL.LoadIdentity(); 95 | 96 | GL.Begin(PrimitiveType.Triangles); 97 | 98 | GL.Color4(Color.Green); 99 | GL.Vertex3(0, 300, 0); 100 | GL.Vertex3(0, 0, 0); 101 | GL.Vertex3(300, 0, 0); 102 | 103 | GL.End(); 104 | 105 | Interlocked.Increment(ref _fps); 106 | } 107 | 108 | private void OpenTkControl_OnExceptionOccurred(object sender, UnhandledExceptionEventArgs e) 109 | { 110 | Debug.WriteLine(e.ExceptionObject); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /OpenTkControlExample/OpenTK.dll.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /OpenTkControlExample/OpenTkControlExamples.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {097A4564-77D3-497B-8B84-16E8222F164D} 8 | WinExe 9 | OpenTkControlExample 10 | OpenTkControlExample 11 | v4.5 12 | 512 13 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 14 | 4 15 | true 16 | 17 | 18 | 19 | AnyCPU 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | 28 | 29 | AnyCPU 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | 37 | 38 | 39 | ..\packages\OpenTK.2.0.0\lib\net20\OpenTK.dll 40 | 41 | 42 | 43 | 44 | 4.0 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | MSBuild:Compile 53 | Designer 54 | 55 | 56 | MSBuild:Compile 57 | Designer 58 | 59 | 60 | App.xaml 61 | Code 62 | 63 | 64 | MainWindow.xaml 65 | Code 66 | 67 | 68 | 69 | 70 | Code 71 | 72 | 73 | True 74 | True 75 | Resources.resx 76 | 77 | 78 | True 79 | Settings.settings 80 | True 81 | 82 | 83 | ResXFileCodeGenerator 84 | Resources.Designer.cs 85 | 86 | 87 | 88 | 89 | SettingsSingleFileGenerator 90 | Settings.Designer.cs 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | {06f9fd53-c287-4275-8b98-4b891ea3abdd} 99 | OpenTkControl 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /OpenTkControlExample/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | using System.Windows; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("OpenTkControlExample")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("OpenTkControlExample")] 13 | [assembly: AssemblyCopyright("Copyright © 2017")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | //In order to begin building localizable applications, set 23 | //CultureYouAreCodingWith in your .csproj file 24 | //inside a . For example, if you are using US english 25 | //in your source files, set the to en-US. Then uncomment 26 | //the NeutralResourceLanguage attribute below. Update the "en-US" in 27 | //the line below to match the UICulture setting in the project file. 28 | 29 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] 30 | 31 | 32 | [assembly: ThemeInfo( 33 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 34 | //(used if a resource is not found in the page, 35 | // or application resource dictionaries) 36 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 37 | //(used if a resource is not found in the page, 38 | // app, or any theme specific resource dictionaries) 39 | )] 40 | 41 | 42 | // Version information for an assembly consists of the following four values: 43 | // 44 | // Major Version 45 | // Minor Version 46 | // Build Number 47 | // Revision 48 | // 49 | // You can specify all the values or you can default the Build and Revision Numbers 50 | // by using the '*' as shown below: 51 | // [assembly: AssemblyVersion("1.0.*")] 52 | [assembly: AssemblyVersion("1.0.0.0")] 53 | [assembly: AssemblyFileVersion("1.0.0.0")] 54 | -------------------------------------------------------------------------------- /OpenTkControlExample/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace OpenTkControlExample.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("OpenTkControlExample.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /OpenTkControlExample/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /OpenTkControlExample/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace OpenTkControlExample.Properties { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.1.0.0")] 16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 19 | 20 | public static Settings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /OpenTkControlExample/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /OpenTkControlExample/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenTkControl 2 | 3 | This project aims to make it possible to achieve better performance with OpenTk in WPF and it achieves this by copying the data less than existing solutions, such as the examples here: https://github.com/freakinpenguin/OpenTK-WPF. It also allows for all of the rendering to be performed off of the UI thread to improve responsiveness and provides a variety of settings that can be used to further improve performance. 4 | 5 | ## Getting Started 6 | 7 | 1. Install the nuget package available here: https://www.nuget.org/packages/JayFleischer.OpenTkControl 8 | 2. Add either a ThreadOpenTkControl or UiOpenTkControl to a WPF window 9 | 3. Subscribe to the GlRender event and draw something! 10 | 11 | ## Class Overview 12 | 13 | This library provides two different implementations: 14 | 15 | UiOpenTkControl - Performs all rendering on the UI thread. In general, this version will perform worse, even if it's the only thing drawn on the screen and therefore I recomment using ThreadOpenTkControl, unless you really need rendering to occur on the UI thread for some reason 16 | 17 | ThreadOpenTkControl - Performs all OpenGL rendering on a dedicated update thread to improve performance. 18 | 19 | ## Features 20 | 21 | * Rendering - The GlRender event will be called whenever it is time to render 22 | * Continous Mode - The control will either repaint constantly or only when RequestRepaint is called depending on what Continous is set to. 23 | * Screenshots - Screenshots are rendered separately from the display rendering, which makes it possible for them to have a different size from the display resolution 24 | * Thread Safety - RequestRepaint and Screenshot are thread safe 25 | * Error Reporting - All exceptions are reported to the ExceptionOccurred event 26 | * Other useful properties include FrameRateLimit, OpenGlVersion, MaxPixels, PixelScale and ThreadName 27 | * Changing any of these properties will take effect immediately, except ThreadName and OpenGlVersion 28 | 29 | ## Bugs 30 | 31 | Please report any issues on GitHub. I also welcome pull requests with bug fixes and improvements. 32 | --------------------------------------------------------------------------------