Initial Commit
Based off ReinUsesLisp's PR: https://github.com/Ryujinx/Ryujinx/pull/394
This commit is contained in:
parent
2e143365eb
commit
98a81fccdb
10 changed files with 192 additions and 107 deletions
|
@ -4,9 +4,9 @@ namespace Ryujinx.Graphics.Gal
|
|||
{
|
||||
public interface IGalShader
|
||||
{
|
||||
void Create(IGalMemory Memory, long Key, GalShaderType Type);
|
||||
void Create(long KeyA, long KeyB, byte[] BinaryA, byte[] BinaryB, GalShaderType Type);
|
||||
|
||||
void Create(IGalMemory Memory, long VpAPos, long Key, GalShaderType Type);
|
||||
bool TryGetSize(long Key, out long Size);
|
||||
|
||||
IEnumerable<ShaderDeclInfo> GetConstBufferUsage(long Key);
|
||||
IEnumerable<ShaderDeclInfo> GetTextureUsage(long Key);
|
||||
|
|
|
@ -15,7 +15,11 @@ namespace Ryujinx.Graphics.Gal.OpenGL
|
|||
|
||||
public OGLShaderProgram Current;
|
||||
|
||||
private ConcurrentDictionary<long, OGLShaderStage> Stages;
|
||||
private Dictionary<long, List<OGLShaderStage>> Stages;
|
||||
|
||||
private Dictionary<long, OGLShaderStage> TopStages;
|
||||
|
||||
private Dictionary<long, long> TopStageSizes;
|
||||
|
||||
private Dictionary<OGLShaderProgram, int> Programs;
|
||||
|
||||
|
@ -29,61 +33,97 @@ namespace Ryujinx.Graphics.Gal.OpenGL
|
|||
{
|
||||
this.Buffer = Buffer;
|
||||
|
||||
Stages = new ConcurrentDictionary<long, OGLShaderStage>();
|
||||
Stages = new Dictionary<long, List<OGLShaderStage>>();
|
||||
|
||||
TopStages = new Dictionary<long, OGLShaderStage>();
|
||||
|
||||
TopStageSizes = new Dictionary<long, long>();
|
||||
|
||||
Programs = new Dictionary<OGLShaderProgram, int>();
|
||||
}
|
||||
|
||||
public void Create(IGalMemory Memory, long Key, GalShaderType Type)
|
||||
{
|
||||
Stages.GetOrAdd(Key, (Stage) => ShaderStageFactory(Memory, Key, 0, false, Type));
|
||||
}
|
||||
|
||||
public void Create(IGalMemory Memory, long VpAPos, long Key, GalShaderType Type)
|
||||
{
|
||||
Stages.GetOrAdd(Key, (Stage) => ShaderStageFactory(Memory, VpAPos, Key, true, Type));
|
||||
}
|
||||
|
||||
private OGLShaderStage ShaderStageFactory(
|
||||
IGalMemory Memory,
|
||||
long Position,
|
||||
long PositionB,
|
||||
bool IsDualVp,
|
||||
public void Create(
|
||||
long KeyA,
|
||||
long KeyB,
|
||||
byte[] BinaryA,
|
||||
byte[] BinaryB,
|
||||
GalShaderType Type)
|
||||
{
|
||||
long Key = KeyB;
|
||||
|
||||
if (Stages.TryGetValue(Key, out List<OGLShaderStage> Cache))
|
||||
{
|
||||
OGLShaderStage CachedStage = Cache.Find((OGLShaderStage Stage) => Stage.EqualsBinary(BinaryA, BinaryB));
|
||||
|
||||
if (CachedStage != null)
|
||||
{
|
||||
TopStages[Key] = CachedStage;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Cache = new List<OGLShaderStage>();
|
||||
|
||||
Stages.Add(Key, Cache);
|
||||
}
|
||||
|
||||
GlslProgram Program;
|
||||
|
||||
GlslDecompiler Decompiler = new GlslDecompiler();
|
||||
|
||||
int ShaderDumpIndex = ShaderDumper.DumpIndex;
|
||||
int ShaderDumpIndex = ShaderHelper.DumpIndex;
|
||||
|
||||
if (IsDualVp)
|
||||
if (BinaryA != null)
|
||||
{
|
||||
ShaderDumper.Dump(Memory, Position, Type, "a");
|
||||
ShaderDumper.Dump(Memory, PositionB, Type, "b");
|
||||
ShaderHelper.Dump(BinaryA, Type, "a");
|
||||
ShaderHelper.Dump(BinaryB, Type, "b");
|
||||
|
||||
Program = Decompiler.Decompile(Memory, Position, PositionB, Type);
|
||||
Program = Decompiler.Decompile(BinaryA, BinaryB, Type);
|
||||
}
|
||||
else
|
||||
{
|
||||
ShaderDumper.Dump(Memory, Position, Type);
|
||||
ShaderHelper.Dump(BinaryB, Type);
|
||||
|
||||
Program = Decompiler.Decompile(Memory, Position, Type);
|
||||
Program = Decompiler.Decompile(BinaryB, Type);
|
||||
}
|
||||
|
||||
string Code = Program.Code;
|
||||
OGLShaderStage NewStage = new OGLShaderStage(
|
||||
Type,
|
||||
BinaryA,
|
||||
BinaryB,
|
||||
Program.Code,
|
||||
Program.Uniforms,
|
||||
Program.Textures);
|
||||
|
||||
if (ShaderDumper.IsDumpEnabled())
|
||||
Cache.Add(NewStage);
|
||||
|
||||
TopStages[Key] = NewStage;
|
||||
|
||||
if (BinaryA != null)
|
||||
{
|
||||
Code = "//Shader " + ShaderDumpIndex + Environment.NewLine + Code;
|
||||
TopStageSizes[KeyA] = BinaryA.Length;
|
||||
}
|
||||
|
||||
return new OGLShaderStage(Type, Code, Program.Uniforms, Program.Textures);
|
||||
TopStageSizes[KeyB] = BinaryB.Length;
|
||||
}
|
||||
|
||||
public bool TryGetSize(long Key, out long Size)
|
||||
{
|
||||
if (TopStageSizes.TryGetValue(Key, out Size))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Size = 0;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public IEnumerable<ShaderDeclInfo> GetConstBufferUsage(long Key)
|
||||
{
|
||||
if (Stages.TryGetValue(Key, out OGLShaderStage Stage))
|
||||
if (TopStages.TryGetValue(Key, out OGLShaderStage Stage))
|
||||
{
|
||||
return Stage.ConstBufferUsage;
|
||||
}
|
||||
|
@ -93,7 +133,7 @@ namespace Ryujinx.Graphics.Gal.OpenGL
|
|||
|
||||
public IEnumerable<ShaderDeclInfo> GetTextureUsage(long Key)
|
||||
{
|
||||
if (Stages.TryGetValue(Key, out OGLShaderStage Stage))
|
||||
if (TopStages.TryGetValue(Key, out OGLShaderStage Stage))
|
||||
{
|
||||
return Stage.TextureUsage;
|
||||
}
|
||||
|
@ -122,7 +162,7 @@ namespace Ryujinx.Graphics.Gal.OpenGL
|
|||
|
||||
public void Bind(long Key)
|
||||
{
|
||||
if (Stages.TryGetValue(Key, out OGLShaderStage Stage))
|
||||
if (TopStages.TryGetValue(Key, out OGLShaderStage Stage))
|
||||
{
|
||||
Bind(Stage);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using OpenTK.Graphics.OpenGL;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Graphics.Gal.OpenGL
|
||||
{
|
||||
|
@ -17,8 +18,6 @@ namespace Ryujinx.Graphics.Gal.OpenGL
|
|||
{
|
||||
public int Handle { get; private set; }
|
||||
|
||||
public bool IsCompiled { get; private set; }
|
||||
|
||||
public GalShaderType Type { get; private set; }
|
||||
|
||||
public string Code { get; private set; }
|
||||
|
@ -26,13 +25,20 @@ namespace Ryujinx.Graphics.Gal.OpenGL
|
|||
public IEnumerable<ShaderDeclInfo> ConstBufferUsage { get; private set; }
|
||||
public IEnumerable<ShaderDeclInfo> TextureUsage { get; private set; }
|
||||
|
||||
private byte[] BinaryA;
|
||||
private byte[] BinaryB;
|
||||
|
||||
public OGLShaderStage(
|
||||
GalShaderType Type,
|
||||
byte[] BinaryA,
|
||||
byte[] BinaryB,
|
||||
string Code,
|
||||
IEnumerable<ShaderDeclInfo> ConstBufferUsage,
|
||||
IEnumerable<ShaderDeclInfo> TextureUsage)
|
||||
{
|
||||
this.Type = Type;
|
||||
this.BinaryA = BinaryA;
|
||||
this.BinaryB = BinaryB;
|
||||
this.Code = Code;
|
||||
this.ConstBufferUsage = ConstBufferUsage;
|
||||
this.TextureUsage = TextureUsage;
|
||||
|
@ -63,6 +69,21 @@ namespace Ryujinx.Graphics.Gal.OpenGL
|
|||
}
|
||||
}
|
||||
|
||||
public bool EqualsBinary(byte[] BinaryA, byte[] BinaryB)
|
||||
{
|
||||
if (!BinaryB.SequenceEqual(this.BinaryB))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (BinaryA != null)
|
||||
{
|
||||
return BinaryA.SequenceEqual(this.BinaryA);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void CompileAndCheck(int Handle, string Code)
|
||||
{
|
||||
GL.ShaderSource(Handle, Code);
|
||||
|
|
|
@ -109,16 +109,15 @@ namespace Ryujinx.Graphics.Gal.Shader
|
|||
}
|
||||
|
||||
public GlslProgram Decompile(
|
||||
IGalMemory Memory,
|
||||
long VpAPosition,
|
||||
long VpBPosition,
|
||||
byte[] BinaryA,
|
||||
byte[] BinaryB,
|
||||
GalShaderType ShaderType)
|
||||
{
|
||||
Header = new ShaderHeader(Memory, VpAPosition);
|
||||
HeaderB = new ShaderHeader(Memory, VpBPosition);
|
||||
Header = new ShaderHeader(BinaryA);
|
||||
HeaderB = new ShaderHeader(BinaryB);
|
||||
|
||||
Blocks = ShaderDecoder.Decode(Memory, VpAPosition);
|
||||
BlocksB = ShaderDecoder.Decode(Memory, VpBPosition);
|
||||
Blocks = ShaderDecoder.Decode(BinaryA);
|
||||
BlocksB = ShaderDecoder.Decode(BinaryB);
|
||||
|
||||
GlslDecl DeclVpA = new GlslDecl(Blocks, ShaderType, Header);
|
||||
GlslDecl DeclVpB = new GlslDecl(BlocksB, ShaderType, HeaderB);
|
||||
|
@ -128,12 +127,12 @@ namespace Ryujinx.Graphics.Gal.Shader
|
|||
return Decompile();
|
||||
}
|
||||
|
||||
public GlslProgram Decompile(IGalMemory Memory, long Position, GalShaderType ShaderType)
|
||||
public GlslProgram Decompile(byte[] Binary, GalShaderType ShaderType)
|
||||
{
|
||||
Header = new ShaderHeader(Memory, Position);
|
||||
Header = new ShaderHeader(Binary);
|
||||
HeaderB = null;
|
||||
|
||||
Blocks = ShaderDecoder.Decode(Memory, Position);
|
||||
Blocks = ShaderDecoder.Decode(Binary);
|
||||
BlocksB = null;
|
||||
|
||||
Decl = new GlslDecl(Blocks, ShaderType, Header);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ryujinx.Graphics.Gal.Shader
|
||||
|
@ -8,15 +9,13 @@ namespace Ryujinx.Graphics.Gal.Shader
|
|||
|
||||
private const bool AddDbgComments = true;
|
||||
|
||||
public static ShaderIrBlock[] Decode(IGalMemory Memory, long Start)
|
||||
public static ShaderIrBlock[] Decode(byte[] Binary)
|
||||
{
|
||||
Dictionary<int, ShaderIrBlock> Visited = new Dictionary<int, ShaderIrBlock>();
|
||||
Dictionary<int, ShaderIrBlock> VisitedEnd = new Dictionary<int, ShaderIrBlock>();
|
||||
|
||||
Queue<ShaderIrBlock> Blocks = new Queue<ShaderIrBlock>();
|
||||
|
||||
long Beginning = Start + HeaderSize;
|
||||
|
||||
ShaderIrBlock Enqueue(int Position, ShaderIrBlock Source = null)
|
||||
{
|
||||
if (!Visited.TryGetValue(Position, out ShaderIrBlock Output))
|
||||
|
@ -36,13 +35,13 @@ namespace Ryujinx.Graphics.Gal.Shader
|
|||
return Output;
|
||||
}
|
||||
|
||||
ShaderIrBlock Entry = Enqueue(0);
|
||||
ShaderIrBlock Entry = Enqueue((int)HeaderSize);
|
||||
|
||||
while (Blocks.Count > 0)
|
||||
{
|
||||
ShaderIrBlock Current = Blocks.Dequeue();
|
||||
|
||||
FillBlock(Memory, Current, Beginning);
|
||||
FillBlock(Binary, Current, HeaderSize);
|
||||
|
||||
//Set child blocks. "Branch" is the block the branch instruction
|
||||
//points to (when taken), "Next" is the block at the next address,
|
||||
|
@ -136,27 +135,24 @@ namespace Ryujinx.Graphics.Gal.Shader
|
|||
return Graph;
|
||||
}
|
||||
|
||||
private static void FillBlock(IGalMemory Memory, ShaderIrBlock Block, long Beginning)
|
||||
private static void FillBlock(byte[] Binary, ShaderIrBlock Block, long Beginning)
|
||||
{
|
||||
int Position = Block.Position;
|
||||
|
||||
do
|
||||
{
|
||||
//Ignore scheduling instructions, which are written every 32 bytes.
|
||||
if ((Position & 0x1f) == 0)
|
||||
if (((Position - Beginning) & 0x1f) == 0)
|
||||
{
|
||||
Position += 8;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
uint Word0 = (uint)Memory.ReadInt32(Position + Beginning + 0);
|
||||
uint Word1 = (uint)Memory.ReadInt32(Position + Beginning + 4);
|
||||
long OpCode = BitConverter.ToInt64(Binary, Position);
|
||||
|
||||
Position += 8;
|
||||
|
||||
long OpCode = Word0 | (long)Word1 << 32;
|
||||
|
||||
ShaderDecodeFunc Decode = ShaderOpCodeTable.GetDecoder(OpCode);
|
||||
|
||||
if (AddDbgComments)
|
||||
|
|
|
@ -59,13 +59,13 @@ namespace Ryujinx.Graphics.Gal.Shader
|
|||
public bool OmapSampleMask { get; private set; }
|
||||
public bool OmapDepth { get; private set; }
|
||||
|
||||
public ShaderHeader(IGalMemory Memory, long Position)
|
||||
public unsafe ShaderHeader(byte[] Binary)
|
||||
{
|
||||
uint CommonWord0 = (uint)Memory.ReadInt32(Position + 0);
|
||||
uint CommonWord1 = (uint)Memory.ReadInt32(Position + 4);
|
||||
uint CommonWord2 = (uint)Memory.ReadInt32(Position + 8);
|
||||
uint CommonWord3 = (uint)Memory.ReadInt32(Position + 12);
|
||||
uint CommonWord4 = (uint)Memory.ReadInt32(Position + 16);
|
||||
uint CommonWord0 = BitConverter.ToUInt32(Binary, 0);
|
||||
uint CommonWord1 = BitConverter.ToUInt32(Binary, 4);
|
||||
uint CommonWord2 = BitConverter.ToUInt32(Binary, 8);
|
||||
uint CommonWord3 = BitConverter.ToUInt32(Binary, 12);
|
||||
uint CommonWord4 = BitConverter.ToUInt32(Binary, 16);
|
||||
|
||||
SphType = ReadBits(CommonWord0, 0, 5);
|
||||
Version = ReadBits(CommonWord0, 5, 5);
|
||||
|
@ -92,8 +92,8 @@ namespace Ryujinx.Graphics.Gal.Shader
|
|||
StoreReqEnd = ReadBits(CommonWord4, 24, 8);
|
||||
|
||||
//Type 2 (fragment?) reading
|
||||
uint Type2OmapTarget = (uint)Memory.ReadInt32(Position + 72);
|
||||
uint Type2Omap = (uint)Memory.ReadInt32(Position + 76);
|
||||
uint Type2OmapTarget = BitConverter.ToUInt32(Binary, 72);
|
||||
uint Type2Omap = BitConverter.ToUInt32(Binary, 76);
|
||||
|
||||
OmapTargets = new OmapTarget[8];
|
||||
|
||||
|
|
|
@ -3,13 +3,13 @@ using System.IO;
|
|||
|
||||
namespace Ryujinx.Graphics.Gal
|
||||
{
|
||||
static class ShaderDumper
|
||||
public static class ShaderHelper
|
||||
{
|
||||
private static string RuntimeDir;
|
||||
|
||||
public static int DumpIndex { get; private set; } = 1;
|
||||
|
||||
public static void Dump(IGalMemory Memory, long Position, GalShaderType Type, string ExtSuffix = "")
|
||||
public static void Dump(byte[] Binary, GalShaderType Type, string ExtSuffix = "")
|
||||
{
|
||||
if (!IsDumpEnabled())
|
||||
{
|
||||
|
@ -23,47 +23,20 @@ namespace Ryujinx.Graphics.Gal
|
|||
|
||||
DumpIndex++;
|
||||
|
||||
using (FileStream FullFile = File.Create(FullPath))
|
||||
using (FileStream CodeFile = File.Create(CodePath))
|
||||
using (BinaryWriter Writer = new BinaryWriter(CodeFile))
|
||||
{
|
||||
BinaryWriter FullWriter = new BinaryWriter(FullFile);
|
||||
BinaryWriter CodeWriter = new BinaryWriter(CodeFile);
|
||||
long Offset;
|
||||
|
||||
for (long i = 0; i < 0x50; i += 4)
|
||||
for (Offset = 0; Offset + 0x50 < Binary.LongLength; Offset++)
|
||||
{
|
||||
FullWriter.Write(Memory.ReadInt32(Position + i));
|
||||
Writer.Write(Binary[Offset + 0x50]);
|
||||
}
|
||||
|
||||
long Offset = 0;
|
||||
|
||||
ulong Instruction = 0;
|
||||
|
||||
//Dump until a NOP instruction is found
|
||||
while ((Instruction >> 52 & 0xfff8) != 0x50b0)
|
||||
{
|
||||
uint Word0 = (uint)Memory.ReadInt32(Position + 0x50 + Offset + 0);
|
||||
uint Word1 = (uint)Memory.ReadInt32(Position + 0x50 + Offset + 4);
|
||||
|
||||
Instruction = Word0 | (ulong)Word1 << 32;
|
||||
|
||||
//Zero instructions (other kind of NOP) stop immediatly,
|
||||
//this is to avoid two rows of zeroes
|
||||
if (Instruction == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
FullWriter.Write(Instruction);
|
||||
CodeWriter.Write(Instruction);
|
||||
|
||||
Offset += 8;
|
||||
}
|
||||
|
||||
//Align to meet nvdisasm requeriments
|
||||
//Align to meet nvdisasm requirements
|
||||
while (Offset % 0x20 != 0)
|
||||
{
|
||||
FullWriter.Write(0);
|
||||
CodeWriter.Write(0);
|
||||
Writer.Write(0);
|
||||
|
||||
Offset += 4;
|
||||
}
|
|
@ -289,7 +289,14 @@ namespace Ryujinx.Graphics.Graphics3d
|
|||
|
||||
Keys[(int)GalShaderType.Vertex] = VpBPos;
|
||||
|
||||
Gpu.Renderer.Shader.Create(Vmm, VpAPos, VpBPos, GalShaderType.Vertex);
|
||||
if (IsShaderModified(Vmm, VpAPos) || IsShaderModified(Vmm, VpBPos))
|
||||
{
|
||||
byte[] BinaryA = ReadShaderBinary(Vmm, VpAPos);
|
||||
byte[] BinaryB = ReadShaderBinary(Vmm, VpBPos);
|
||||
|
||||
Gpu.Renderer.Shader.Create(VpAPos, VpBPos, BinaryA, BinaryB, GalShaderType.Vertex);
|
||||
}
|
||||
|
||||
Gpu.Renderer.Shader.Bind(VpBPos);
|
||||
|
||||
Index = 2;
|
||||
|
@ -316,7 +323,14 @@ namespace Ryujinx.Graphics.Graphics3d
|
|||
|
||||
Keys[(int)Type] = Key;
|
||||
|
||||
Gpu.Renderer.Shader.Create(Vmm, Key, Type);
|
||||
if (IsShaderModified(Vmm, Key))
|
||||
{
|
||||
byte[] Binary = ReadShaderBinary(Vmm, Key);
|
||||
|
||||
Gpu.Renderer.Shader.Create(0, Key, null, Binary, Type);
|
||||
}
|
||||
|
||||
|
||||
Gpu.Renderer.Shader.Bind(Key);
|
||||
}
|
||||
|
||||
|
@ -1010,5 +1024,48 @@ namespace Ryujinx.Graphics.Graphics3d
|
|||
{
|
||||
Registers[(int)Reg] = Value;
|
||||
}
|
||||
|
||||
private bool IsShaderModified(NvGpuVmm Vmm, long Key)
|
||||
{
|
||||
long Address = Vmm.GetPhysicalAddress(Key);
|
||||
|
||||
if (Gpu.Renderer.Shader.TryGetSize(Address, out long Size))
|
||||
{
|
||||
return Vmm.IsRegionModified(Address, Size, NvGpuBufferType.Shader);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private byte[] ReadShaderBinary(NvGpuVmm Vmm, long Key)
|
||||
{
|
||||
long Size = GetShaderSize(Vmm, Key);
|
||||
|
||||
long Address = Vmm.GetPhysicalAddress(Key);
|
||||
|
||||
return Vmm.ReadBytes(Key, Size);
|
||||
}
|
||||
|
||||
private static long GetShaderSize(NvGpuVmm Vmm, long Position)
|
||||
{
|
||||
const int NopInst = 0x50b0;
|
||||
|
||||
long Offset = 0x50;
|
||||
|
||||
ulong OpCode = 0;
|
||||
|
||||
do
|
||||
{
|
||||
uint Word0 = (uint)Vmm.ReadInt32(Position + Offset + 0);
|
||||
uint Word1 = (uint)Vmm.ReadInt32(Position + Offset + 4);
|
||||
|
||||
OpCode = Word0 | (ulong)Word1 << 32;
|
||||
|
||||
Offset += 8;
|
||||
}
|
||||
while ((OpCode >> 52 & 0xfff8) != NopInst && OpCode != 0);
|
||||
|
||||
return Offset - 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ namespace Ryujinx.Graphics.Memory
|
|||
{
|
||||
public enum NvGpuBufferType
|
||||
{
|
||||
Shader,
|
||||
Index,
|
||||
Vertex,
|
||||
Texture,
|
||||
|
|
|
@ -24,14 +24,12 @@ namespace Ryujinx.ShaderTools
|
|||
case "f": ShaderType = GalShaderType.Fragment; break;
|
||||
}
|
||||
|
||||
using (FileStream FS = new FileStream(args[1], FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
Memory Mem = new Memory(FS);
|
||||
|
||||
GlslProgram Program = Decompiler.Decompile(Mem, 0, ShaderType);
|
||||
byte[] Binary = File.ReadAllBytes(args[1]);
|
||||
|
||||
Console.WriteLine(Program.Code);
|
||||
}
|
||||
GlslProgram Program = Decompiler.Decompile(Binary, ShaderType);
|
||||
|
||||
Console.WriteLine(Program.Code);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
Loading…
Add table
Reference in a new issue