Search code examples
c#unity-game-enginegpgpuhlsl

3D Buffers in HLSL?


I wanna send a series of integers to HLSL in the form of a 3D array using unity. I've been trying to do this for a couple of days now, but without any gain. I tried to pack the buffers into each other (StructuredBuffer<StructuredBuffer<StructuredBuffer<int>>>), but it simply won't work. And I need to make this thing resizable, so I can't use arrays in structs. What should I do?

EDIT: To clarify a bit more what I am trying to do here, this is a medical program. When you go make a scan of your body, some files are generated. Those files are called DICOM files(.dcm). Those files are given out to a doctor. The doctor should open the program, select all of the DICOM files and load them. Each DICOM file contains an image. However, those images are not as the normal images used in our daily life. Those images are grayscale and each pixel has a value that ranges between -1000 to a couple of thousands, so each pixel is saved as 2 bytes(or an Int16). I need to generate a 3D model of the body that got scanned, so I'm using the Marching Cubes algorithm to generate it(have a look at Polygonising a Scalar Field). The problem is I used to loop over each pixel in about 360 512*512 sized images, which took too much time. I used to read the pixel data from each file once I needed it when I used the CPU. Now I'm trying to make this process occur at runtime. I need to send all of the pixel data to the GPU before processing it. That's my problem. I need the GPU to read data from disk. Because that ain't possible, I need to send 360*512*512*4 bytes of data to the GPU in the form of 3D array of Ints. I'm also planning to keep the data there to avoid retransfer of that huge amount of memory. What should I do? Refer to this link to know more about what I'm doing


Solution

  • From what I've understood, I would suggest to try the following:

    1. Flatten your data (nested buffers are not what you want on your gpu)

    2. Split your data across multiple ComputeBuffers if necessary (when I played around with them on a Nvidia Titan X I could store approximately 1GB of data per buffer. I was rendering a 3D point cloud with 1.5GB of data or something, the 360MBytes of data you mentioned should not be a problem then)

    3. If you need multiple buffers: let them overlap as needed for your marching cubes algorithm

    4. Do all of your calculations in a ComputeShader (I think requires DX11, if you have multiple buffer, run it multiple times and accumulate your results) and then use the results in a standard shader which your call from OnPostRender function (use Graphics.DrawProcedural inside to just draw points or build a mesh on the gpu)

    Edit (Might be interesting to you)

    If you want to append data to a gpu buffer (because you don't know the exact size or you can't write it to the gpu at once), you can use AppendBuffers and a ComputeShader.

    C# Script Fragments:

    struct DataStruct
    {
        ...
    }
    
    DataStruct[] yourData;
    yourData = loadStuff();    
    
    ComputeBuffer tmpBuffer = new ComputeBuffer(512, Marshal.SizeOf(typeof(DataStruct)));
    ComputeBuffer gpuData = new ComputeBuffer(MAX_SIZE, Marshal.SizeOf(typeof(DataStruct)), ComputeBufferType.Append);
    
    for (int i = 0; i < yourData.Length / 512; i++) {
    
        // write data subset to temporary buffer on gpu
        tmpBuffer.SetData(DataStruct.Skip(i*512).Take((i+1)*512).ToArray()); // Use fancy Linq stuff to select data subset
    
        // set up and run compute shader for appending data to "gpuData" buffer
        AppendComputeShader.SetBuffer(0, "inBuffer", tmpBuffer);
        AppendComputeShader.SetBuffer(0, "appendBuffer", gpuData);
        AppendComputeShader.Dispatch(0, 512/8, 1, 1); // 8 = gpu work group size -> use 512/8 work groups
    }
    

    ComputeShader:

    struct DataStruct // replicate struct in shader
    {
        ...
    }
    
    #pragma kernel append
    StructuredBuffer<DataStruct> inBuffer;
    AppendStructuredBuffer<DataStruct> appendBuffer;
    
    [numthreads(8,1,1)]
    void append(int id: SV_DispatchThreadID) {
        appendBuffer.Append(inBuffer[id]);
    }
    

    Note:

    • AppendComputeShader has to be assigned via the Inspector
    • 512 is an arbitrary batch size, there is an upper limit of how much data you can append to a gpu buffer at once, but I think that depends on the hardware (for me it seemed to be 65536 * 4 Bytes)
    • you have to provide a maximum size for gpu buffers (on the Titan X it seems to be ~1GB)