Inhalt der RenderTarget-Textur in ein Bitmap kopieren

Im Moment beschäftige ich mich damit, ein kleines Unittest ähnliches Tool für eine 3D-Engine zu schreiben. Funktionsweise ist relativ einfach: Es wird eine Textur angelegt, in die ganz normal gerendert werden kann. Nach Abschluss des Render-Vorgangs soll der Inhalt der Textur in den Hauptspeicher, z. B. als normale Bitmap geladen werden (ob jetzt WPF oder System.Drawing ist egal..). Dieses entstandene Bild kann man jetzt nutzen, um zu prüfen, ob diverse Render-Schritte korrekt funktioniert haben. So weit, so gut, dass soll dann gar nicht weiter Teil dieses Beitrags sein. Mit was ich mich hier beschäftigen will, ist der Weg, wie man den Inhalt einer Textur von der Grafikkarte in den Hauptspeicher bekommt.

Hört sich einfach an, oder? Es ist aber leider nicht nur ein „Texture.ToBitmap“ oder so etwas Ähnliches. Nein, Problem Nummer eins ist, dass das RenderTarget, welches ich kopieren möchte, folgendermaßen erstellt ist.

/// <summary>
/// Creates a render target texture with the given width and height.
/// </summary>
/// <param name="device">Graphics device.</param>
/// <param name="width">Width of generated texture.</param>
/// <param name="height">Height of generated texture.</param>
public static D3D11.Texture2D CreateRenderTargetTexture(D3D11.Device device, int width, int height, GraphicsConfiguration gfxConfig)
{
    D3D11.Texture2DDescription textureDescription = new D3D11.Texture2DDescription();

    if ((gfxConfig.TryEnableAntialiasing) &&
        (GraphicsCore.Current.Features.IsStandardAntialiasingPossible))
    {
        textureDescription.Width = width;
        textureDescription.Height = height;
        textureDescription.MipLevels = 1;
        textureDescription.ArraySize = 1;
        textureDescription.Format = DXGI.Format.B8G8R8A8_UNorm;
        textureDescription.Usage = D3D11.ResourceUsage.Default;
        textureDescription.SampleDescription = new DXGI.SampleDescription(4, 0);
        textureDescription.BindFlags = D3D11.BindFlags.ShaderResource | D3D11.BindFlags.RenderTarget;
        textureDescription.CpuAccessFlags = D3D11.CpuAccessFlags.None;
        textureDescription.OptionFlags = D3D11.ResourceOptionFlags.None;
    }
    else
    {
        textureDescription.Width = width;
        textureDescription.Height = height;
        textureDescription.MipLevels = 1;
        textureDescription.ArraySize = 1;
        textureDescription.Format = DXGI.Format.B8G8R8A8_UNorm;
        textureDescription.Usage = D3D11.ResourceUsage.Default;
        textureDescription.SampleDescription = new DXGI.SampleDescription(1, 0);
        textureDescription.BindFlags = D3D11.BindFlags.ShaderResource | D3D11.BindFlags.RenderTarget;
        textureDescription.CpuAccessFlags = D3D11.CpuAccessFlags.None;
        textureDescription.OptionFlags = D3D11.ResourceOptionFlags.None;
    }

    return new D3D11.Texture2D(device, textureDescription);
}

Die Aufmerksamkeit möchte ich hier auf die Zuweisungen „.Usage = D3D11.ResourceUsage.Default“ und „.CpuAccessFlags = D3D11.CpuAccessFlags.None“ legen. Laut Msdn sorgen diese Eigenschaften zwar dafür, dass die Gpu möglichst schnell auf diese Textur rendern kann, allerdings schränken sie auch den Zugriff der Cpu auf 0 Lesezugriff ein. Tja, so einfach kommt man dann wohl doch nicht ran.

Der Artikel auf Msdn sagt aber zum Glück, wie man mit der Cpu trotzdem den Inhalt auslesen kann, und zwar muss man sich dazu eine weitere Textur mit der Eigenschaft „.Usage = D3D11.ResourceUsage.Staging“ anlegen. Diese macht die Textur zum Gegenteil des RenderTargets. Ist die Eigenschaft so gesetzt, so hat die Gpu so gut wie keinen Zugriff mehr auf die Textur, die Cpu dagegen kann sie frei auslesen. Das einzige, was die Gpu noch machen kann, ist es, den Inhalt einer anderen Textur dort hinein zu kopieren. Damit schließt sich dann auch der Kreis für mein kleines Vorhaben hier. Man muss also eine Textur mit „.Usage = D3D11.ResourceUsage.Staging“, damit man den Inhalt des RenderTargets dort hinein kopieren kann um anschließend die Daten per Cpu auszulesen.

Das Anlegen einer Textur mit „.Usage = D3D11.ResourceUsage.Staging“ ist relativ einfach. Nachfolgender Code zeigt das Vorgehen kurz.

/// <summary>
/// Creates a staging texture which enables copying data from gpu to cpu memory.
/// </summary>
/// <param name="device">Graphics device.</param>
/// <param name="width">Width of generated texture.</param>
/// <param name="height">Height of generated texture.</param>
public static D3D11.Texture2D CreateStagingTexture(D3D11.Device device, int width, int height)
{
    //For handling of staging resource see
    // http://msdn.microsoft.com/en-us/library/windows/desktop/ff476259(v=vs.85).aspx

    D3D11.Texture2DDescription textureDescription = new D3D11.Texture2DDescription();
    textureDescription.Width = width;
    textureDescription.Height = height;
    textureDescription.MipLevels = 1;
    textureDescription.ArraySize = 1;
    textureDescription.Format = DXGI.Format.B8G8R8A8_UNorm;
    textureDescription.Usage = D3D11.ResourceUsage.Staging;
    textureDescription.SampleDescription = new DXGI.SampleDescription(1, 0);
    textureDescription.BindFlags = D3D11.BindFlags.None;
    textureDescription.CpuAccessFlags = D3D11.CpuAccessFlags.Read;
    textureDescription.OptionFlags = D3D11.ResourceOptionFlags.None;

    return new D3D11.Texture2D(device, textureDescription);
}

ABER: Bei solchen Texturen gibt es ein paar Einschränkungen. So kann z. B. kein Multisampling verwendet werden!

Der nächste Schritt ist nun das Kopieren an sich. Laut Msdn sind dafür Aufrufe von CopyResource oder CopySubresourceRegion zulässig. Das nächste Problem erwähnt dabei Microsoft auch beileufig: Über diesen weg kann man nicht den Inhalt eines RenderTargets mit MultiSampling auf die „Staging“ Textur kopieren. Dieser Weg direkt klappt nur, wenn man für das RenderTarget auch kein Multisampling verwendet. Naja gut, für mich jetzt nicht das große Problem, will man die Funktion aber nutzen, um von der 3D-Szene Screenshots zu machen, ist das Blöd. Einzige Lösung, die mir für dieses Problem einfallen würde, ist die Verwendung einer weiteren Textur, in welche die Texturdaten zunächst per ResolveSubresource kopiert werden können (Hier funktioniert der Weg von Multisampling-Textur auf Non-Multisampling-Textur). Aber gut, zurück zum Thema. Nachfolgend die Aufrufe, die ich verwendet, um den Inhalt des RenderTargets in die Textur zu bekommen, welche dann durch die Cpu ausgelesen werden kann.

//Get and read data from the gpu (create copy helper texture on demand)
if (m_copyHelperTextureStaging == null)
{
    m_copyHelperTextureStaging = GraphicsHelper.CreateStagingTexture(m_device, m_pixelWidth, m_pixelHeight);
}
m_deviceContext.CopyResource(m_renderTarget, m_copyHelperTextureStaging);

Als nächstes folgt jetzt noch die Logik, mit der ich den Inhalt der Textur in den Hauptspeicher lade. In diesem Fall lade ich die Daten auf folgendem Weg in ein System.Drawing.Bitmap.

/// <summary>
/// Takes a screenshot and returns it as a gdi bitmap.
/// </summary>
public GDI.Bitmap TakeScreenshotGdi()
{
    //Get and read data from the gpu (create copy helper texture on demand)
    if (m_copyHelperTextureStaging == null)
    {
        m_copyHelperTextureStaging = GraphicsHelper.CreateStagingTexture(m_device, m_pixelWidth, m_pixelHeight);
    }
    m_deviceContext.CopyResource(m_renderTarget, m_copyHelperTextureStaging);

    //Prepare target bitmap
    GDI.Bitmap result = new GDI.Bitmap(m_pixelWidth, m_pixelHeight);

    SharpDX.DataBox dataBox = m_deviceContext.MapSubresource(m_copyHelperTextureStaging, 0, D3D11.MapMode.Read, D3D11.MapFlags.None);
    try
    {
        //Lock bitmap so it can be accessed for texture loading
        System.Drawing.Imaging.BitmapData bitmapData = result.LockBits(
            new System.Drawing.Rectangle(0, 0, result.Width, result.Height),
            System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
        try
        {
            //Copy bitmap data
            NativeMethods.memcpy(
                bitmapData.Scan0, dataBox.DataPointer, 
                new UIntPtr((uint)(m_pixelWidth * m_pixelHeight * 4)));
        }
        finally
        {
            result.UnlockBits(bitmapData);
        }
    }
    finally
    {
        m_deviceContext.UnmapSubresource(m_copyHelperTextureStaging, 0);
    }

    return result;
}

Der Weg ist relativ einfach. Zunächst weise ich den Treiber per MapSubresource an, den Inhalt der Textur in den Hauptspeicher zu laden. Danach lege ich eine Bitmap an, sperre diese, und kopiere dann den Inhalt der Textur aus dem Speicherbereich von DirectX in die des Bitmap. Fertig. Das NativeMethods.memcpy ist übrigens ein Plattform-Aufruf, welcher einfach nur einen Speicherblock im Hauptspeicher kopiert (die C- und C++-Programmierer kennen sich sofort aus..).

Quellen:

2 Gedanken zu „Inhalt der RenderTarget-Textur in ein Bitmap kopieren“

  1. Hi Roland
    I’m a software team lead at Pacom Systems (www.pacom.com)
    We are currently working on integration between our system and a video management system. Using FlyLeaf code (https://github.com/SuRGeoNix/Flyleaf) to handle and render RTSP stream.
    We ran into a problem with capturing snapshots and recording streams into local file. Simply because we are not that familiar with FFMPEG and video processing in general (not our core knowledge)
    Your post came up as we are desperately trying to overcome this predicament.
    Would you be able to help? Are you available for quick consultation?
    Really need help.
    Thanks!
    Michael Zolotarev

    Antworten
  2. Hi Michael,
    sounds like a very interesting problem. If you can access the render target texture from FlyLeaf then you should be able to take a snapshop from the video.
    I think the TextureUploader class from SeeingSharp 2 can help you as a code sample: https://github.com/RolandKoenig/SeeingSharp2/blob/master/SeeingSharp/Multimedia/Core/_Util/TextureUploader.cs
    This class copies a Texture2D form GPU video memory to system memory (as a MemoryMappedTexture). From there you can continue to create a Bitmap from the MemoryMappedTexture and save it to disc.
    Hope that helps

    Antworten

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.