Skip to content

TextConsole

Chuck Walbourn edited this page Jul 10, 2019 · 7 revisions

This is a C++ implementation of a simple console for displaying text. As more text is printed, the text display scrolls up.

Text console test

TextConsole.h

class TextConsole
{
public:
    TextConsole();
    TextConsole(
        ID3D12Device* device,
        DirectX::ResourceUploadBatch& upload,
        const DirectX::RenderTargetState& rtState,
        const wchar_t* fontName,
        D3D12_CPU_DESCRIPTOR_HANDLE cpuDescriptor, D3D12_GPU_DESCRIPTOR_HANDLE gpuDescriptor);

    TextConsole(TextConsole&&) = delete;
    TextConsole& operator= (TextConsole&&) = delete;

    TextConsole(TextConsole const&) = delete;
    TextConsole& operator= (TextConsole const&) = delete;

    void Render(ID3D12GraphicsCommandList* commandList);

    void Clear();

    void Write(const wchar_t *str);
    void WriteLine(const wchar_t *str);
    void Format(const wchar_t* strFormat, ...);

    void SetWindow(const RECT& layout);

    void XM_CALLCONV SetForegroundColor(DirectX::FXMVECTOR color) { DirectX::XMStoreFloat4(&m_textColor, color); }

    void ReleaseDevice();
    void RestoreDevice(
        ID3D12Device* device,
        DirectX::ResourceUploadBatch& upload,
        const DirectX::RenderTargetState& rtState,
        const wchar_t* fontName,
        D3D12_CPU_DESCRIPTOR_HANDLE cpuDescriptor, D3D12_GPU_DESCRIPTOR_HANDLE gpuDescriptor);

    void SetViewport(const D3D12_VIEWPORT& viewPort);

    void SetRotation(DXGI_MODE_ROTATION rotation);

private:
    void ProcessString(const wchar_t* str);
    void IncrementLine();

    RECT                                            m_layout;
    DirectX::XMFLOAT4                               m_textColor;

    unsigned int                                    m_columns;
    unsigned int                                    m_rows;
    unsigned int                                    m_currentColumn;
    unsigned int                                    m_currentLine;

    std::unique_ptr<wchar_t[]>                      m_buffer;
    std::unique_ptr<wchar_t*[]>                     m_lines;
    std::vector<wchar_t>                            m_tempBuffer;

    std::unique_ptr<DirectX::SpriteBatch>           m_batch;
    std::unique_ptr<DirectX::SpriteFont>            m_font;

    std::mutex                                      m_mutex;
};

TextConsole.cpp

#include "TextConsole.h"

#include <assert.h>

using Microsoft::WRL::ComPtr;

using namespace DirectX;

TextConsole::TextConsole()
    : m_textColor(1.f, 1.f, 1.f, 1.f)
{
    Clear();
}

TextConsole::TextConsole(
    ID3D12Device* device,
    ResourceUploadBatch& upload,
    const RenderTargetState& rtState,
    const wchar_t* fontName,
    D3D12_CPU_DESCRIPTOR_HANDLE cpuDescriptor, D3D12_GPU_DESCRIPTOR_HANDLE gpuDescriptor)
    : m_textColor(1.f, 1.f, 1.f, 1.f)
{
    RestoreDevice(device, upload, rtState, fontName, cpuDescriptor, gpuDescriptor);

    Clear();
}

void TextConsole::Render(ID3D12GraphicsCommandList* commandList)
{
    std::lock_guard<std::mutex> lock(m_mutex);

    float lineSpacing = m_font->GetLineSpacing();

    float x = float(m_layout.left);
    float y = float(m_layout.top);

    XMVECTOR color = XMLoadFloat4(&m_textColor);

    m_batch->Begin(commandList);

    unsigned int textLine = unsigned int(m_currentLine - m_rows + m_rows + 1) % m_rows;

    for (unsigned int line = 0; line < m_rows; ++line)
    {
        XMFLOAT2 pos(x, y + lineSpacing * float(line));

        if (*m_lines[textLine])
        {
            m_font->DrawString(m_batch.get(), m_lines[textLine], pos, color);
        }

        textLine = unsigned int(textLine + 1) % m_rows;
    }

    m_batch->End();
}

void TextConsole::Clear()
{
    if (m_buffer)
    {
        memset(m_buffer.get(), 0, sizeof(wchar_t) * (m_columns + 1) * m_rows);
    }

    m_currentColumn = m_currentLine = 0;
}

void TextConsole::Write(const wchar_t *str)
{
    std::lock_guard<std::mutex> lock(m_mutex);

    ProcessString(str);
}

void TextConsole::WriteLine(const wchar_t *str)
{
    std::lock_guard<std::mutex> lock(m_mutex);

    ProcessString(str);
    IncrementLine();
}

void TextConsole::Format(const wchar_t* strFormat, ...)
{
    std::lock_guard<std::mutex> lock(m_mutex);

    va_list argList;
    va_start(argList, strFormat);

    auto len = size_t(_vscwprintf(strFormat, argList) + 1);

    if (m_tempBuffer.size() < len)
        m_tempBuffer.resize(len);

    memset(m_tempBuffer.data(), 0, sizeof(wchar_t) * len);

    vswprintf_s(m_tempBuffer.data(), m_tempBuffer.size(), strFormat, argList);

    va_end(argList);

    ProcessString(m_tempBuffer.data());
}

void TextConsole::SetWindow(const RECT& layout)
{
    m_layout = layout;

    assert(m_font != 0);

    float lineSpacing = m_font->GetLineSpacing();
    unsigned int rows = std::max<unsigned int>(1, static_cast<unsigned int>(float(layout.bottom - layout.top) / lineSpacing));

    RECT fontLayout = m_font->MeasureDrawBounds(L"X", XMFLOAT2(0,0));
    unsigned int columns = std::max<unsigned int>(1, static_cast<unsigned int>(float(layout.right - layout.left) / float(fontLayout.right - fontLayout.left)));

    std::unique_ptr<wchar_t[]> buffer(new wchar_t[(columns + 1) * rows]);
    memset(buffer.get(), 0, sizeof(wchar_t) * (columns + 1) * rows);

    std::unique_ptr<wchar_t*[]> lines(new wchar_t*[rows]);
    for (unsigned int line = 0; line < rows; ++line)
    {
        lines[line] = buffer.get() + (columns + 1) * line;
    }

    if (m_lines)
    {
        unsigned int c = std::min<unsigned int>(columns, m_columns);
        unsigned int r = std::min<unsigned int>(rows, m_rows);

        for (unsigned int line = 0; line < r; ++line)
        {
            memcpy(lines[line], m_lines[line], c * sizeof(wchar_t));
        }
    }

    std::swap(columns, m_columns);
    std::swap(rows, m_rows);
    std::swap(buffer, m_buffer);
    std::swap(lines, m_lines);
}

void TextConsole::ReleaseDevice()
{
    m_batch.reset();
    m_font.reset();
}

void TextConsole::RestoreDevice(
    ID3D12Device* device,
    ResourceUploadBatch& upload,
    const RenderTargetState& rtState,
    const wchar_t* fontName,
    D3D12_CPU_DESCRIPTOR_HANDLE cpuDescriptor, D3D12_GPU_DESCRIPTOR_HANDLE gpuDescriptor)
{
    {
        SpriteBatchPipelineStateDescription pd(rtState);
        m_batch = std::make_unique<SpriteBatch>(device, upload, pd);
    }

    m_font = std::make_unique<SpriteFont>(device, upload, fontName, cpuDescriptor, gpuDescriptor);

    m_font->SetDefaultCharacter(L' ');
}

void TextConsole::SetViewport(const D3D12_VIEWPORT& viewPort)
{
    if (m_batch)
    {
        m_batch->SetViewport(viewPort);
    }
}

void TextConsole::SetRotation(DXGI_MODE_ROTATION rotation)
{
    if (m_batch)
    {
        m_batch->SetRotation(rotation);
    }
}

void TextConsole::ProcessString(const wchar_t* str)
{
    if (!m_lines)
        return;

    float width = float(m_layout.right - m_layout.left);

    for (const wchar_t* ch = str; *ch != 0; ++ch)
    {
        if (*ch == '\n')
        {
            IncrementLine();
            continue;
        }

        bool increment = false;

        if (m_currentColumn >= m_columns)
        {
            increment = true;
        }
        else
        {
            m_lines[m_currentLine][m_currentColumn] = *ch;

            auto fontSize = m_font->MeasureString(m_lines[m_currentLine]);
            if (XMVectorGetX(fontSize) > width)
            {
                m_lines[m_currentLine][m_currentColumn] = L'\0';

                increment = true;
            }
        }

        if (increment)
        {
            IncrementLine();
            m_lines[m_currentLine][0] = *ch;
        }

        ++m_currentColumn;
    }
}

void TextConsole::IncrementLine()
{
    if (!m_lines)
        return;

    m_currentLine = (m_currentLine + 1) % m_rows;
    m_currentColumn = 0;
    memset(m_lines[m_currentLine], 0, sizeof(wchar_t) * (m_columns + 1));
}

Example

To use the text console, add a variable to hold the instance:

std::unique_ptr<DX::TextConsole> m_console;

Create it in your initialization:

m_console = std::make_unique<DX::TextConsole>();

// Optionally set a color other than white with something like:
// m_console->SetForegroundColor(Colors::Yellow);

In the equivalent to CreateDeviceDependentResources:

m_console->RestoreDevice(device, upload, rtState,
    L"consolas.spritefont",
    m_resourceDescriptors->GetCpuHandle(Descriptors::ConsolasFont,
    m_resourceDescriptors->GetGpuHandle(Descriptors::ConsolasFont);

For best results, consider using a monospaced (non-proportional) font. Proportional fonts will work, but might introduce additional newlines and will have a 'ragged' right edge.

In OnDeviceLost:

m_console->ReleaseDevice();

In the equivalent to CreateWindowSizeDependentResources, provide the pixel rectangle where you want the console text rendered:

auto viewport = m_deviceResources->GetScreenViewport();
m_console->SetViewport(viewport);

RECT size = m_deviceResources->GetOutputSize();

m_console->SetWindow(SimpleMath::Viewport::ComputeTitleSafeArea(size.right, size.bottom));

Then in your Render function after clearing the screen and drawing whatever background image you want.

auto heap = m_resourceDescriptors->Heap();
commandList->SetDescriptorHeaps(1, &heap);

...

m_console->Render(commandList);

Wherever you want to add text to the console, use Write, WriteLine, and/or Format:

m_console->WriteLine(L"This is a test");
m_console->WriteLine(L"Line 2");
m_console->Format(L"Time %u, %f ", timer.GetFrameCount(), timer.GetTotalSeconds());

If you want to empty the console text, call Clear.

Threading model

The text console class is thread-safe using a built-in mutex so that you can call Write, WriteLine, Format, or Clear from other threads. Since it uses a SpriteBatch to render, the Render function itself must be run on one thread at a time.

Xbox One

Since Xbox One XDK apps do not have 'lost device' scenarios, you can avoid using RestoreDevice and ReleaseDevice, and just use the alternate constructor in CreateDeviceDependentResources:

m_console = std::make_unique<DX::TextConsole>(device, upload, rtState,
  L"consolas.spritefont",
  m_resourceDescriptors->GetCpuHandle(Descriptors::ConsolasFont),
  m_resourceDescriptors->GetGpuHandle(Descriptors::ConsolasFont));

The example above uses the Viewport function ComputeTitleSafeArea which is important when rendering text or other UI element on televisions. This is of course optional when rendering on a PC or mobile device and can use the full render viewport instead.

UWP

For UWP apps, be sure to set the rotation for mobile devices, laptops, tablets, etc.:

m_console->SetRotation( m_deviceResources->GetRotation() );

For Use

  • Universal Windows Platform apps
  • Windows desktop apps
  • Windows 11
  • Windows 10
  • Xbox One
  • Xbox Series X|S

For Development

  • Visual Studio 2022
  • Visual Studio 2019 (16.11)
  • clang/LLVM v12 - v18
  • MinGW 12.2, 13.2
  • CMake 3.20

Related Projects

DirectX Tool Kit for DirectX 11

DirectXMesh

DirectXTex

DirectXMath

Tools

Test Suite

Model Viewer

Content Exporter

DxCapsViewer

See also

DirectX Landing Page

Clone this wiki locally