Как выполнить HLSL от C#? - PullRequest
       131

Как выполнить HLSL от C#?

0 голосов
/ 06 августа 2020

Итак, я пишу программу C# в сообществе Visual Studio 2019, однако для некоторых операций я бы хотел, чтобы она выполнялась на графическом процессоре, а не на процессоре. У меня есть небольшой опыт работы с HLSL, поскольку я написал, например, некоторые вычислительные шейдеры для некоторых проектов Unity, однако я действительно не смог найти (через Google) какой-либо способ выполнения кода HLSL из программы C# вне использования Unity.

Предполагая, что я правильно понимаю термин «ядро» (с тем, как он используется в Unity для запуска вычислительных шейдеров), я бы хотел специально: 1: из CPU записать данные в буфер ядра шейдера, 2: запустить ядро ​​определенное количество раз, и 3. Попросите ЦП прочитать несколько буферов из ядра.

Чтобы дать пример того, что я хочу, вот как я могу получить код C# для запуска ядра HLSL с UnityEngine: (Например: в C# он генерирует некоторые случайные числа от -1 до 1, а затем в шейдере умножает каждую запись на 4) C#

using UnityEngine;

public class Test : MonoBehaviour
{
    ComputeBuffer buffer;
    public ComputeShader shader; //Has been set to reference the shader in Unity

    void Start ()
    {
        //Create array of random values from -1 to 1
        int[] v = new int[4 * 4];
        for (int i = 0; i < v.Length; i++)
        {
            v[i] = Random.Range(-1, 2);
        }
        //Create buffer
        buffer = new ComputeBuffer(v.Length, sizeof(int));
        shader.SetBuffer(0, "Result", buffer);
        //Set values of buffer to random values
        buffer.SetData(v);
        //Execute the shader
        shader.Dispatch(0, 4 / 2, 4 / 2, 1);
        //Get values of buffer
        buffer.GetData(v);
        //Dispose buffer
        buffer.Dispose();
        //Print values
        for (int i = 0; i < v.Length; i += 4)
        {
            print(v[i + 0] + "," + v[i + 1] + "," + v[i + 2] + "," + v[i + 3]);
        }
    }
}

HLSL

#pragma kernel CSMain

RWStructuredBuffer<int> Result;

[numthreads(2,2,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    Result[id.x + id.y * 4] = 4 * Result[id.x + id.y * 4];
}

Напечатано:

-4, 4, -4, 0 0, 4, 0, 0 -4, -4, -4, 4 4, 0, 4, 4

Изменить: Вещи, которые я пробовал: 2 недели спустя, и хотя никто не ответил на эти вопросы, я искал решения. Все еще ищу, но хочу обновить это некоторыми способами, которые я видел до сих пор: (Обратите внимание, что я не особо разбирался в некоторых из них, например, потому что они, похоже, не то, что я ищу)

  • Кажется, у Microsoft есть официальное как -to при компиляции вычислительных шейдеров, но для C ++. Если я прав в этом, вероятно, можно было бы использовать его для создания DLL для использования в C#. Однако основная проблема, с которой я столкнулся с этим методом, заключается в том, что он требует знания C ++, которого у меня нет. Тем не менее, я планирую проверить его.

  • Compute Sharp - это пакет NuGet, который утверждает, что может принимать код C#, преобразовывать его в код HLSL , и запустите его на графическом процессоре. Однако это, похоже, не совсем то, что я хочу, поскольку он принимает не код HLSL, а код C#. В любом случае решил проверить это, так как это довольно незначительный недостаток, однако при попытке установить последнюю версию на NuGet (1.3.1), который также является последней стабильной версией, выдает ошибку «Не удалось установить пакет [... ]. Вы пытаетесь установить этот пакет в проект, предназначенный для [.NetFramework версии 4.7.2], но пакет не содержит ссылок на сборки или файлов содержимого, совместимых с этой структурой. [...] ". Поэтому я решил сначала проверить другие методы.

  • SharpDX кажется пакетом NuGet, который позволяет вам работать с кодом HLSL в вашем C#. NET проект. Используя SharpDX.Direct3D11, вы даже можете создавать вычислительные шейдеры. Кажется, именно то, что я хочу, поэтому я больше изучал этот, чем другие. Я только что столкнулся с одной большой проблемой с этим: SharpDX, похоже, очень мало в виде руководств или объяснений о том, как его использовать. Используя то, что Google Translate называет японским примером вычислительного шейдера, использующего SharpDX в качестве чертежа, я смог заставить кое-что работать. Проблема с тем, что мне удалось сделать, заключается в том, что 1) есть части, которые я вообще не понимаю, что они делают, просто без них ничего не работает, и 2) внесение того, что кажется вполне приемлемыми изменениями, например, наличие второй буфер может сломать его причудливыми способами. Плюс, казалось бы, необходимость ссылаться на структурированные буферы через их позицию в скомпилированном шейдере (а не их предварительно скомпилированную позицию), кажется действительно плохой и, по моему небольшому опыту, не удобна для работы. Можно предположить, что большинство проблем, которые у меня есть, легко решаются, если кто-то знает, как правильно использовать пакет, однако тогда это просто возвращает меня к тому, что я действительно не могу найти множество руководств, объяснений или примеров того, как использовать пакет. Если кому-то интересно, вот мой, вероятно, плохой пример использования SharpDX для генерации 4 чисел 0,1,2,3 и умножения их на 4 в шейдере:

    C#

    using System;
    using SharpDX;
    using SharpDX.Direct3D;
    using SharpDX.Direct3D11;
    using SharpDX.D3DCompiler;
    //Based on https://gist.github.com/oguna/624969e732a868ec17f05694012c1b63
    
    namespace C_Sharp_Shader_test
    {
        class Program
        {
            static void Main(string[] args)
            {
                int groupSize = 2; //Needs to match what is written in the shader
                int totalSize = 4; //Needs to be a multiple of groupSize, or else the shader will try to either change part of the array past its length, or not change the last parts of the array
                int elementByteSize = 4; //The size of a single element of the input-data in bytes (An int is made of 4 bytes)
                //Create device
                Device device = new Device(DriverType.Hardware, DeviceCreationFlags.SingleThreaded);
                //Create compute shader
                CompilationResult bytecode = ShaderBytecode.CompileFromFile("Shader.hlsl", "CSMain", "cs_5_0"); //(Gotta have the shader-file Shader.hlsl be copied to the output directory for this to work)
                ComputeShader cs = new ComputeShader(device, bytecode);
                bytecode.Dispose();
                //Create input data (0,1,2,3)
                int[] inputData = new int[totalSize];
                for (int i = 0; i < inputData.Length; i++)
                {
                    inputData[i] = i;
                }
                for (int i = 0; i < inputData.Length; i++)
                {
                    Console.WriteLine(inputData[i]);
                }
                Console.WriteLine("");
                //Create input buffer that has the input data
                BufferDescription inputDesc = new BufferDescription()
                {
                    SizeInBytes = elementByteSize * totalSize, //Size of the buffer in bytes
                    Usage = ResourceUsage.Default, //Lets the buffer be both written and read by the GPU
                    BindFlags = BindFlags.ShaderResource | BindFlags.UnorderedAccess,
                    OptionFlags = ResourceOptionFlags.BufferStructured,
                    StructureByteStride = elementByteSize, //The size of each element in bytes
                    CpuAccessFlags = CpuAccessFlags.Read //Lets the CPU read this buffer
                };
                SharpDX.Direct3D11.Buffer buffer = SharpDX.Direct3D11.Buffer.Create(device, inputData, inputDesc);
                //Create resource view (Seems to just be needed for the buffer)
                ShaderResourceViewDescription srvDesc = new ShaderResourceViewDescription()
                {
                    Format = SharpDX.DXGI.Format.Unknown,
                    Dimension = ShaderResourceViewDimension.Buffer,
                    Buffer = new ShaderResourceViewDescription.BufferResource()
                    {
                        ElementWidth = elementByteSize
                    }
                };
                ShaderResourceView srvs = new ShaderResourceView(device, buffer, srvDesc);
                //Create access view (Seems to just be needed for the buffer)
                UnorderedAccessViewDescription uavDesc = new UnorderedAccessViewDescription()
                {
                    Format = SharpDX.DXGI.Format.Unknown,
                    Dimension = UnorderedAccessViewDimension.Buffer,
                    Buffer = new UnorderedAccessViewDescription.BufferResource()
                    {
                        ElementCount = totalSize
                    }
                };
                UnorderedAccessView uavs = new UnorderedAccessView(device, buffer, uavDesc);
                //Set up shader
                DeviceContext context = device.ImmediateContext;
                context.ComputeShader.Set(cs);
                //Set up shader's buffer
                context.ComputeShader.SetConstantBuffer(0, buffer);
                context.ComputeShader.SetShaderResource(0, srvs);
                context.ComputeShader.SetUnorderedAccessView(0, uavs);
                //Execute shader
                int threadGroupCount = (totalSize + groupSize - 1) / groupSize; // +groupSize-1 to round up
                context.Dispatch(threadGroupCount, 1, 1);
                //Set an array "outputData" equal to the buffer's values
                DataStream ds;
                context.MapSubresource(buffer, MapMode.Read, MapFlags.None, out ds);
                int[] outputData = ds.ReadRange<int>(4);
                //Dispose stuff
                context.ClearState();
                Utilities.Dispose(ref srvs);
                Utilities.Dispose(ref uavs);
                Utilities.Dispose(ref buffer);
                Utilities.Dispose(ref cs);
                Utilities.Dispose(ref device);
                //Print values
                for (int i = 0; i < outputData.Length; i++)
                {
                    Console.WriteLine(outputData[i]);
                }
                //Wait so it doesn't close the console immediately.
                Console.ReadKey();
            }
        }
    }
    

    HLSL

    RWStructuredBuffer<int> Result;
    
    [numthreads(2, 1, 1)]
    void CSMain(uint3 id : SV_DispatchThreadID)
    {
        Result[id.x] = Result[id.x] * 4;
    }
    

    Вывод:

    0 1 2 3 0 4 8 12

1 Ответ

0 голосов
/ 21 августа 2020

Простой пример (не оптимизированный, просто PO C) для использования вычислений в DirextX через SharpDX. Фактический шейдер должен быть похожим ... D. Luna, isbn 978-1-942270-06-5)

using SharpDX;
using SharpDX.D3DCompiler;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using System;
using System.Diagnostics;
using Buffer = SharpDX.Direct3D11.Buffer;

namespace GpGpuDemo.Backend

{
  public class DirectComputeCalculatorWithReadBackSharpDx : IParallelCalculator
  {
    public string Description => "GPU-accelerated via SharpDX/DirectCompute (with readback) ";

    public unsafe void Calculate(float[] arrayA, float[] arrayB, float[] arrayC, Action<string> Report)
    {
        var sw = new Stopwatch();
        int count = arrayA.Length;

        var device = new Device(DriverType.Hardware, DeviceCreationFlags.None);

        const int warpsize = 128;
        string DCShaderSource = @"
        
            StructuredBuffer<float> a;
            StructuredBuffer<float> b;
            RWStructuredBuffer<float> c;
            [numthreads(" + warpsize.ToString() + @",1,1)]
            void VectorAdd(uint3 threadId : SV_DispatchThreadID)
            {
                    uint index = threadId.x;
                    c[index] = a[index] * b[index]+a[index];                                          
            }
        ";

        // Compile the shader.
        var computeShaderCode = ShaderBytecode.Compile(DCShaderSource, "VectorAdd", "cs_5_0", ShaderFlags.None, EffectFlags.None);
        var computeShader = new ComputeShader(device, computeShaderCode);
        device.ImmediateContext.ComputeShader.Set(computeShader);

        // description for input buffers
        var inputBufferDescription = new BufferDescription
        {
            BindFlags = BindFlags.ShaderResource,
            OptionFlags = ResourceOptionFlags.BufferStructured,
            Usage = ResourceUsage.Dynamic,
            CpuAccessFlags = CpuAccessFlags.Write,
            SizeInBytes = count * sizeof(float),
            StructureByteStride = sizeof(float)
        };


        // Description for the output buffer itself, and the view required to bind it to the pipeline.
        var outputBufferDescription = new BufferDescription
        {
            BindFlags = BindFlags.UnorderedAccess,
            OptionFlags = ResourceOptionFlags.BufferStructured,
            Usage = ResourceUsage.Default,
            CpuAccessFlags = CpuAccessFlags.None,
            SizeInBytes = count * sizeof(float),
            StructureByteStride = sizeof(float)
        };



        var stagingBufferDescription = new BufferDescription
        {
            BindFlags = BindFlags.None,
            CpuAccessFlags = CpuAccessFlags.Read,
            OptionFlags = ResourceOptionFlags.BufferStructured,
            SizeInBytes = count * sizeof(float),
            StructureByteStride = sizeof(float),
            Usage = ResourceUsage.Staging,
        };

        var stagingBuffer = new Buffer(device, stagingBufferDescription);
        var outputBuffer = new Buffer(device, outputBufferDescription);

        var outputViewDescription = new UnorderedAccessViewDescription
        {
            Buffer = new UnorderedAccessViewDescription.BufferResource() { FirstElement = 0, Flags = UnorderedAccessViewBufferFlags.None, ElementCount = count },

            Format = SharpDX.DXGI.Format.Unknown,
            Dimension = UnorderedAccessViewDimension.Buffer
        };
        var outputView = new UnorderedAccessView(device, outputBuffer, outputViewDescription);


        float[] DCArrC = new float[count];

        // prepare input buffers

        DataStream dsA;
        fixed (float* aAddress = arrayA)
        {
            dsA = new DataStream((IntPtr)aAddress, System.Buffer.ByteLength(arrayA), true, false);
        }
        var ArrayA = new Buffer(device, dsA, inputBufferDescription);
        var ArrayAView = new ShaderResourceView(device, ArrayA);

        DataStream dsB;
        fixed (float* bAddress = arrayB)
        {
            dsB = new DataStream((IntPtr)bAddress, System.Buffer.ByteLength(arrayB), true, false);
        }
        var ArrayB = new Buffer(device, dsB, inputBufferDescription);
        var ArrayBView = new ShaderResourceView(device, ArrayB);

        DataBox output;
        device.ImmediateContext.ComputeShader.SetUnorderedAccessView(0, outputView);
        device.ImmediateContext.ComputeShader.SetShaderResource(0, ArrayAView);
        device.ImmediateContext.ComputeShader.SetShaderResource(1, ArrayBView);

        for (int i = 0; i < 5; i++)
        {
            sw.Restart();
            for (int teller = 0; teller < 10; teller++)
            {
                device.ImmediateContext.Dispatch(count / warpsize, 1, 1);
            }
            device.ImmediateContext.CopyResource(outputBuffer, stagingBuffer);
            DataStream result;
            output = device.ImmediateContext.MapSubresource(stagingBuffer, MapMode.Read, MapFlags.None, out result);
            fixed (float* cAddress = arrayC)
            {
                result.Read((IntPtr)cAddress, 0, System.Buffer.ByteLength(arrayC));
            }
            device.ImmediateContext.UnmapSubresource(stagingBuffer, 0);
            sw.Stop();
            var s = sw.Elapsed;

            Report($"Operation finished in {s.Minutes} minutes, {s.Seconds} seconds, {s.Milliseconds} milliseconds");
        }
        ArrayA.Dispose();
        ArrayB.Dispose();
        dsA.Dispose();
        dsB.Dispose();
    }
  }
}

Вы можете написать мне для получения полного рабочего решения, сравнивая выполнение на ЦП (однопоточное и многопоточное , в OpenCL и DirectCompute)

...