Added buffer and buffer<>, tweaked more documentation
This commit is contained in:
257
Diamond/Buffer.cs
Normal file
257
Diamond/Buffer.cs
Normal file
@@ -0,0 +1,257 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using NLog;
|
||||
using OpenTK.Graphics.OpenGL4;
|
||||
|
||||
namespace Diamond
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrap an OpenGL buffer object
|
||||
/// </summary>
|
||||
public class Buffer : GLObject
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
#region Static
|
||||
|
||||
private static readonly Dictionary<BufferTarget, Buffer> BoundBuffers;
|
||||
|
||||
/// <inheritdoc/>
|
||||
static Buffer()
|
||||
{
|
||||
BoundBuffers = new Dictionary<BufferTarget, Buffer>();
|
||||
foreach (var value in Enum.GetValues(typeof(BufferTarget)).Cast<BufferTarget>())
|
||||
BoundBuffers[value] = null;
|
||||
}
|
||||
|
||||
#region ArrayBuffer
|
||||
|
||||
/// <summary>
|
||||
/// The buffer will be used as a source for vertex data, but the connection is only made when
|
||||
/// glVertexAttribPointer is called. The pointer field of this function is taken as a byte
|
||||
/// offset from the beginning of whatever buffer is currently bound to this target.
|
||||
/// </summary>
|
||||
public static Buffer ArrayBuffer
|
||||
{
|
||||
get => BoundBuffers[BufferTarget.ArrayBuffer];
|
||||
set => Bind(BufferTarget.ArrayBuffer, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ElementArrayBuffer
|
||||
|
||||
/// <summary>
|
||||
/// All rendering functions of the form gl*Draw*Elements* will use the pointer field as a byte
|
||||
/// offset from the beginning of the buffer object bound to this target. The indices used for
|
||||
/// indexed rendering will be taken from the buffer object. Note that this binding target is
|
||||
/// part of a Vertex Array Objects state, so a VAO must be bound before binding a buffer here.
|
||||
/// </summary>
|
||||
public static Buffer ElementArrayBuffer
|
||||
{
|
||||
get => BoundBuffers[BufferTarget.ArrayBuffer];
|
||||
set => Bind(BufferTarget.ElementArrayBuffer, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DrawIndirectBuffer
|
||||
|
||||
/// <summary>
|
||||
/// The buffer bound to this target will be used as the source for the indirect data when performing
|
||||
/// indirect rendering. This is only available in core OpenGL 4.0 or with ARB_draw_indirect.
|
||||
/// </summary>
|
||||
public static Buffer DrawIndirectBuffer
|
||||
{
|
||||
get => BoundBuffers[BufferTarget.DrawIndirectBuffer];
|
||||
set => Bind(BufferTarget.DrawIndirectBuffer, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Bind a buffer to a target. If buffer is null, unbinds the target.
|
||||
/// </summary>
|
||||
/// <param name="target">The binding target</param>
|
||||
/// <param name="buffer">The buffer to bind, or 0 if null</param>
|
||||
public static void Bind(BufferTarget target, Buffer buffer)
|
||||
{
|
||||
GL.BindBuffer(target, buffer?.Id ?? 0);
|
||||
Logger.Debug("Bound {0} to {1}", (object) buffer ?? "default buffer", target);
|
||||
|
||||
BoundBuffers[target] = buffer;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ctor, Delete()
|
||||
|
||||
/// <summary>
|
||||
/// Create a buffer object wrapper
|
||||
/// </summary>
|
||||
internal Buffer()
|
||||
: base(GL.GenBuffer())
|
||||
{
|
||||
Logger.Debug("Created {0}", this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Delete()
|
||||
{
|
||||
Logger.Debug("Disposing {0}", this);
|
||||
GL.DeleteBuffer(Id);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
#region Queries
|
||||
|
||||
/// <summary>
|
||||
/// The usage hint for the current data store
|
||||
/// </summary>
|
||||
public BufferUsageHint Usage => (BufferUsageHint) Get(BufferParameterName.BufferUsage);
|
||||
|
||||
/// <summary>
|
||||
/// The size of the current data store in bytes
|
||||
/// </summary>
|
||||
public int Size => Get(BufferParameterName.BufferSize);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stored
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Get a property of this buffer
|
||||
/// </summary>
|
||||
/// <param name="param">The property to get</param>
|
||||
/// <returns>The int value of the property</returns>
|
||||
public int Get(BufferParameterName param)
|
||||
{
|
||||
ArrayBuffer = this;
|
||||
GL.GetBufferParameter(BufferTarget.ArrayBuffer, param, out int res);
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bind this buffer to a target
|
||||
/// </summary>
|
||||
/// <param name="target">The binding target</param>
|
||||
public void Bind(BufferTarget target) => Bind(target, this);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() =>
|
||||
$"'Buffer {Id}'";
|
||||
|
||||
/// <summary>
|
||||
/// Upload data to this buffer. Deletes the existing data store and creates a new one
|
||||
/// from the marshalled size of T.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Data element type, used with Marshal.SizeOf to calculate size of data store</typeparam>
|
||||
/// <param name="data">Values to upload</param>
|
||||
/// <param name="usage">Usage hint for for this data.</param>
|
||||
// todo: add usage hint documentation
|
||||
public void Upload<T>(T[] data, BufferUsageHint usage = BufferUsageHint.StaticDraw) where T : struct
|
||||
{
|
||||
Logger.Debug("Updating {0} data (replacing store)", this);
|
||||
ArrayBuffer = this;
|
||||
GL.BufferData(BufferTarget.ArrayBuffer, data.SizeInBytes(), data, usage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload the data to this buffer within a range. Does not delete the existing data store.
|
||||
/// </summary>
|
||||
/// <param name="data">The data to upload. The range is also applied to this array.</param>
|
||||
/// <param name="offset">The offset index of data to begin uploading</param>
|
||||
/// <param name="count">The number of T elements to upload</param>
|
||||
/// <typeparam name="T">Data element type. Offsets are applied according to the size of this in bytes.</typeparam>
|
||||
public void Upload<T>(T[] data, int offset, int count) where T : struct
|
||||
{
|
||||
Logger.Debug("Updating {0} data range ({1} for {2}] from type {3}",
|
||||
this, offset, count, typeof(T).Name);
|
||||
ArrayBuffer = this;
|
||||
GL.BufferSubData(BufferTarget.ArrayBuffer, (IntPtr) offset, count, data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Factory Methods
|
||||
|
||||
/// <summary>
|
||||
/// Creates a buffer, initialized with an array
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Data element type.</typeparam>
|
||||
/// <param name="data">Data used to initialize the buffer</param>
|
||||
/// <param name="usage">The usage hint for this buffer</param>
|
||||
/// <returns>The initialized buffer, or null if initialization failed</returns>
|
||||
// todo: add usage hint documentation
|
||||
public static Buffer<T> FromData<T>(T[] data, BufferUsageHint usage = BufferUsageHint.StaticDraw)
|
||||
where T : struct
|
||||
{
|
||||
var buffer = new Buffer<T>();
|
||||
|
||||
buffer.Upload(data, usage);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic overload for Buffer. Caches properties like data, usage, and size.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Data type used for all operations</typeparam>
|
||||
// todo this needs to be made as complete as the non-generic version
|
||||
public sealed class Buffer<T> : Buffer where T : struct
|
||||
{
|
||||
/// <summary>
|
||||
/// Cached usage hint for the buffer's storage
|
||||
/// </summary>
|
||||
public new BufferUsageHint Usage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the buffer data storage in T units.
|
||||
/// </summary>
|
||||
public new int Size { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The data in this buffer. Not copied to the buffer until Upload() is called.
|
||||
/// </summary>
|
||||
public T[] Data { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Upload data to this buffer. Deletes the existing data store and creates a new one.
|
||||
/// </summary>
|
||||
/// <param name="data">Values to upload</param>
|
||||
/// <param name="usage">Usage hint for for this data.</param>
|
||||
// todo: add usage hint documentation
|
||||
public void Upload(T[] data, BufferUsageHint usage = BufferUsageHint.StaticDraw)
|
||||
{
|
||||
base.Upload<T>(data, usage);
|
||||
Data = data;
|
||||
Usage = usage;
|
||||
Size = data.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload the data to this buffer within a range. Does not delete the existing data store.
|
||||
/// </summary>
|
||||
/// <param name="offset">The offset index of data to begin uploading</param>
|
||||
/// <param name="count">The number of T elements to upload</param>
|
||||
public void Upload(int offset, int count)
|
||||
{
|
||||
base.Upload<T>(Data, offset, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<DocumentationFile>bin\Debug\Diamond.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
@@ -51,6 +52,8 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Buffer.cs" />
|
||||
<Compile Include="Extensions.cs" />
|
||||
<Compile Include="GLObject.cs" />
|
||||
<Compile Include="Util\SubArray.cs" />
|
||||
<Compile Include="Shaders\Program.cs" />
|
||||
|
||||
26
Diamond/Extensions.cs
Normal file
26
Diamond/Extensions.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Diamond
|
||||
{
|
||||
internal static class Extensions
|
||||
{
|
||||
private static readonly Dictionary<Type, int> ByteSizes = new Dictionary<Type, int>();
|
||||
|
||||
public static int SizeInBytes(this Type type)
|
||||
{
|
||||
if (!ByteSizes.ContainsKey(type))
|
||||
ByteSizes[type] = Marshal.SizeOf(type);
|
||||
return ByteSizes[type];
|
||||
}
|
||||
|
||||
public static int SizeInBytes<T>(this T[] arr) where T : struct
|
||||
{
|
||||
return typeof(T).SizeInBytes() * arr.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,9 @@ using OpenTK.Graphics;
|
||||
|
||||
namespace Diamond
|
||||
{
|
||||
/// <summary>
|
||||
/// A wrapper class for any OpenGL object
|
||||
/// </summary>
|
||||
public abstract class GLObject : IDisposable
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Diamond.Shaders
|
||||
/// <summary>
|
||||
/// Wrap and OpenGL program object
|
||||
/// </summary>
|
||||
public class Program : GLObject
|
||||
public sealed class Program : GLObject
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
@@ -19,31 +19,36 @@ namespace Diamond.Shaders
|
||||
private static Program _current;
|
||||
|
||||
/// <summary>
|
||||
/// The currently active program
|
||||
/// The currently active program, or null if the default program is active.
|
||||
/// </summary>
|
||||
public static Program Current
|
||||
{
|
||||
get => _current;
|
||||
set
|
||||
{
|
||||
if (value != null && !value.Linked)
|
||||
{
|
||||
Logger.Error("Cannot use {0} because it is not linked", value);
|
||||
value = null;
|
||||
set => Use(value);
|
||||
}
|
||||
|
||||
Logger.Debug("Using {0}", (object) value ?? "default program");
|
||||
GL.UseProgram((_current = value)?.Id ?? 0);
|
||||
/// <summary>
|
||||
/// Use this program. If program is null, use the default program.
|
||||
/// </summary>
|
||||
public static void Use(Program program)
|
||||
{
|
||||
if (program != null && !program.Linked)
|
||||
{
|
||||
Logger.Error("Cannot use {0} because it is not linked", program);
|
||||
program = null;
|
||||
}
|
||||
|
||||
GL.UseProgram(program?.Id ?? 0);
|
||||
Logger.Debug("Using {0}", (object) program ?? "default program");
|
||||
|
||||
_current = program;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor, Delete()
|
||||
#region ctor, Delete()
|
||||
|
||||
/// <summary>
|
||||
/// Create a program object wrapper
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
private Program()
|
||||
: base(GL.CreateProgram())
|
||||
{
|
||||
@@ -64,12 +69,12 @@ namespace Diamond.Shaders
|
||||
#region Queries
|
||||
|
||||
/// <summary>
|
||||
/// The number of active uniforms
|
||||
/// Gets the number of active uniforms
|
||||
/// </summary>
|
||||
public int ActiveUniforms => Get(GetProgramParameterName.ActiveUniforms);
|
||||
|
||||
/// <summary>
|
||||
/// The number of active attributes
|
||||
/// Gets the number of active attributes
|
||||
/// </summary>
|
||||
public int ActiveAttributes => Get(GetProgramParameterName.ActiveAttributes);
|
||||
|
||||
@@ -78,7 +83,7 @@ namespace Diamond.Shaders
|
||||
#region Stored
|
||||
|
||||
/// <summary>
|
||||
/// The InfoLog for this program
|
||||
/// The InfoLog for this program since it was last linked
|
||||
/// </summary>
|
||||
public string InfoLog { get; private set; }
|
||||
|
||||
@@ -94,7 +99,7 @@ namespace Diamond.Shaders
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Get a property of this program
|
||||
/// Get a property of this program. invokes glGetProgram and returns an int result.
|
||||
/// </summary>
|
||||
/// <param name="param">The program property to get</param>
|
||||
/// <returns>The int value of the program property</returns>
|
||||
@@ -105,7 +110,8 @@ namespace Diamond.Shaders
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to link this program
|
||||
/// Try to link this program. If linking fails, the InfoLog is updated, and attribute and uniform caches are reset.
|
||||
/// If linking is successful, attribute and uniform caches are generated
|
||||
/// </summary>
|
||||
public void Link()
|
||||
{
|
||||
@@ -148,11 +154,9 @@ namespace Diamond.Shaders
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use this program
|
||||
/// <para></para>
|
||||
/// Equivalent to <code>Program.Current = value</code>
|
||||
/// Use this program. Equivalent to Program.Use(this);
|
||||
/// </summary>
|
||||
public void Use() => Current = this;
|
||||
public void Use() => Use(this);
|
||||
|
||||
#region Attribute Locations
|
||||
|
||||
@@ -163,18 +167,19 @@ namespace Diamond.Shaders
|
||||
/// Check if this program has an active attribute
|
||||
/// </summary>
|
||||
/// <param name="name">The attribute name</param>
|
||||
/// <returns>Whether the progrma has an active attribute</returns>
|
||||
/// <returns>Whether the program has an active attribute</returns>
|
||||
public bool HasAttribute(string name) => _attributes.ContainsKey(name);
|
||||
|
||||
/// <summary>
|
||||
/// Check if this program has an active uniform
|
||||
/// </summary>
|
||||
/// <param name="name">The uniform name</param>
|
||||
/// <returns>Whether the progrma has an active uniform</returns>
|
||||
/// <returns>Whether the program has an active uniform</returns>
|
||||
public bool HasUniform(string name) => _uniforms.ContainsKey(name);
|
||||
|
||||
/// <summary>
|
||||
/// Get the location of an attribute by name
|
||||
/// Get the location of an attribute by name. Throws InvalidOperationException if the attribute
|
||||
/// is not found. This can be checked with HasAttribute
|
||||
/// </summary>
|
||||
/// <param name="name">The attribute name</param>
|
||||
/// <returns>The location of the attribute</returns>
|
||||
@@ -186,7 +191,8 @@ namespace Diamond.Shaders
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the location of an uniform by name
|
||||
/// Get the location of an uniform by name. Throws InvalidOperationException if the uniform
|
||||
/// is not found. This can be checked with HasUniform
|
||||
/// </summary>
|
||||
/// <param name="name">The uniform name</param>
|
||||
/// <returns>The location of the uniform</returns>
|
||||
@@ -199,11 +205,21 @@ namespace Diamond.Shaders
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Attach a shader to this program
|
||||
/// Attach a shader to this program. If shader is null, does nothing. If shader is not compiled
|
||||
/// it is still attached but program link will likely fail.
|
||||
/// </summary>
|
||||
/// <param name="shader">The shader to attach</param>
|
||||
public void Attach(Shader shader)
|
||||
{
|
||||
if (shader == null)
|
||||
{
|
||||
Logger.Error("Cannot attach null shader to {0}", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shader.Compiled)
|
||||
Logger.Warn("{0} is not compiled. Program link will likely fail.", shader);
|
||||
|
||||
Logger.Debug("Attaching {0} to {1}", shader, this);
|
||||
GL.AttachShader(Id, shader.Id);
|
||||
}
|
||||
@@ -217,14 +233,16 @@ namespace Diamond.Shaders
|
||||
#region Factory Methods
|
||||
|
||||
/// <summary>
|
||||
/// Create and link a program from precompiled shaders
|
||||
/// Create and link a program from precompiled shaders. If any shader is null or uncompiled it is still
|
||||
/// attached, although program link will likely fail.
|
||||
/// </summary>
|
||||
/// <param name="shaders">The shaders used in this program</param>
|
||||
/// <returns>A linked program, or null if initialization failed</returns>
|
||||
public static Program FromShaders(params Shader[] shaders) => FromShaders((IEnumerable<Shader>) shaders);
|
||||
|
||||
/// <summary>
|
||||
/// Create and link a program from precompiled shaders
|
||||
/// Create and link a program from precompiled shaders. If any shader is null or uncompiled it is still
|
||||
/// attached, although program link will likely fail.
|
||||
/// </summary>
|
||||
/// <param name="shaders">The shaders used in this program</param>
|
||||
/// <returns>A linked program, or null if initialization failed</returns>
|
||||
@@ -239,15 +257,7 @@ namespace Diamond.Shaders
|
||||
var program = new Program();
|
||||
|
||||
foreach (var shader in shaders)
|
||||
{
|
||||
if (shader == null)
|
||||
{
|
||||
Logger.Error("One or more shaders is null - cannot create program");
|
||||
program.Dispose();
|
||||
return null;
|
||||
}
|
||||
program.Attach(shader);
|
||||
}
|
||||
|
||||
program.Link();
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Diamond.Shaders
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
#region Constructor, Delete()
|
||||
#region ctor, Delete()
|
||||
|
||||
/// <summary>
|
||||
/// Create a shader object wrapper
|
||||
|
||||
@@ -89,6 +89,7 @@ namespace Diamond.Util
|
||||
return arr;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
if (Length == 0)
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Diamond.Shaders;
|
||||
using Diamond.Util;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenTK;
|
||||
using OpenTK.Graphics;
|
||||
using OpenTK.Graphics.OpenGL4;
|
||||
using Diamond;
|
||||
using Buffer = Diamond.Buffer;
|
||||
|
||||
namespace hexworld
|
||||
{
|
||||
@@ -24,6 +21,7 @@ namespace hexworld
|
||||
}
|
||||
|
||||
private Program _pgm;
|
||||
private Buffer<float> _buf;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnUnload(EventArgs e)
|
||||
@@ -41,7 +39,25 @@ namespace hexworld
|
||||
_pgm = Program.FromFiles("res/obj.fs.glsl", "res/obj.vs.glsl");
|
||||
|
||||
Program.Current = _pgm;
|
||||
Program.Current = null;
|
||||
|
||||
_buf = Buffer.FromData(new float[]
|
||||
{
|
||||
-.8f, -.8f,
|
||||
+.8f, -.8f,
|
||||
+.0f, +.8f
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnRenderFrame(FrameEventArgs e)
|
||||
{
|
||||
base.OnRenderFrame(e);
|
||||
|
||||
GL.Viewport(ClientRectangle);
|
||||
|
||||
GL.Clear(ClearBufferMask.ColorBufferBit);
|
||||
|
||||
SwapBuffers();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,12 @@
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<DefineConstants>TRACE;DEBUG</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
|
||||
<DocumentationFile>
|
||||
</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
|
||||
Reference in New Issue
Block a user